diff options
Diffstat (limited to 'src/com/android/messaging/sms')
16 files changed, 0 insertions, 7022 deletions
diff --git a/src/com/android/messaging/sms/ApnDatabase.java b/src/com/android/messaging/sms/ApnDatabase.java deleted file mode 100644 index a8d0d0c..0000000 --- a/src/com/android/messaging/sms/ApnDatabase.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * 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); - } -} diff --git a/src/com/android/messaging/sms/ApnsXmlProcessor.java b/src/com/android/messaging/sms/ApnsXmlProcessor.java deleted file mode 100644 index 976896c..0000000 --- a/src/com/android/messaging/sms/ApnsXmlProcessor.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * 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); - } - } -} diff --git a/src/com/android/messaging/sms/BugleApnSettingsLoader.java b/src/com/android/messaging/sms/BugleApnSettingsLoader.java deleted file mode 100644 index 43c95ac..0000000 --- a/src/com/android/messaging/sms/BugleApnSettingsLoader.java +++ /dev/null @@ -1,646 +0,0 @@ -/* - * 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; - } -} diff --git a/src/com/android/messaging/sms/BugleCarrierConfigValuesLoader.java b/src/com/android/messaging/sms/BugleCarrierConfigValuesLoader.java deleted file mode 100644 index ac6c7e4..0000000 --- a/src/com/android/messaging/sms/BugleCarrierConfigValuesLoader.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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); - } - } -} diff --git a/src/com/android/messaging/sms/BugleUserAgentInfoLoader.java b/src/com/android/messaging/sms/BugleUserAgentInfoLoader.java deleted file mode 100644 index a8dc5c4..0000000 --- a/src/com/android/messaging/sms/BugleUserAgentInfoLoader.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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); - } - } -} diff --git a/src/com/android/messaging/sms/DatabaseMessages.java b/src/com/android/messaging/sms/DatabaseMessages.java deleted file mode 100644 index 0f662e5..0000000 --- a/src/com/android/messaging/sms/DatabaseMessages.java +++ /dev/null @@ -1,1006 +0,0 @@ -/* - * 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.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.provider.Telephony.Mms; -import android.provider.Telephony.Sms; -import android.text.TextUtils; -import android.util.Log; -import android.webkit.MimeTypeMap; - -import com.android.messaging.Factory; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.media.VideoThumbnailRequest; -import com.android.messaging.mmslib.pdu.CharacterSets; -import com.android.messaging.util.Assert; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.MediaMetadataRetrieverWrapper; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.google.common.collect.Lists; - -import java.io.ByteArrayOutputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -/** - * Class contains various SMS/MMS database entities from telephony provider - */ -public class DatabaseMessages { - private static final String TAG = LogUtil.BUGLE_TAG; - - public abstract static class DatabaseMessage { - public abstract int getProtocol(); - public abstract String getUri(); - public abstract long getTimestampInMillis(); - - @Override - public boolean equals(final Object other) { - if (other == null || !(other instanceof DatabaseMessage)) { - return false; - } - final DatabaseMessage otherDbMsg = (DatabaseMessage) other; - // No need to check timestamp since we only need this when we compare - // messages at the same timestamp - return TextUtils.equals(getUri(), otherDbMsg.getUri()); - } - - @Override - public int hashCode() { - // No need to check timestamp since we only need this when we compare - // messages at the same timestamp - return getUri().hashCode(); - } - } - - /** - * SMS message - */ - public static class SmsMessage extends DatabaseMessage implements Parcelable { - private static int sIota = 0; - public static final int INDEX_ID = sIota++; - public static final int INDEX_TYPE = sIota++; - public static final int INDEX_ADDRESS = sIota++; - public static final int INDEX_BODY = sIota++; - public static final int INDEX_DATE = sIota++; - public static final int INDEX_THREAD_ID = sIota++; - public static final int INDEX_STATUS = sIota++; - public static final int INDEX_READ = sIota++; - public static final int INDEX_SEEN = sIota++; - public static final int INDEX_DATE_SENT = sIota++; - public static final int INDEX_SUB_ID = sIota++; - - private static String[] sProjection; - - public static String[] getProjection() { - if (sProjection == null) { - String[] projection = new String[] { - Sms._ID, - Sms.TYPE, - Sms.ADDRESS, - Sms.BODY, - Sms.DATE, - Sms.THREAD_ID, - Sms.STATUS, - Sms.READ, - Sms.SEEN, - Sms.DATE_SENT, - Sms.SUBSCRIPTION_ID, - }; - if (!MmsUtils.hasSmsDateSentColumn()) { - projection[INDEX_DATE_SENT] = Sms.DATE; - } - if (!OsUtil.isAtLeastL_MR1()) { - Assert.equals(INDEX_SUB_ID, projection.length - 1); - String[] withoutSubId = new String[projection.length - 1]; - System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); - projection = withoutSubId; - } - - sProjection = projection; - } - - return sProjection; - } - - public String mUri; - public String mAddress; - public String mBody; - private long mRowId; - public long mTimestampInMillis; - public long mTimestampSentInMillis; - public int mType; - public long mThreadId; - public int mStatus; - public boolean mRead; - public boolean mSeen; - public int mSubId; - - private SmsMessage() { - } - - /** - * Load from a cursor of a query that returns the SMS to import - * - * @param cursor - */ - private void load(final Cursor cursor) { - mRowId = cursor.getLong(INDEX_ID); - mAddress = cursor.getString(INDEX_ADDRESS); - mBody = cursor.getString(INDEX_BODY); - mTimestampInMillis = cursor.getLong(INDEX_DATE); - // Before ICS, there is no "date_sent" so use copy of "date" value - mTimestampSentInMillis = cursor.getLong(INDEX_DATE_SENT); - mType = cursor.getInt(INDEX_TYPE); - mThreadId = cursor.getLong(INDEX_THREAD_ID); - mStatus = cursor.getInt(INDEX_STATUS); - mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; - mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; - mUri = ContentUris.withAppendedId(Sms.CONTENT_URI, mRowId).toString(); - mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); - } - - /** - * Get a new SmsMessage by loading from the cursor of a query - * that returns the SMS to import - * - * @param cursor - * @return - */ - public static SmsMessage get(final Cursor cursor) { - final SmsMessage msg = new SmsMessage(); - msg.load(cursor); - return msg; - } - - @Override - public String getUri() { - return mUri; - } - - public int getSubId() { - return mSubId; - } - - @Override - public int getProtocol() { - return MessageData.PROTOCOL_SMS; - } - - @Override - public long getTimestampInMillis() { - return mTimestampInMillis; - } - - @Override - public int describeContents() { - return 0; - } - - private SmsMessage(final Parcel in) { - mUri = in.readString(); - mRowId = in.readLong(); - mTimestampInMillis = in.readLong(); - mTimestampSentInMillis = in.readLong(); - mType = in.readInt(); - mThreadId = in.readLong(); - mStatus = in.readInt(); - mRead = in.readInt() != 0; - mSeen = in.readInt() != 0; - mSubId = in.readInt(); - - // SMS specific - mAddress = in.readString(); - mBody = in.readString(); - } - - public static final Parcelable.Creator<SmsMessage> CREATOR - = new Parcelable.Creator<SmsMessage>() { - @Override - public SmsMessage createFromParcel(final Parcel in) { - return new SmsMessage(in); - } - - @Override - public SmsMessage[] newArray(final int size) { - return new SmsMessage[size]; - } - }; - - @Override - public void writeToParcel(final Parcel out, final int flags) { - out.writeString(mUri); - out.writeLong(mRowId); - out.writeLong(mTimestampInMillis); - out.writeLong(mTimestampSentInMillis); - out.writeInt(mType); - out.writeLong(mThreadId); - out.writeInt(mStatus); - out.writeInt(mRead ? 1 : 0); - out.writeInt(mSeen ? 1 : 0); - out.writeInt(mSubId); - - // SMS specific - out.writeString(mAddress); - out.writeString(mBody); - } - } - - /** - * MMS message - */ - public static class MmsMessage extends DatabaseMessage implements Parcelable { - private static int sIota = 0; - public static final int INDEX_ID = sIota++; - public static final int INDEX_MESSAGE_BOX = sIota++; - public static final int INDEX_SUBJECT = sIota++; - public static final int INDEX_SUBJECT_CHARSET = sIota++; - public static final int INDEX_MESSAGE_SIZE = sIota++; - public static final int INDEX_DATE = sIota++; - public static final int INDEX_DATE_SENT = sIota++; - public static final int INDEX_THREAD_ID = sIota++; - public static final int INDEX_PRIORITY = sIota++; - public static final int INDEX_STATUS = sIota++; - public static final int INDEX_READ = sIota++; - public static final int INDEX_SEEN = sIota++; - public static final int INDEX_CONTENT_LOCATION = sIota++; - public static final int INDEX_TRANSACTION_ID = sIota++; - public static final int INDEX_MESSAGE_TYPE = sIota++; - public static final int INDEX_EXPIRY = sIota++; - public static final int INDEX_RESPONSE_STATUS = sIota++; - public static final int INDEX_RETRIEVE_STATUS = sIota++; - public static final int INDEX_SUB_ID = sIota++; - - private static String[] sProjection; - - public static String[] getProjection() { - if (sProjection == null) { - String[] projection = new String[] { - Mms._ID, - Mms.MESSAGE_BOX, - Mms.SUBJECT, - Mms.SUBJECT_CHARSET, - Mms.MESSAGE_SIZE, - Mms.DATE, - Mms.DATE_SENT, - Mms.THREAD_ID, - Mms.PRIORITY, - Mms.STATUS, - Mms.READ, - Mms.SEEN, - Mms.CONTENT_LOCATION, - Mms.TRANSACTION_ID, - Mms.MESSAGE_TYPE, - Mms.EXPIRY, - Mms.RESPONSE_STATUS, - Mms.RETRIEVE_STATUS, - Mms.SUBSCRIPTION_ID, - }; - - if (!OsUtil.isAtLeastL_MR1()) { - Assert.equals(INDEX_SUB_ID, projection.length - 1); - String[] withoutSubId = new String[projection.length - 1]; - System.arraycopy(projection, 0, withoutSubId, 0, withoutSubId.length); - projection = withoutSubId; - } - - sProjection = projection; - } - - return sProjection; - } - - public String mUri; - private long mRowId; - public int mType; - public String mSubject; - public int mSubjectCharset; - private long mSize; - public long mTimestampInMillis; - public long mSentTimestampInMillis; - public long mThreadId; - public int mPriority; - public int mStatus; - public boolean mRead; - public boolean mSeen; - public String mContentLocation; - public String mTransactionId; - public int mMmsMessageType; - public long mExpiryInMillis; - public int mSubId; - public String mSender; - public int mResponseStatus; - public int mRetrieveStatus; - - public List<MmsPart> mParts = Lists.newArrayList(); - private boolean mPartsProcessed = false; - - private MmsMessage() { - } - - /** - * Load from a cursor of a query that returns the MMS to import - * - * @param cursor - */ - public void load(final Cursor cursor) { - mRowId = cursor.getLong(INDEX_ID); - mType = cursor.getInt(INDEX_MESSAGE_BOX); - mSubject = cursor.getString(INDEX_SUBJECT); - mSubjectCharset = cursor.getInt(INDEX_SUBJECT_CHARSET); - if (!TextUtils.isEmpty(mSubject)) { - // PduPersister stores the subject using ISO_8859_1 - // Let's load it using that encoding and convert it back to its original - // See PduPersister.persist and PduPersister.toIsoString - // (Refer to bug b/11162476) - mSubject = getDecodedString( - getStringBytes(mSubject, CharacterSets.ISO_8859_1), mSubjectCharset); - } - mSize = cursor.getLong(INDEX_MESSAGE_SIZE); - // MMS db times are in seconds - mTimestampInMillis = cursor.getLong(INDEX_DATE) * 1000; - mSentTimestampInMillis = cursor.getLong(INDEX_DATE_SENT) * 1000; - mThreadId = cursor.getLong(INDEX_THREAD_ID); - mPriority = cursor.getInt(INDEX_PRIORITY); - mStatus = cursor.getInt(INDEX_STATUS); - mRead = cursor.getInt(INDEX_READ) == 0 ? false : true; - mSeen = cursor.getInt(INDEX_SEEN) == 0 ? false : true; - mContentLocation = cursor.getString(INDEX_CONTENT_LOCATION); - mTransactionId = cursor.getString(INDEX_TRANSACTION_ID); - mMmsMessageType = cursor.getInt(INDEX_MESSAGE_TYPE); - mExpiryInMillis = cursor.getLong(INDEX_EXPIRY) * 1000; - mResponseStatus = cursor.getInt(INDEX_RESPONSE_STATUS); - mRetrieveStatus = cursor.getInt(INDEX_RETRIEVE_STATUS); - // Clear all parts in case we reuse this object - mParts.clear(); - mPartsProcessed = false; - mUri = ContentUris.withAppendedId(Mms.CONTENT_URI, mRowId).toString(); - mSubId = PhoneUtils.getDefault().getSubIdFromTelephony(cursor, INDEX_SUB_ID); - } - - /** - * Get a new MmsMessage by loading from the cursor of a query - * that returns the MMS to import - * - * @param cursor - * @return - */ - public static MmsMessage get(final Cursor cursor) { - final MmsMessage msg = new MmsMessage(); - msg.load(cursor); - return msg; - } - /** - * Add a loaded MMS part - * - * @param part - */ - public void addPart(final MmsPart part) { - mParts.add(part); - } - - public List<MmsPart> getParts() { - return mParts; - } - - public long getSize() { - if (!mPartsProcessed) { - processParts(); - } - return mSize; - } - - /** - * Process loaded MMS parts to obtain the combined text, the combined attachment url, - * the combined content type and the combined size. - */ - private void processParts() { - if (mPartsProcessed) { - return; - } - mPartsProcessed = true; - // Remember the width and height of the first media part - // These are needed when building attachment list - long sizeOfParts = 0L; - for (final MmsPart part : mParts) { - sizeOfParts += part.mSize; - } - if (mSize <= 0) { - mSize = mSubject != null ? mSubject.getBytes().length : 0L; - mSize += sizeOfParts; - } - } - - @Override - public String getUri() { - return mUri; - } - - public long getId() { - return mRowId; - } - - public int getSubId() { - return mSubId; - } - - @Override - public int getProtocol() { - return MessageData.PROTOCOL_MMS; - } - - @Override - public long getTimestampInMillis() { - return mTimestampInMillis; - } - - @Override - public int describeContents() { - return 0; - } - - public void setSender(final String sender) { - mSender = sender; - } - - private MmsMessage(final Parcel in) { - mUri = in.readString(); - mRowId = in.readLong(); - mTimestampInMillis = in.readLong(); - mSentTimestampInMillis = in.readLong(); - mType = in.readInt(); - mThreadId = in.readLong(); - mStatus = in.readInt(); - mRead = in.readInt() != 0; - mSeen = in.readInt() != 0; - mSubId = in.readInt(); - - // MMS specific - mSubject = in.readString(); - mContentLocation = in.readString(); - mTransactionId = in.readString(); - mSender = in.readString(); - - mSize = in.readLong(); - mExpiryInMillis = in.readLong(); - - mSubjectCharset = in.readInt(); - mPriority = in.readInt(); - mMmsMessageType = in.readInt(); - mResponseStatus = in.readInt(); - mRetrieveStatus = in.readInt(); - - final int nParts = in.readInt(); - mParts = new ArrayList<MmsPart>(); - mPartsProcessed = false; - for (int i = 0; i < nParts; i++) { - mParts.add((MmsPart) in.readParcelable(getClass().getClassLoader())); - } - } - - public static final Parcelable.Creator<MmsMessage> CREATOR - = new Parcelable.Creator<MmsMessage>() { - @Override - public MmsMessage createFromParcel(final Parcel in) { - return new MmsMessage(in); - } - - @Override - public MmsMessage[] newArray(final int size) { - return new MmsMessage[size]; - } - }; - - @Override - public void writeToParcel(final Parcel out, final int flags) { - out.writeString(mUri); - out.writeLong(mRowId); - out.writeLong(mTimestampInMillis); - out.writeLong(mSentTimestampInMillis); - out.writeInt(mType); - out.writeLong(mThreadId); - out.writeInt(mStatus); - out.writeInt(mRead ? 1 : 0); - out.writeInt(mSeen ? 1 : 0); - out.writeInt(mSubId); - - out.writeString(mSubject); - out.writeString(mContentLocation); - out.writeString(mTransactionId); - out.writeString(mSender); - - out.writeLong(mSize); - out.writeLong(mExpiryInMillis); - - out.writeInt(mSubjectCharset); - out.writeInt(mPriority); - out.writeInt(mMmsMessageType); - out.writeInt(mResponseStatus); - out.writeInt(mRetrieveStatus); - - out.writeInt(mParts.size()); - for (final MmsPart part : mParts) { - out.writeParcelable(part, 0); - } - } - } - - /** - * Part of an MMS message - */ - public static class MmsPart implements Parcelable { - public static final String[] PROJECTION = new String[] { - Mms.Part._ID, - Mms.Part.MSG_ID, - Mms.Part.CHARSET, - Mms.Part.CONTENT_TYPE, - Mms.Part.TEXT, - }; - private static int sIota = 0; - public static final int INDEX_ID = sIota++; - public static final int INDEX_MSG_ID = sIota++; - public static final int INDEX_CHARSET = sIota++; - public static final int INDEX_CONTENT_TYPE = sIota++; - public static final int INDEX_TEXT = sIota++; - - public String mUri; - public long mRowId; - public long mMessageId; - public String mContentType; - public String mText; - public int mCharset; - private int mWidth; - private int mHeight; - public long mSize; - - private MmsPart() { - } - - /** - * Load from a cursor of a query that returns the MMS part to import - * - * @param cursor - */ - public void load(final Cursor cursor, final boolean loadMedia) { - mRowId = cursor.getLong(INDEX_ID); - mMessageId = cursor.getLong(INDEX_MSG_ID); - mContentType = cursor.getString(INDEX_CONTENT_TYPE); - mText = cursor.getString(INDEX_TEXT); - mCharset = cursor.getInt(INDEX_CHARSET); - mWidth = 0; - mHeight = 0; - mSize = 0; - if (isMedia()) { - // For importing we don't load media since performance is critical - // For loading when we receive mms, we do load media to get enough - // information of the media file - if (loadMedia) { - if (ContentType.isImageType(mContentType)) { - loadImage(); - } else if (ContentType.isVideoType(mContentType)) { - loadVideo(); - } // No need to load audio for parsing - mSize = MmsUtils.getMediaFileSize(getDataUri()); - } - } else { - // Load text if not media type - loadText(); - } - mUri = Uri.withAppendedPath(Mms.CONTENT_URI, cursor.getString(INDEX_ID)).toString(); - } - - /** - * Get content type from file extension - */ - private static String extractContentType(final Context context, final Uri uri) { - final String path = uri.getPath(); - final MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton(); - String extension = MimeTypeMap.getFileExtensionFromUrl(path); - if (TextUtils.isEmpty(extension)) { - // getMimeTypeFromExtension() doesn't handle spaces in filenames nor can it handle - // urlEncoded strings. Let's try one last time at finding the extension. - final int dotPos = path.lastIndexOf('.'); - if (0 <= dotPos) { - extension = path.substring(dotPos + 1); - } - } - return mimeTypeMap.getMimeTypeFromExtension(extension); - } - - /** - * Get text of a text part - */ - private void loadText() { - byte[] data = null; - if (isEmbeddedTextType()) { - // Embedded text, get from the "text" column - if (!TextUtils.isEmpty(mText)) { - data = getStringBytes(mText, mCharset); - } - } else { - // Not embedded, load from disk - final ContentResolver resolver = - Factory.get().getApplicationContext().getContentResolver(); - final Uri uri = getDataUri(); - InputStream is = null; - final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - is = resolver.openInputStream(uri); - final byte[] buffer = new byte[256]; - int len = is.read(buffer); - while (len >= 0) { - baos.write(buffer, 0, len); - len = is.read(buffer); - } - } catch (final IOException e) { - LogUtil.e(TAG, - "DatabaseMessages.MmsPart: loading text from file failed: " + e, e); - } finally { - if (is != null) { - try { - is.close(); - } catch (final IOException e) { - LogUtil.e(TAG, "DatabaseMessages.MmsPart: close file failed: " + e, e); - } - } - } - data = baos.toByteArray(); - } - if (data != null && data.length > 0) { - mSize = data.length; - mText = getDecodedString(data, mCharset); - } - } - - /** - * Load image file of an image part and parse the dimensions and type - */ - private void loadImage() { - final Context context = Factory.get().getApplicationContext(); - final ContentResolver resolver = context.getContentResolver(); - final Uri uri = getDataUri(); - // We have to get the width and height of the image -- they're needed when adding - // an attachment in bugle. - InputStream is = null; - try { - is = resolver.openInputStream(uri); - final BitmapFactory.Options opt = new BitmapFactory.Options(); - opt.inJustDecodeBounds = true; - BitmapFactory.decodeStream(is, null, opt); - mContentType = opt.outMimeType; - mWidth = opt.outWidth; - mHeight = opt.outHeight; - if (TextUtils.isEmpty(mContentType)) { - // BitmapFactory couldn't figure out the image type. That's got to be a bad - // sign, but see if we can figure it out from the file extension. - mContentType = extractContentType(context, uri); - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "DatabaseMessages.MmsPart.loadImage: file not found", e); - } finally { - if (is != null) { - try { - is.close(); - } catch (final IOException e) { - Log.e(TAG, "IOException caught while closing stream", e); - } - } - } - } - - /** - * Load video file of a video part and parse the dimensions and type - */ - private void loadVideo() { - // This is a coarse check, and should not be applied to outgoing messages. However, - // currently, this does not cause any problems. - if (!VideoThumbnailRequest.shouldShowIncomingVideoThumbnails()) { - return; - } - final Uri uri = getDataUri(); - final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); - try { - retriever.setDataSource(uri); - // FLAG: This inadvertently fixes a problem with phone receiving audio - // messages on some carrier. We should handle this in a less accidental way so that - // we don't break it again. (The carrier changes the content type in the wrapper - // in-transit from audio/mp4 to video/3gpp without changing the data) - // Also note: There is a bug in some OEM device where mmr returns - // video/ffmpeg for image files. That shouldn't happen here but be aware. - mContentType = - retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE); - final Bitmap bitmap = retriever.getFrameAtTime(-1); - if (bitmap != null) { - mWidth = bitmap.getWidth(); - mHeight = bitmap.getHeight(); - } else { - // Get here if it's not actually video (see above) - LogUtil.i(LogUtil.BUGLE_TAG, "loadVideo: Got null bitmap from " + uri); - } - } catch (IOException e) { - LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting metadata from " + uri, e); - } finally { - retriever.release(); - } - } - - /** - * Get media file size - */ - private long getMediaFileSize() { - final Context context = Factory.get().getApplicationContext(); - final Uri uri = getDataUri(); - AssetFileDescriptor fd = null; - try { - fd = context.getContentResolver().openAssetFileDescriptor(uri, "r"); - if (fd != null) { - return fd.getParcelFileDescriptor().getStatSize(); - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "DatabaseMessages.MmsPart: cound not find media file: " + e, e); - } finally { - if (fd != null) { - try { - fd.close(); - } catch (final IOException e) { - LogUtil.e(TAG, "DatabaseMessages.MmsPart: failed to close " + e, e); - } - } - } - return 0L; - } - - /** - * @return If the type is a text type that stores text embedded (i.e. in db table) - */ - private boolean isEmbeddedTextType() { - return ContentType.TEXT_PLAIN.equals(mContentType) - || ContentType.APP_SMIL.equals(mContentType) - || ContentType.TEXT_HTML.equals(mContentType); - } - - /** - * Get an instance of the MMS part from the part table cursor - * - * @param cursor - * @param loadMedia Whether to load the media file of the part - * @return - */ - public static MmsPart get(final Cursor cursor, final boolean loadMedia) { - final MmsPart part = new MmsPart(); - part.load(cursor, loadMedia); - return part; - } - - public boolean isText() { - return ContentType.TEXT_PLAIN.equals(mContentType) - || ContentType.TEXT_HTML.equals(mContentType) - || ContentType.APP_WAP_XHTML.equals(mContentType); - } - - public boolean isMedia() { - return ContentType.isImageType(mContentType) - || ContentType.isVideoType(mContentType) - || ContentType.isAudioType(mContentType) - || ContentType.isVCardType(mContentType); - } - - public boolean isImage() { - return ContentType.isImageType(mContentType); - } - - public Uri getDataUri() { - return Uri.parse("content://mms/part/" + mRowId); - } - - @Override - public int describeContents() { - return 0; - } - - private MmsPart(final Parcel in) { - mUri = in.readString(); - mRowId = in.readLong(); - mMessageId = in.readLong(); - mContentType = in.readString(); - mText = in.readString(); - mCharset = in.readInt(); - mWidth = in.readInt(); - mHeight = in.readInt(); - mSize = in.readLong(); - } - - public static final Parcelable.Creator<MmsPart> CREATOR - = new Parcelable.Creator<MmsPart>() { - @Override - public MmsPart createFromParcel(final Parcel in) { - return new MmsPart(in); - } - - @Override - public MmsPart[] newArray(final int size) { - return new MmsPart[size]; - } - }; - - @Override - public void writeToParcel(final Parcel out, final int flags) { - out.writeString(mUri); - out.writeLong(mRowId); - out.writeLong(mMessageId); - out.writeString(mContentType); - out.writeString(mText); - out.writeInt(mCharset); - out.writeInt(mWidth); - out.writeInt(mHeight); - out.writeLong(mSize); - } - } - - /** - * This class provides the same DatabaseMessage interface over a local SMS db message - */ - public static class LocalDatabaseMessage extends DatabaseMessage implements Parcelable { - private final int mProtocol; - private final String mUri; - private final long mTimestamp; - private final long mLocalId; - private final String mConversationId; - - public LocalDatabaseMessage(final long localId, final int protocol, final String uri, - final long timestamp, final String conversationId) { - mLocalId = localId; - mProtocol = protocol; - mUri = uri; - mTimestamp = timestamp; - mConversationId = conversationId; - } - - @Override - public int getProtocol() { - return mProtocol; - } - - @Override - public long getTimestampInMillis() { - return mTimestamp; - } - - @Override - public String getUri() { - return mUri; - } - - public long getLocalId() { - return mLocalId; - } - - public String getConversationId() { - return mConversationId; - } - - @Override - public int describeContents() { - return 0; - } - - private LocalDatabaseMessage(final Parcel in) { - mUri = in.readString(); - mConversationId = in.readString(); - mLocalId = in.readLong(); - mTimestamp = in.readLong(); - mProtocol = in.readInt(); - } - - public static final Parcelable.Creator<LocalDatabaseMessage> CREATOR - = new Parcelable.Creator<LocalDatabaseMessage>() { - @Override - public LocalDatabaseMessage createFromParcel(final Parcel in) { - return new LocalDatabaseMessage(in); - } - - @Override - public LocalDatabaseMessage[] newArray(final int size) { - return new LocalDatabaseMessage[size]; - } - }; - - @Override - public void writeToParcel(final Parcel out, final int flags) { - out.writeString(mUri); - out.writeString(mConversationId); - out.writeLong(mLocalId); - out.writeLong(mTimestamp); - out.writeInt(mProtocol); - } - } - - /** - * Address for MMS message - */ - public static class MmsAddr { - public static final String[] PROJECTION = new String[] { - Mms.Addr.ADDRESS, - Mms.Addr.CHARSET, - }; - private static int sIota = 0; - public static final int INDEX_ADDRESS = sIota++; - public static final int INDEX_CHARSET = sIota++; - - public static String get(final Cursor cursor) { - final int charset = cursor.getInt(INDEX_CHARSET); - // PduPersister stores the addresses using ISO_8859_1 - // Let's load it using that encoding and convert it back to its original - // See PduPersister.persistAddress - return getDecodedString( - getStringBytes(cursor.getString(INDEX_ADDRESS), CharacterSets.ISO_8859_1), - charset); - } - } - - /** - * Decoded string by character set - */ - public static String getDecodedString(final byte[] data, final int charset) { - if (CharacterSets.ANY_CHARSET == charset) { - return new String(data); // system default encoding. - } else { - try { - final String name = CharacterSets.getMimeName(charset); - return new String(data, name); - } catch (final UnsupportedEncodingException e) { - try { - return new String(data, CharacterSets.MIMENAME_ISO_8859_1); - } catch (final UnsupportedEncodingException exception) { - return new String(data); // system default encoding. - } - } - } - } - - /** - * Unpack a given String into a byte[]. - */ - public static byte[] getStringBytes(final String data, final int charset) { - if (CharacterSets.ANY_CHARSET == charset) { - return data.getBytes(); - } else { - try { - final String name = CharacterSets.getMimeName(charset); - return data.getBytes(name); - } catch (final UnsupportedEncodingException e) { - return data.getBytes(); - } - } - } -} diff --git a/src/com/android/messaging/sms/MmsConfig.java b/src/com/android/messaging/sms/MmsConfig.java deleted file mode 100755 index f13d785..0000000 --- a/src/com/android/messaging/sms/MmsConfig.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * 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); - } -} diff --git a/src/com/android/messaging/sms/MmsFailureException.java b/src/com/android/messaging/sms/MmsFailureException.java deleted file mode 100644 index dd702ee..0000000 --- a/src/com/android/messaging/sms/MmsFailureException.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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; - } -} diff --git a/src/com/android/messaging/sms/MmsSender.java b/src/com/android/messaging/sms/MmsSender.java deleted file mode 100644 index 6dfa81a..0000000 --- a/src/com/android/messaging/sms/MmsSender.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * 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; - } - } -} diff --git a/src/com/android/messaging/sms/MmsSmsUtils.java b/src/com/android/messaging/sms/MmsSmsUtils.java deleted file mode 100644 index 1a0ef99..0000000 --- a/src/com/android/messaging/sms/MmsSmsUtils.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * 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."); - } - } -} diff --git a/src/com/android/messaging/sms/MmsUtils.java b/src/com/android/messaging/sms/MmsUtils.java deleted file mode 100644 index 913e9a6..0000000 --- a/src/com/android/messaging/sms/MmsUtils.java +++ /dev/null @@ -1,2747 +0,0 @@ -/* - * 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.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.res.AssetFileDescriptor; -import android.content.res.Resources; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.media.MediaMetadataRetriever; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.Bundle; -import android.provider.Settings; -import android.provider.Telephony; -import android.provider.Telephony.Mms; -import android.provider.Telephony.Sms; -import android.provider.Telephony.Threads; -import android.telephony.SmsManager; -import android.telephony.SmsMessage; -import android.text.TextUtils; -import android.text.util.Rfc822Token; -import android.text.util.Rfc822Tokenizer; - -import com.android.messaging.Factory; -import com.android.messaging.R; -import com.android.messaging.datamodel.MediaScratchFileProvider; -import com.android.messaging.datamodel.action.DownloadMmsAction; -import com.android.messaging.datamodel.action.SendMessageAction; -import com.android.messaging.datamodel.data.MessageData; -import com.android.messaging.datamodel.data.MessagePartData; -import com.android.messaging.datamodel.data.ParticipantData; -import com.android.messaging.mmslib.InvalidHeaderValueException; -import com.android.messaging.mmslib.MmsException; -import com.android.messaging.mmslib.SqliteWrapper; -import com.android.messaging.mmslib.pdu.CharacterSets; -import com.android.messaging.mmslib.pdu.EncodedStringValue; -import com.android.messaging.mmslib.pdu.GenericPdu; -import com.android.messaging.mmslib.pdu.NotificationInd; -import com.android.messaging.mmslib.pdu.PduBody; -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.PduPart; -import com.android.messaging.mmslib.pdu.PduPersister; -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.sms.SmsSender.SendResult; -import com.android.messaging.util.Assert; -import com.android.messaging.util.BugleGservices; -import com.android.messaging.util.BugleGservicesKeys; -import com.android.messaging.util.BuglePrefs; -import com.android.messaging.util.ContentType; -import com.android.messaging.util.DebugUtils; -import com.android.messaging.util.EmailAddress; -import com.android.messaging.util.ImageUtils; -import com.android.messaging.util.ImageUtils.ImageResizer; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.MediaMetadataRetrieverWrapper; -import com.android.messaging.util.OsUtil; -import com.android.messaging.util.PhoneUtils; -import com.google.common.base.Joiner; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.UUID; - -/** - * Utils for sending sms/mms messages. - */ -public class MmsUtils { - private static final String TAG = LogUtil.BUGLE_TAG; - - public static final boolean DEFAULT_DELIVERY_REPORT_MODE = false; - public static final boolean DEFAULT_READ_REPORT_MODE = false; - public static final long DEFAULT_EXPIRY_TIME_IN_SECONDS = 7 * 24 * 60 * 60; - public static final int DEFAULT_PRIORITY = PduHeaders.PRIORITY_NORMAL; - - public static final int MAX_SMS_RETRY = 3; - - /** - * MMS request succeeded - */ - public static final int MMS_REQUEST_SUCCEEDED = 0; - /** - * MMS request failed with a transient error and can be retried automatically - */ - public static final int MMS_REQUEST_AUTO_RETRY = 1; - /** - * MMS request failed with an error and can be retried manually - */ - public static final int MMS_REQUEST_MANUAL_RETRY = 2; - /** - * MMS request failed with a specific error and should not be retried - */ - public static final int MMS_REQUEST_NO_RETRY = 3; - - public static final String getRequestStatusDescription(final int status) { - switch (status) { - case MMS_REQUEST_SUCCEEDED: - return "SUCCEEDED"; - case MMS_REQUEST_AUTO_RETRY: - return "AUTO_RETRY"; - case MMS_REQUEST_MANUAL_RETRY: - return "MANUAL_RETRY"; - case MMS_REQUEST_NO_RETRY: - return "NO_RETRY"; - default: - return String.valueOf(status) + " (check MmsUtils)"; - } - } - - public static final int PDU_HEADER_VALUE_UNDEFINED = 0; - - private static final int DEFAULT_DURATION = 5000; //ms - - // amount of space to leave in a MMS for text and overhead. - private static final int MMS_MAX_SIZE_SLOP = 1024; - public static final long INVALID_TIMESTAMP = 0L; - private static String[] sNoSubjectStrings; - - public static class MmsInfo { - public Uri mUri; - public int mMessageSize; - public PduBody mPduBody; - } - - // Sync all remote messages apart from drafts - private static final String REMOTE_SMS_SELECTION = String.format( - Locale.US, - "(%s IN (%d, %d, %d, %d, %d))", - Sms.TYPE, - Sms.MESSAGE_TYPE_INBOX, - Sms.MESSAGE_TYPE_OUTBOX, - Sms.MESSAGE_TYPE_QUEUED, - Sms.MESSAGE_TYPE_FAILED, - Sms.MESSAGE_TYPE_SENT); - - private static final String REMOTE_MMS_SELECTION = String.format( - Locale.US, - "((%s IN (%d, %d, %d, %d)) AND (%s IN (%d, %d, %d)))", - Mms.MESSAGE_BOX, - Mms.MESSAGE_BOX_INBOX, - Mms.MESSAGE_BOX_OUTBOX, - Mms.MESSAGE_BOX_SENT, - Mms.MESSAGE_BOX_FAILED, - Mms.MESSAGE_TYPE, - PduHeaders.MESSAGE_TYPE_SEND_REQ, - PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND, - PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF); - - /** - * Type selection for importing sms messages. - * - * @return The SQL selection for importing sms messages - */ - public static String getSmsTypeSelectionSql() { - return REMOTE_SMS_SELECTION; - } - - /** - * Type selection for importing mms messages. - * - * @return The SQL selection for importing mms messages. This selects the message type, - * not including the selection on timestamp. - */ - public static String getMmsTypeSelectionSql() { - return REMOTE_MMS_SELECTION; - } - - // SMIL spec: http://www.w3.org/TR/SMIL3 - - private static final String sSmilImagePart = - "<par dur=\"" + DEFAULT_DURATION + "ms\">" + - "<img src=\"%s\" region=\"Image\" />" + - "</par>"; - - private static final String sSmilVideoPart = - "<par dur=\"%2$dms\">" + - "<video src=\"%1$s\" dur=\"%2$dms\" region=\"Image\" />" + - "</par>"; - - private static final String sSmilAudioPart = - "<par dur=\"%2$dms\">" + - "<audio src=\"%1$s\" dur=\"%2$dms\" />" + - "</par>"; - - private static final String sSmilTextPart = - "<par dur=\"" + DEFAULT_DURATION + "ms\">" + - "<text src=\"%s\" region=\"Text\" />" + - "</par>"; - - private static final String sSmilPart = - "<par dur=\"" + DEFAULT_DURATION + "ms\">" + - "<ref src=\"%s\" />" + - "</par>"; - - private static final String sSmilTextOnly = - "<smil>" + - "<head>" + - "<layout>" + - "<root-layout/>" + - "<region id=\"Text\" top=\"0\" left=\"0\" " - + "height=\"100%%\" width=\"100%%\"/>" + - "</layout>" + - "</head>" + - "<body>" + - "%s" + // constructed body goes here - "</body>" + - "</smil>"; - - private static final String sSmilVisualAttachmentsOnly = - "<smil>" + - "<head>" + - "<layout>" + - "<root-layout/>" + - "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" " - + "height=\"100%%\" width=\"100%%\"/>" + - "</layout>" + - "</head>" + - "<body>" + - "%s" + // constructed body goes here - "</body>" + - "</smil>"; - - private static final String sSmilVisualAttachmentsWithText = - "<smil>" + - "<head>" + - "<layout>" + - "<root-layout/>" + - "<region id=\"Image\" fit=\"meet\" top=\"0\" left=\"0\" " - + "height=\"80%%\" width=\"100%%\"/>" + - "<region id=\"Text\" top=\"80%%\" left=\"0\" height=\"20%%\" " - + "width=\"100%%\"/>" + - "</layout>" + - "</head>" + - "<body>" + - "%s" + // constructed body goes here - "</body>" + - "</smil>"; - - private static final String sSmilNonVisualAttachmentsOnly = - "<smil>" + - "<head>" + - "<layout>" + - "<root-layout/>" + - "</layout>" + - "</head>" + - "<body>" + - "%s" + // constructed body goes here - "</body>" + - "</smil>"; - - private static final String sSmilNonVisualAttachmentsWithText = sSmilTextOnly; - - public static final String MMS_DUMP_PREFIX = "mmsdump-"; - public static final String SMS_DUMP_PREFIX = "smsdump-"; - - public static final int MIN_VIDEO_BYTES_PER_SECOND = 4 * 1024; - public static final int MIN_IMAGE_BYTE_SIZE = 16 * 1024; - public static final int MAX_VIDEO_ATTACHMENT_COUNT = 1; - - public static MmsInfo makePduBody(final Context context, final MessageData message, - final int subId) { - final PduBody pb = new PduBody(); - - // Compute data size requirements for this message: count up images and total size of - // non-image attachments. - int totalLength = 0; - int countImage = 0; - for (final MessagePartData part : message.getParts()) { - if (part.isAttachment()) { - final String contentType = part.getContentType(); - if (ContentType.isImageType(contentType)) { - countImage++; - } else if (ContentType.isVCardType(contentType)) { - totalLength += getDataLength(context, part.getContentUri()); - } else { - totalLength += getMediaFileSize(part.getContentUri()); - } - } - } - final long minSize = countImage * MIN_IMAGE_BYTE_SIZE; - final int byteBudget = MmsConfig.get(subId).getMaxMessageSize() - totalLength - - MMS_MAX_SIZE_SLOP; - final double budgetFactor = - minSize > 0 ? Math.max(1.0, byteBudget / ((double) minSize)) : 1; - final int bytesPerImage = (int) (budgetFactor * MIN_IMAGE_BYTE_SIZE); - final int widthLimit = MmsConfig.get(subId).getMaxImageWidth(); - final int heightLimit = MmsConfig.get(subId).getMaxImageHeight(); - - // Actually add the attachments, shrinking images appropriately. - int index = 0; - totalLength = 0; - boolean hasVisualAttachment = false; - boolean hasNonVisualAttachment = false; - boolean hasText = false; - final StringBuilder smilBody = new StringBuilder(); - for (final MessagePartData part : message.getParts()) { - String srcName; - if (part.isAttachment()) { - String contentType = part.getContentType(); - if (ContentType.isImageType(contentType)) { - // There's a good chance that if we selected the image from our media picker the - // content type is image/*. Fix the content type here for gifs so that we only - // need to open the input stream once. All other gif vs static image checks will - // only have to do a string comparison which is much cheaper. - final boolean isGif = ImageUtils.isGif(contentType, part.getContentUri()); - contentType = isGif ? ContentType.IMAGE_GIF : contentType; - srcName = String.format(isGif ? "image%06d.gif" : "image%06d.jpg", index); - smilBody.append(String.format(sSmilImagePart, srcName)); - totalLength += addPicturePart(context, pb, index, part, - widthLimit, heightLimit, bytesPerImage, srcName, contentType); - hasVisualAttachment = true; - } else if (ContentType.isVideoType(contentType)) { - srcName = String.format("video%06d.mp4", index); - final int length = addVideoPart(context, pb, part, srcName); - totalLength += length; - smilBody.append(String.format(sSmilVideoPart, srcName, - getMediaDurationMs(context, part, DEFAULT_DURATION))); - hasVisualAttachment = true; - } else if (ContentType.isVCardType(contentType)) { - srcName = String.format("contact%06d.vcf", index); - totalLength += addVCardPart(context, pb, part, srcName); - smilBody.append(String.format(sSmilPart, srcName)); - hasNonVisualAttachment = true; - } else if (ContentType.isAudioType(contentType)) { - srcName = String.format("recording%06d.amr", index); - totalLength += addOtherPart(context, pb, part, srcName); - final int duration = getMediaDurationMs(context, part, -1); - Assert.isTrue(duration != -1); - smilBody.append(String.format(sSmilAudioPart, srcName, duration)); - hasNonVisualAttachment = true; - } else { - srcName = String.format("other%06d.dat", index); - totalLength += addOtherPart(context, pb, part, srcName); - smilBody.append(String.format(sSmilPart, srcName)); - } - index++; - } - if (!TextUtils.isEmpty(part.getText())) { - hasText = true; - } - } - - if (hasText) { - final String srcName = String.format("text.%06d.txt", index); - final String text = message.getMessageText(); - totalLength += addTextPart(context, pb, text, srcName); - - // Append appropriate SMIL to the body. - smilBody.append(String.format(sSmilTextPart, srcName)); - } - - final String smilTemplate = getSmilTemplate(hasVisualAttachment, - hasNonVisualAttachment, hasText); - addSmilPart(pb, smilTemplate, smilBody.toString()); - - final MmsInfo mmsInfo = new MmsInfo(); - mmsInfo.mPduBody = pb; - mmsInfo.mMessageSize = totalLength; - - return mmsInfo; - } - - private static int getMediaDurationMs(final Context context, final MessagePartData part, - final int defaultDurationMs) { - Assert.notNull(context); - Assert.notNull(part); - Assert.isTrue(ContentType.isAudioType(part.getContentType()) || - ContentType.isVideoType(part.getContentType())); - - final MediaMetadataRetrieverWrapper retriever = new MediaMetadataRetrieverWrapper(); - try { - retriever.setDataSource(part.getContentUri()); - return retriever.extractInteger( - MediaMetadataRetriever.METADATA_KEY_DURATION, defaultDurationMs); - } catch (final IOException e) { - LogUtil.i(LogUtil.BUGLE_TAG, "Error extracting duration from " + part.getContentUri(), e); - return defaultDurationMs; - } finally { - retriever.release(); - } - } - - private static void setPartContentLocationAndId(final PduPart part, final String srcName) { - // Set Content-Location. - part.setContentLocation(srcName.getBytes()); - - // Set Content-Id. - final int index = srcName.lastIndexOf("."); - final String contentId = (index == -1) ? srcName : srcName.substring(0, index); - part.setContentId(contentId.getBytes()); - } - - private static int addTextPart(final Context context, final PduBody pb, - final String text, final String srcName) { - final PduPart part = new PduPart(); - - // Set Charset if it's a text media. - part.setCharset(CharacterSets.UTF_8); - - // Set Content-Type. - part.setContentType(ContentType.TEXT_PLAIN.getBytes()); - - // Set Content-Location. - setPartContentLocationAndId(part, srcName); - - part.setData(text.getBytes()); - - pb.addPart(part); - - return part.getData().length; - } - - private static int addPicturePart(final Context context, final PduBody pb, final int index, - final MessagePartData messagePart, int widthLimit, int heightLimit, - final int maxPartSize, final String srcName, final String contentType) { - final Uri imageUri = messagePart.getContentUri(); - final int width = messagePart.getWidth(); - final int height = messagePart.getHeight(); - - // Swap the width and height limits to match the orientation of the image so we scale the - // picture as little as possible. - if ((height > width) != (heightLimit > widthLimit)) { - final int temp = widthLimit; - widthLimit = heightLimit; - heightLimit = temp; - } - - final int orientation = ImageUtils.getOrientation(context, imageUri); - int imageSize = getDataLength(context, imageUri); - if (imageSize <= 0) { - LogUtil.e(TAG, "Can't get image", new Exception()); - return 0; - } - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addPicturePart size: " + imageSize + " width: " - + width + " widthLimit: " + widthLimit - + " height: " + height - + " heightLimit: " + heightLimit); - } - - PduPart part; - // Check if we're already within the limits - in which case we don't need to resize. - // The size can be zero here, even when the media has content. See the comment in - // MediaModel.initMediaSize. Sometimes it'll compute zero and it's costly to read the - // whole stream to compute the size. When we call getResizedImageAsPart(), we'll correctly - // set the size. - if (imageSize <= maxPartSize && - width <= widthLimit && - height <= heightLimit && - (orientation == android.media.ExifInterface.ORIENTATION_UNDEFINED || - orientation == android.media.ExifInterface.ORIENTATION_NORMAL)) { - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addPicturePart - already sized"); - } - part = new PduPart(); - part.setDataUri(imageUri); - part.setContentType(contentType.getBytes()); - } else { - part = getResizedImageAsPart(widthLimit, heightLimit, maxPartSize, - width, height, orientation, imageUri, context, contentType); - if (part == null) { - final OutOfMemoryError e = new OutOfMemoryError(); - LogUtil.e(TAG, "Can't resize image: not enough memory?", e); - throw e; - } - imageSize = part.getData().length; - } - - setPartContentLocationAndId(part, srcName); - - pb.addPart(index, part); - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addPicturePart size: " + imageSize); - } - - return imageSize; - } - - private static void addPartForUri(final Context context, final PduBody pb, - final String srcName, final Uri uri, final String contentType) { - final PduPart part = new PduPart(); - part.setDataUri(uri); - part.setContentType(contentType.getBytes()); - - setPartContentLocationAndId(part, srcName); - - pb.addPart(part); - } - - private static int addVCardPart(final Context context, final PduBody pb, - final MessagePartData messagePart, final String srcName) { - final Uri vcardUri = messagePart.getContentUri(); - final String contentType = messagePart.getContentType(); - final int vcardSize = getDataLength(context, vcardUri); - if (vcardSize <= 0) { - LogUtil.e(TAG, "Can't get vcard", new Exception()); - return 0; - } - - addPartForUri(context, pb, srcName, vcardUri, contentType); - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addVCardPart size: " + vcardSize); - } - - return vcardSize; - } - - /** - * Add video part recompressing video if necessary. If recompression fails, part is not - * added. - */ - private static int addVideoPart(final Context context, final PduBody pb, - final MessagePartData messagePart, final String srcName) { - final Uri attachmentUri = messagePart.getContentUri(); - String contentType = messagePart.getContentType(); - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString()); - } - - if (TextUtils.isEmpty(contentType)) { - contentType = ContentType.VIDEO_3G2; - } - - addPartForUri(context, pb, srcName, attachmentUri, contentType); - return (int) getMediaFileSize(attachmentUri); - } - - private static int addOtherPart(final Context context, final PduBody pb, - final MessagePartData messagePart, final String srcName) { - final Uri attachmentUri = messagePart.getContentUri(); - final String contentType = messagePart.getContentType(); - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "addPart attachmentUrl: " + attachmentUri.toString()); - } - - final int dataSize = (int) getMediaFileSize(attachmentUri); - - addPartForUri(context, pb, srcName, attachmentUri, contentType); - - return dataSize; - } - - private static void addSmilPart(final PduBody pb, final String smilTemplate, - final String smilBody) { - final PduPart smilPart = new PduPart(); - smilPart.setContentId("smil".getBytes()); - smilPart.setContentLocation("smil.xml".getBytes()); - smilPart.setContentType(ContentType.APP_SMIL.getBytes()); - final String smil = String.format(smilTemplate, smilBody); - smilPart.setData(smil.getBytes()); - pb.addPart(0, smilPart); - } - - private static String getSmilTemplate(final boolean hasVisualAttachments, - final boolean hasNonVisualAttachments, final boolean hasText) { - if (hasVisualAttachments) { - return hasText ? sSmilVisualAttachmentsWithText : sSmilVisualAttachmentsOnly; - } - if (hasNonVisualAttachments) { - return hasText ? sSmilNonVisualAttachmentsWithText : sSmilNonVisualAttachmentsOnly; - } - return sSmilTextOnly; - } - - private static int getDataLength(final Context context, final Uri uri) { - InputStream is = null; - try { - is = context.getContentResolver().openInputStream(uri); - try { - return is == null ? 0 : is.available(); - } catch (final IOException e) { - LogUtil.e(TAG, "getDataLength couldn't stream: " + uri, e); - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "getDataLength couldn't open: " + uri, e); - } finally { - if (is != null) { - try { - is.close(); - } catch (final IOException e) { - LogUtil.e(TAG, "getDataLength couldn't close: " + uri, e); - } - } - } - return 0; - } - - /** - * Returns {@code true} if group mms is turned on, - * {@code false} otherwise. - * - * For the group mms feature to be enabled, the following must be true: - * 1. the feature is enabled in mms_config.xml (currently on by default) - * 2. the feature is enabled in the SMS settings page - * - * @return true if group mms is supported - */ - public static boolean groupMmsEnabled(final int subId) { - final Context context = Factory.get().getApplicationContext(); - final Resources resources = context.getResources(); - final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); - final String groupMmsKey = resources.getString(R.string.group_mms_pref_key); - final boolean groupMmsEnabledDefault = resources.getBoolean(R.bool.group_mms_pref_default); - final boolean groupMmsPrefOn = prefs.getBoolean(groupMmsKey, groupMmsEnabledDefault); - return MmsConfig.get(subId).getGroupMmsEnabled() && groupMmsPrefOn; - } - - /** - * Get a version of this image resized to fit the given dimension and byte-size limits. Note - * that the content type of the resulting PduPart may not be the same as the content type of - * this UriImage; always call {@link PduPart#getContentType()} to get the new content type. - * - * @param widthLimit The width limit, in pixels - * @param heightLimit The height limit, in pixels - * @param byteLimit The binary size limit, in bytes - * @param width The image width, in pixels - * @param height The image height, in pixels - * @param orientation Orientation constant from ExifInterface for rotating or flipping the - * image - * @param imageUri Uri to the image data - * @param context Needed to open the image - * @return A new PduPart containing the resized image data - */ - private static PduPart getResizedImageAsPart(final int widthLimit, - final int heightLimit, final int byteLimit, final int width, final int height, - final int orientation, final Uri imageUri, final Context context, final String contentType) { - final PduPart part = new PduPart(); - - final byte[] data = ImageResizer.getResizedImageData(width, height, orientation, - widthLimit, heightLimit, byteLimit, imageUri, context, contentType); - if (data == null) { - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.v(TAG, "Resize image failed."); - } - return null; - } - - part.setData(data); - // Any static images will be compressed into a jpeg - final String contentTypeOfResizedImage = ImageUtils.isGif(contentType, imageUri) - ? ContentType.IMAGE_GIF : ContentType.IMAGE_JPEG; - part.setContentType(contentTypeOfResizedImage.getBytes()); - - return part; - } - - /** - * Get media file size - */ - public static long getMediaFileSize(final Uri uri) { - final Context context = Factory.get().getApplicationContext(); - AssetFileDescriptor fd = null; - try { - fd = context.getContentResolver().openAssetFileDescriptor(uri, "r"); - if (fd != null) { - return fd.getParcelFileDescriptor().getStatSize(); - } - } catch (final FileNotFoundException e) { - LogUtil.e(TAG, "MmsUtils.getMediaFileSize: cound not find media file: " + e, e); - } finally { - if (fd != null) { - try { - fd.close(); - } catch (final IOException e) { - LogUtil.e(TAG, "MmsUtils.getMediaFileSize: failed to close " + e, e); - } - } - } - return 0L; - } - - // Code for extracting the actual phone numbers for the participants in a conversation, - // given a thread id. - - private static final Uri ALL_THREADS_URI = - Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(); - - private static final String[] RECIPIENTS_PROJECTION = { - Threads._ID, - Threads.RECIPIENT_IDS - }; - - private static final int RECIPIENT_IDS = 1; - - public static List<String> getRecipientsByThread(final long threadId) { - final String spaceSepIds = getRawRecipientIdsForThread(threadId); - if (!TextUtils.isEmpty(spaceSepIds)) { - final Context context = Factory.get().getApplicationContext(); - return getAddresses(context, spaceSepIds); - } - return null; - } - - // NOTE: There are phones on which you can't get the recipients from the thread id for SMS - // until you have a message in the conversation! - public static String getRawRecipientIdsForThread(final long threadId) { - if (threadId <= 0) { - return null; - } - final Context context = Factory.get().getApplicationContext(); - final ContentResolver cr = context.getContentResolver(); - final Cursor thread = cr.query( - ALL_THREADS_URI, - RECIPIENTS_PROJECTION, "_id=?", new String[] { String.valueOf(threadId) }, null); - if (thread != null) { - try { - if (thread.moveToFirst()) { - // recipientIds will be a space-separated list of ids into the - // canonical addresses table. - return thread.getString(RECIPIENT_IDS); - } - } finally { - thread.close(); - } - } - return null; - } - - private static final Uri SINGLE_CANONICAL_ADDRESS_URI = - Uri.parse("content://mms-sms/canonical-address"); - - private static List<String> getAddresses(final Context context, final String spaceSepIds) { - final List<String> numbers = new ArrayList<String>(); - final String[] ids = spaceSepIds.split(" "); - for (final String id : ids) { - long longId; - - try { - longId = Long.parseLong(id); - if (longId < 0) { - LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id " + longId); - continue; - } - } catch (final NumberFormatException ex) { - LogUtil.e(TAG, "MmsUtils.getAddresses: invalid id. " + ex, ex); - // skip this id - continue; - } - - // TODO: build a single query where we get all the addresses at once. - Cursor c = null; - try { - c = context.getContentResolver().query( - ContentUris.withAppendedId(SINGLE_CANONICAL_ADDRESS_URI, longId), - null, null, null, null); - } catch (final Exception e) { - LogUtil.e(TAG, "MmsUtils.getAddresses: query failed for id " + longId, e); - } - if (c != null) { - try { - if (c.moveToFirst()) { - final String number = c.getString(0); - if (!TextUtils.isEmpty(number)) { - numbers.add(number); - } else { - LogUtil.w(TAG, "Canonical MMS/SMS address is empty for id: " + longId); - } - } - } finally { - c.close(); - } - } - } - if (numbers.isEmpty()) { - LogUtil.w(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]"); - } - return numbers; - } - - // Get telephony SMS thread ID - public static long getOrCreateSmsThreadId(final Context context, final String dest) { - // use destinations to determine threadId - final Set<String> recipients = new HashSet<String>(); - recipients.add(dest); - try { - return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e); - return -1; - } - } - - // Get telephony SMS thread ID - public static long getOrCreateThreadId(final Context context, final List<String> dests) { - if (dests == null || dests.size() == 0) { - return -1; - } - // use destinations to determine threadId - final Set<String> recipients = new HashSet<String>(dests); - try { - return MmsSmsUtils.Threads.getOrCreateThreadId(context, recipients); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: getting thread id failed: " + e); - return -1; - } - } - - /** - * Add an SMS to the given URI with thread_id specified. - * - * @param resolver the content resolver to use - * @param uri the URI to add the message to - * @param subId subId for the receiving sim - * @param address the address of the sender - * @param body the body of the message - * @param subject the psuedo-subject of the message - * @param date the timestamp for the message - * @param read true if the message has been read, false if not - * @param threadId the thread_id of the message - * @return the URI for the new message - */ - private static Uri addMessageToUri(final ContentResolver resolver, - final Uri uri, final int subId, final String address, final String body, - final String subject, final Long date, final boolean read, final boolean seen, - final int status, final int type, final long threadId) { - final ContentValues values = new ContentValues(7); - - values.put(Telephony.Sms.ADDRESS, address); - if (date != null) { - values.put(Telephony.Sms.DATE, date); - } - values.put(Telephony.Sms.READ, read ? 1 : 0); - values.put(Telephony.Sms.SEEN, seen ? 1 : 0); - values.put(Telephony.Sms.SUBJECT, subject); - values.put(Telephony.Sms.BODY, body); - if (OsUtil.isAtLeastL_MR1()) { - values.put(Telephony.Sms.SUBSCRIPTION_ID, subId); - } - if (status != Telephony.Sms.STATUS_NONE) { - values.put(Telephony.Sms.STATUS, status); - } - if (type != Telephony.Sms.MESSAGE_TYPE_ALL) { - values.put(Telephony.Sms.TYPE, type); - } - if (threadId != -1L) { - values.put(Telephony.Sms.THREAD_ID, threadId); - } - return resolver.insert(uri, values); - } - - // Insert an SMS message to telephony - public static Uri insertSmsMessage(final Context context, final Uri uri, final int subId, - final String dest, final String text, final long timestamp, final int status, - final int type, final long threadId) { - Uri response = null; - try { - response = addMessageToUri(context.getContentResolver(), uri, subId, dest, - text, null /* subject */, timestamp, true /* read */, - true /* seen */, status, type, threadId); - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "Mmsutils: Inserted SMS message into telephony (type = " + type + ")" - + ", uri: " + response); - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: persist sms message failure " + e, e); - } - return response; - } - - // Update SMS message type in telephony; returns true if it succeeded. - public static boolean updateSmsMessageSendingStatus(final Context context, final Uri uri, - final int type, final long date) { - try { - final ContentResolver resolver = context.getContentResolver(); - final ContentValues values = new ContentValues(2); - - values.put(Telephony.Sms.TYPE, type); - values.put(Telephony.Sms.DATE, date); - final int cnt = resolver.update(uri, values, null, null); - if (cnt == 1) { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "Mmsutils: Updated sending SMS " + uri + "; type = " + type - + ", date = " + date + " (millis since epoch)"); - } - return true; - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: update sms message failure " + e, e); - } - return false; - } - - // Persist a sent MMS message in telephony - private static Uri insertSendReq(final Context context, final GenericPdu pdu, final int subId, - final String subPhoneNumber) { - final PduPersister persister = PduPersister.getPduPersister(context); - Uri uri = null; - try { - // Persist the PDU - uri = persister.persist( - pdu, - Mms.Sent.CONTENT_URI, - subId, - subPhoneNumber, - null/*preOpenedFiles*/); - // Update mms table to reflect sent messages are always seen and read - final ContentValues values = new ContentValues(1); - values.put(Mms.READ, 1); - values.put(Mms.SEEN, 1); - SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); - } catch (final MmsException e) { - LogUtil.e(TAG, "MmsUtils: persist mms sent message failure " + e, e); - } - return uri; - } - - // Persist a received MMS message in telephony - public static Uri insertReceivedMmsMessage(final Context context, - final RetrieveConf retrieveConf, final int subId, final String subPhoneNumber, - final long receivedTimestampInSeconds, final String contentLocation) { - final PduPersister persister = PduPersister.getPduPersister(context); - Uri uri = null; - try { - uri = persister.persist( - retrieveConf, - Mms.Inbox.CONTENT_URI, - subId, - subPhoneNumber, - null/*preOpenedFiles*/); - - final ContentValues values = new ContentValues(2); - // Update mms table with local time instead of PDU time - values.put(Mms.DATE, receivedTimestampInSeconds); - // Also update the content location field from NotificationInd so that - // wap push dedup would work even after the wap push is deleted - values.put(Mms.CONTENT_LOCATION, contentLocation); - SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "MmsUtils: Inserted MMS message into telephony, uri: " + uri); - } - } catch (final MmsException e) { - LogUtil.e(TAG, "MmsUtils: persist mms received message failure " + e, e); - // Just returns empty uri to RetrieveMmsRequest, which triggers a permanent failure - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsUtils: update mms received message failure " + e, e); - // Time update failure is ignored. - } - return uri; - } - - // Update MMS message type in telephony; returns true if it succeeded. - public static boolean updateMmsMessageSendingStatus(final Context context, final Uri uri, - final int box, final long timestampInMillis) { - try { - final ContentResolver resolver = context.getContentResolver(); - final ContentValues values = new ContentValues(); - - final long timestampInSeconds = timestampInMillis / 1000L; - values.put(Telephony.Mms.MESSAGE_BOX, box); - values.put(Telephony.Mms.DATE, timestampInSeconds); - final int cnt = resolver.update(uri, values, null, null); - if (cnt == 1) { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "Mmsutils: Updated sending MMS " + uri + "; box = " + box - + ", date = " + timestampInSeconds + " (secs since epoch)"); - } - return true; - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: update mms message failure " + e, e); - } - return false; - } - - /** - * Parse values from a received sms message - * - * @param context - * @param msgs The received sms message content - * @param error The received sms error - * @return Parsed values from the message - */ - public static ContentValues parseReceivedSmsMessage( - final Context context, final SmsMessage[] msgs, final int error) { - final SmsMessage sms = msgs[0]; - final ContentValues values = new ContentValues(); - - values.put(Sms.ADDRESS, sms.getDisplayOriginatingAddress()); - values.put(Sms.BODY, buildMessageBodyFromPdus(msgs)); - if (MmsUtils.hasSmsDateSentColumn()) { - // TODO:: The boxing here seems unnecessary. - values.put(Sms.DATE_SENT, Long.valueOf(sms.getTimestampMillis())); - } - values.put(Sms.PROTOCOL, sms.getProtocolIdentifier()); - if (sms.getPseudoSubject().length() > 0) { - values.put(Sms.SUBJECT, sms.getPseudoSubject()); - } - values.put(Sms.REPLY_PATH_PRESENT, sms.isReplyPathPresent() ? 1 : 0); - values.put(Sms.SERVICE_CENTER, sms.getServiceCenterAddress()); - // Error code - values.put(Sms.ERROR_CODE, error); - - return values; - } - - // Some providers send formfeeds in their messages. Convert those formfeeds to newlines. - private static String replaceFormFeeds(final String s) { - return s == null ? "" : s.replace('\f', '\n'); - } - - // Parse the message body from message PDUs - private static String buildMessageBodyFromPdus(final SmsMessage[] msgs) { - if (msgs.length == 1) { - // There is only one part, so grab the body directly. - return replaceFormFeeds(msgs[0].getDisplayMessageBody()); - } else { - // Build up the body from the parts. - final StringBuilder body = new StringBuilder(); - for (final SmsMessage msg : msgs) { - try { - // getDisplayMessageBody() can NPE if mWrappedMessage inside is null. - body.append(msg.getDisplayMessageBody()); - } catch (final NullPointerException e) { - // Nothing to do - } - } - return replaceFormFeeds(body.toString()); - } - } - - // Parse the message date - public static Long getMessageDate(final SmsMessage sms, long now) { - // Use now for the timestamp to avoid confusion with clock - // drift between the handset and the SMSC. - // Check to make sure the system is giving us a non-bogus time. - final Calendar buildDate = new GregorianCalendar(2011, 8, 18); // 18 Sep 2011 - final Calendar nowDate = new GregorianCalendar(); - nowDate.setTimeInMillis(now); - if (nowDate.before(buildDate)) { - // It looks like our system clock isn't set yet because the current time right now - // is before an arbitrary time we made this build. Instead of inserting a bogus - // receive time in this case, use the timestamp of when the message was sent. - now = sms.getTimestampMillis(); - } - return now; - } - - /** - * cleanseMmsSubject will take a subject that's says, "<Subject: no subject>", and return - * a null string. Otherwise it will return the original subject string. - * @param resources So the function can grab string resources - * @param subject the raw subject - * @return - */ - public static String cleanseMmsSubject(final Resources resources, final String subject) { - if (TextUtils.isEmpty(subject)) { - return null; - } - if (sNoSubjectStrings == null) { - sNoSubjectStrings = - resources.getStringArray(R.array.empty_subject_strings); - } - for (final String noSubjectString : sNoSubjectStrings) { - if (subject.equalsIgnoreCase(noSubjectString)) { - return null; - } - } - return subject; - } - - // return a semicolon separated list of phone numbers from a smsto: uri. - public static String getSmsRecipients(final Uri uri) { - String recipients = uri.getSchemeSpecificPart(); - final int pos = recipients.indexOf('?'); - if (pos != -1) { - recipients = recipients.substring(0, pos); - } - recipients = replaceUnicodeDigits(recipients).replace(',', ';'); - return recipients; - } - - // This function was lifted from Telephony.PhoneNumberUtils because it was @hide - /** - * Replace arabic/unicode digits with decimal digits. - * @param number - * the number to be normalized. - * @return the replaced number. - */ - private static String replaceUnicodeDigits(final String number) { - final StringBuilder normalizedDigits = new StringBuilder(number.length()); - for (final char c : number.toCharArray()) { - final int digit = Character.digit(c, 10); - if (digit != -1) { - normalizedDigits.append(digit); - } else { - normalizedDigits.append(c); - } - } - return normalizedDigits.toString(); - } - - /** - * @return Whether the data roaming is enabled - */ - private static boolean isDataRoamingEnabled() { - boolean dataRoamingEnabled = false; - final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver(); - if (OsUtil.isAtLeastJB_MR1()) { - dataRoamingEnabled = (Settings.Global.getInt(cr, Settings.Global.DATA_ROAMING, 0) != 0); - } else { - dataRoamingEnabled = (Settings.System.getInt(cr, Settings.System.DATA_ROAMING, 0) != 0); - } - return dataRoamingEnabled; - } - - /** - * @return Whether to auto retrieve MMS - */ - public static boolean allowMmsAutoRetrieve(final int subId) { - final Context context = Factory.get().getApplicationContext(); - final Resources resources = context.getResources(); - final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); - final boolean autoRetrieve = prefs.getBoolean( - resources.getString(R.string.auto_retrieve_mms_pref_key), - resources.getBoolean(R.bool.auto_retrieve_mms_pref_default)); - if (autoRetrieve) { - final boolean autoRetrieveInRoaming = prefs.getBoolean( - resources.getString(R.string.auto_retrieve_mms_when_roaming_pref_key), - resources.getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default)); - final PhoneUtils phoneUtils = PhoneUtils.get(subId); - if ((autoRetrieveInRoaming && phoneUtils.isDataRoamingEnabled()) - || !phoneUtils.isRoaming()) { - return true; - } - } - return false; - } - - /** - * Parse the message row id from a message Uri. - * - * @param messageUri The input Uri - * @return The message row id if valid, otherwise -1 - */ - public static long parseRowIdFromMessageUri(final Uri messageUri) { - try { - if (messageUri != null) { - return ContentUris.parseId(messageUri); - } - } catch (final UnsupportedOperationException e) { - // Nothing to do - } catch (final NumberFormatException e) { - // Nothing to do - } - return -1; - } - - public static SmsMessage getSmsMessageFromDeliveryReport(final Intent intent) { - final byte[] pdu = intent.getByteArrayExtra("pdu"); - return SmsMessage.createFromPdu(pdu); - } - - /** - * Update the status and date_sent column of sms message in telephony provider - * - * @param smsMessageUri - * @param status - * @param timeSentInMillis - */ - public static void updateSmsStatusAndDateSent(final Uri smsMessageUri, final int status, - final long timeSentInMillis) { - if (smsMessageUri == null) { - return; - } - final ContentValues values = new ContentValues(); - values.put(Sms.STATUS, status); - if (MmsUtils.hasSmsDateSentColumn()) { - values.put(Sms.DATE_SENT, timeSentInMillis); - } - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - resolver.update(smsMessageUri, values, null/*where*/, null/*selectionArgs*/); - } - - /** - * Get the SQL selection statement for matching messages with media. - * - * Example for MMS part table: - * "((ct LIKE 'image/%') - * OR (ct LIKE 'video/%') - * OR (ct LIKE 'audio/%') - * OR (ct='application/ogg')) - * - * @param contentTypeColumn The content-type column name - * @return The SQL selection statement for matching media types: image, video, audio - */ - public static String getMediaTypeSelectionSql(final String contentTypeColumn) { - return String.format( - Locale.US, - "((%s LIKE '%s') OR (%s LIKE '%s') OR (%s LIKE '%s') OR (%s='%s'))", - contentTypeColumn, - "image/%", - contentTypeColumn, - "video/%", - contentTypeColumn, - "audio/%", - contentTypeColumn, - ContentType.AUDIO_OGG); - } - - // Max number of operands per SQL query for deleting SMS messages - public static final int MAX_IDS_PER_QUERY = 128; - - /** - * Delete MMS messages with media parts. - * - * Because the telephony provider constraints, we can't use JOIN and delete messages in one - * shot. We have to do a query first and then batch delete the messages based on IDs. - * - * @return The count of messages deleted. - */ - public static int deleteMediaMessages() { - // Do a query first - // - // The WHERE clause has two parts: - // The first part is to select the exact same types of MMS messages as when we import them - // (so that we don't delete messages that are not in local database) - // The second part is to select MMS with media parts, including image, video and audio - final String selection = String.format( - Locale.US, - "%s AND (%s IN (SELECT %s FROM part WHERE %s))", - getMmsTypeSelectionSql(), - Mms._ID, - Mms.Part.MSG_ID, - getMediaTypeSelectionSql(Mms.Part.CONTENT_TYPE)); - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - final Cursor cursor = resolver.query(Mms.CONTENT_URI, - new String[]{ Mms._ID }, - selection, - null/*selectionArgs*/, - null/*sortOrder*/); - int deleted = 0; - if (cursor != null) { - final long[] messageIds = new long[cursor.getCount()]; - try { - int i = 0; - while (cursor.moveToNext()) { - messageIds[i++] = cursor.getLong(0); - } - } finally { - cursor.close(); - } - final int totalIds = messageIds.length; - if (totalIds > 0) { - // Batch delete the messages using IDs - // We don't want to send all IDs at once since there is a limit on SQL statement - for (int start = 0; start < totalIds; start += MAX_IDS_PER_QUERY) { - final int end = Math.min(start + MAX_IDS_PER_QUERY, totalIds); // excluding - final int count = end - start; - final String batchSelection = String.format( - Locale.US, - "%s IN %s", - Mms._ID, - getSqlInOperand(count)); - final String[] batchSelectionArgs = - getSqlInOperandArgs(messageIds, start, count); - final int deletedForBatch = resolver.delete( - Mms.CONTENT_URI, - batchSelection, - batchSelectionArgs); - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "deleteMediaMessages: deleting IDs = " - + Joiner.on(',').skipNulls().join(batchSelectionArgs) - + ", deleted = " + deletedForBatch); - } - deleted += deletedForBatch; - } - } - } - return deleted; - } - - /** - * Get the (?,?,...) thing for the SQL IN operator by a count - * - * @param count - * @return - */ - public static String getSqlInOperand(final int count) { - if (count <= 0) { - return null; - } - final StringBuilder sb = new StringBuilder(); - sb.append("(?"); - for (int i = 0; i < count - 1; i++) { - sb.append(",?"); - } - sb.append(")"); - return sb.toString(); - } - - /** - * Get the args for SQL IN operator from a long ID array - * - * @param ids The original long id array - * @param start Start of the ids to fill the args - * @param count Number of ids to pack - * @return The long array with the id args - */ - private static String[] getSqlInOperandArgs( - final long[] ids, final int start, final int count) { - if (count <= 0) { - return null; - } - final String[] args = new String[count]; - for (int i = 0; i < count; i++) { - args[i] = Long.toString(ids[start + i]); - } - return args; - } - - /** - * Delete SMS and MMS messages that are earlier than a specific timestamp - * - * @param cutOffTimestampInMillis The cut-off timestamp - * @return Total number of messages deleted. - */ - public static int deleteMessagesOlderThan(final long cutOffTimestampInMillis) { - int deleted = 0; - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - // Delete old SMS - final String smsSelection = String.format( - Locale.US, - "%s AND (%s<=%d)", - getSmsTypeSelectionSql(), - Sms.DATE, - cutOffTimestampInMillis); - deleted += resolver.delete(Sms.CONTENT_URI, smsSelection, null/*selectionArgs*/); - // Delete old MMS - final String mmsSelection = String.format( - Locale.US, - "%s AND (%s<=%d)", - getMmsTypeSelectionSql(), - Mms.DATE, - cutOffTimestampInMillis / 1000L); - deleted += resolver.delete(Mms.CONTENT_URI, mmsSelection, null/*selectionArgs*/); - return deleted; - } - - /** - * Update the read status of SMS/MMS messages by thread and timestamp - * - * @param threadId The thread of sms/mms to change - * @param timestampInMillis Change the status before this timestamp - */ - public static void updateSmsReadStatus(final long threadId, final long timestampInMillis) { - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - final ContentValues values = new ContentValues(); - values.put("read", 1); - values.put("seen", 1); /* If you read it you saw it */ - final String smsSelection = String.format( - Locale.US, - "%s=%d AND %s<=%d AND %s=0", - Sms.THREAD_ID, - threadId, - Sms.DATE, - timestampInMillis, - Sms.READ); - resolver.update( - Sms.CONTENT_URI, - values, - smsSelection, - null/*selectionArgs*/); - final String mmsSelection = String.format( - Locale.US, - "%s=%d AND %s<=%d AND %s=0", - Mms.THREAD_ID, - threadId, - Mms.DATE, - timestampInMillis / 1000L, - Mms.READ); - resolver.update( - Mms.CONTENT_URI, - values, - mmsSelection, - null/*selectionArgs*/); - } - - /** - * Update the read status of a single MMS message by its URI - * - * @param mmsUri - * @param read - */ - public static void updateReadStatusForMmsMessage(final Uri mmsUri, final boolean read) { - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - final ContentValues values = new ContentValues(); - values.put(Mms.READ, read ? 1 : 0); - resolver.update(mmsUri, values, null/*where*/, null/*selectionArgs*/); - } - - public static class AttachmentInfo { - public String mUrl; - public String mContentType; - public int mWidth; - public int mHeight; - } - - /** - * Convert byte array to Java String using a charset name - * - * @param bytes - * @param charsetName - * @return - */ - public static String bytesToString(final byte[] bytes, final String charsetName) { - if (bytes == null) { - return null; - } - try { - return new String(bytes, charsetName); - } catch (final UnsupportedEncodingException e) { - LogUtil.e(TAG, "MmsUtils.bytesToString: " + e, e); - return new String(bytes); - } - } - - /** - * Convert a Java String to byte array using a charset name - * - * @param string - * @param charsetName - * @return - */ - public static byte[] stringToBytes(final String string, final String charsetName) { - if (string == null) { - return null; - } - try { - return string.getBytes(charsetName); - } catch (final UnsupportedEncodingException e) { - LogUtil.e(TAG, "MmsUtils.stringToBytes: " + e, e); - return string.getBytes(); - } - } - - private static final String[] TEST_DATE_SENT_PROJECTION = new String[] { Sms.DATE_SENT }; - private static Boolean sHasSmsDateSentColumn = null; - /** - * Check if date_sent column exists on ICS and above devices. We need to do a test - * query to figure that out since on some ICS+ devices, somehow the date_sent column does - * not exist. http://b/17629135 tracks the associated compliance test. - * - * @return Whether "date_sent" column exists in sms table - */ - public static boolean hasSmsDateSentColumn() { - if (sHasSmsDateSentColumn == null) { - Cursor cursor = null; - try { - final Context context = Factory.get().getApplicationContext(); - final ContentResolver resolver = context.getContentResolver(); - cursor = SqliteWrapper.query( - context, - resolver, - Sms.CONTENT_URI, - TEST_DATE_SENT_PROJECTION, - null/*selection*/, - null/*selectionArgs*/, - Sms.DATE_SENT + " ASC LIMIT 1"); - sHasSmsDateSentColumn = true; - } catch (final SQLiteException e) { - LogUtil.w(TAG, "date_sent in sms table does not exist", e); - sHasSmsDateSentColumn = false; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - return sHasSmsDateSentColumn; - } - - private static final String[] TEST_CARRIERS_PROJECTION = - new String[] { Telephony.Carriers.MMSC }; - private static Boolean sUseSystemApn = null; - /** - * Check if we can access the APN data in the Telephony provider. Access was restricted in - * JB MR1 (and some JB MR2) devices. If we can't access the APN, we have to fall back and use - * a private table in our own app. - * - * @return Whether we can access the system APN table - */ - public static boolean useSystemApnTable() { - if (sUseSystemApn == null) { - Cursor cursor = null; - try { - final Context context = Factory.get().getApplicationContext(); - final ContentResolver resolver = context.getContentResolver(); - cursor = SqliteWrapper.query( - context, - resolver, - Telephony.Carriers.CONTENT_URI, - TEST_CARRIERS_PROJECTION, - null/*selection*/, - null/*selectionArgs*/, - null); - sUseSystemApn = true; - } catch (final SecurityException e) { - LogUtil.w(TAG, "Can't access system APN, using internal table", e); - sUseSystemApn = false; - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - return sUseSystemApn; - } - - // For the internal debugger only - public static void setUseSystemApnTable(final boolean turnOn) { - if (!turnOn) { - // We're not turning on to the system table. Instead, we're using our internal table. - final int osVersion = OsUtil.getApiVersion(); - if (osVersion != android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { - // We're turning on local APNs on a device where we wouldn't normally have the - // local APN table. Build it here. - - final SQLiteDatabase database = ApnDatabase.getApnDatabase().getWritableDatabase(); - - // Do we already have the table? - Cursor cursor = null; - try { - cursor = database.query(ApnDatabase.APN_TABLE, - ApnDatabase.APN_PROJECTION, - null, null, null, null, null, null); - } catch (final Exception e) { - // Apparently there's no table, create it now. - ApnDatabase.forceBuildAndLoadApnTables(); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - } - sUseSystemApn = turnOn; - } - - /** - * Checks if we should dump sms, based on both the setting and the global debug - * flag - * - * @return if dump sms is enabled - */ - public static boolean isDumpSmsEnabled() { - if (!DebugUtils.isDebugEnabled()) { - return false; - } - return getDumpSmsOrMmsPref(R.string.dump_sms_pref_key, R.bool.dump_sms_pref_default); - } - - /** - * Checks if we should dump mms, based on both the setting and the global debug - * flag - * - * @return if dump mms is enabled - */ - public static boolean isDumpMmsEnabled() { - if (!DebugUtils.isDebugEnabled()) { - return false; - } - return getDumpSmsOrMmsPref(R.string.dump_mms_pref_key, R.bool.dump_mms_pref_default); - } - - /** - * Load the value of dump sms or mms setting preference - */ - private static boolean getDumpSmsOrMmsPref(final int prefKeyRes, final int defaultKeyRes) { - final Context context = Factory.get().getApplicationContext(); - final Resources resources = context.getResources(); - final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); - final String key = resources.getString(prefKeyRes); - final boolean defaultValue = resources.getBoolean(defaultKeyRes); - return prefs.getBoolean(key, defaultValue); - } - - public static final Uri MMS_PART_CONTENT_URI = Uri.parse("content://mms/part"); - - /** - * Load MMS from telephony - * - * @param mmsUri The MMS pdu Uri - * @return A memory copy of the MMS pdu including parts (but not addresses) - */ - public static DatabaseMessages.MmsMessage loadMms(final Uri mmsUri) { - final Context context = Factory.get().getApplicationContext(); - final ContentResolver resolver = context.getContentResolver(); - DatabaseMessages.MmsMessage mms = null; - Cursor cursor = null; - // Load pdu first - try { - cursor = SqliteWrapper.query(context, resolver, - mmsUri, - DatabaseMessages.MmsMessage.getProjection(), - null/*selection*/, null/*selectionArgs*/, null/*sortOrder*/); - if (cursor != null && cursor.moveToFirst()) { - mms = DatabaseMessages.MmsMessage.get(cursor); - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsLoader: query pdu failure: " + e, e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - if (mms == null) { - return null; - } - // Load parts except SMIL - // TODO: we may need to load SMIL part in the future. - final long rowId = MmsUtils.parseRowIdFromMessageUri(mmsUri); - final String selection = String.format( - Locale.US, - "%s != '%s' AND %s = ?", - Mms.Part.CONTENT_TYPE, - ContentType.APP_SMIL, - Mms.Part.MSG_ID); - cursor = null; - try { - cursor = SqliteWrapper.query(context, resolver, - MMS_PART_CONTENT_URI, - DatabaseMessages.MmsPart.PROJECTION, - selection, - new String[] { Long.toString(rowId) }, - null/*sortOrder*/); - if (cursor != null) { - while (cursor.moveToNext()) { - mms.addPart(DatabaseMessages.MmsPart.get(cursor, true/*loadMedia*/)); - } - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "MmsLoader: query parts failure: " + e, e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - return mms; - } - - /** - * Get the sender of an MMS message - * - * @param recipients The recipient list of the message - * @param mmsUri The pdu uri of the MMS - * @return The sender phone number of the MMS - */ - public static String getMmsSender(final List<String> recipients, final String mmsUri) { - final Context context = Factory.get().getApplicationContext(); - // We try to avoid the database query. - // If this is a 1v1 conv., then the other party is the sender - if (recipients != null && recipients.size() == 1) { - return recipients.get(0); - } - // Otherwise, we have to query the MMS addr table for sender address - // This should only be done for a received group mms message - final Cursor cursor = SqliteWrapper.query( - context, - context.getContentResolver(), - Uri.withAppendedPath(Uri.parse(mmsUri), "addr"), - new String[] { Mms.Addr.ADDRESS, Mms.Addr.CHARSET }, - Mms.Addr.TYPE + "=" + PduHeaders.FROM, - null/*selectionArgs*/, - null/*sortOrder*/); - if (cursor != null) { - try { - if (cursor.moveToFirst()) { - return DatabaseMessages.MmsAddr.get(cursor); - } - } finally { - cursor.close(); - } - } - return null; - } - - public static int bugleStatusForMms(final boolean isOutgoing, final boolean isNotification, - final int messageBox) { - int bugleStatus = MessageData.BUGLE_STATUS_UNKNOWN; - // For a message we sync either - if (isOutgoing) { - if (messageBox == Mms.MESSAGE_BOX_OUTBOX || messageBox == Mms.MESSAGE_BOX_FAILED) { - // Not sent counts as failed and available for manual resend - bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_FAILED; - } else { - // Otherwise outgoing message is complete - bugleStatus = MessageData.BUGLE_STATUS_OUTGOING_COMPLETE; - } - } else if (isNotification) { - // Incoming MMS notifications we sync count as failed and available for manual download - bugleStatus = MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD; - } else { - // Other incoming MMS messages are complete - bugleStatus = MessageData.BUGLE_STATUS_INCOMING_COMPLETE; - } - return bugleStatus; - } - - public static MessageData createMmsMessage(final DatabaseMessages.MmsMessage mms, - final String conversationId, final String participantId, final String selfId, - final int bugleStatus) { - Assert.notNull(mms); - final boolean isNotification = (mms.mMmsMessageType == - PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND); - final int rawMmsStatus = (bugleStatus < MessageData.BUGLE_STATUS_FIRST_INCOMING - ? mms.mRetrieveStatus : mms.mResponseStatus); - - final MessageData message = MessageData.createMmsMessage(mms.getUri(), - participantId, selfId, conversationId, isNotification, bugleStatus, - mms.mContentLocation, mms.mTransactionId, mms.mPriority, mms.mSubject, - mms.mSeen, mms.mRead, mms.getSize(), rawMmsStatus, - mms.mExpiryInMillis, mms.mSentTimestampInMillis, mms.mTimestampInMillis); - - for (final DatabaseMessages.MmsPart part : mms.mParts) { - final MessagePartData messagePart = MmsUtils.createMmsMessagePart(part); - // Import media and text parts (skip SMIL and others) - if (messagePart != null) { - message.addPart(messagePart); - } - } - - if (!message.getParts().iterator().hasNext()) { - message.addPart(MessagePartData.createEmptyMessagePart()); - } - - return message; - } - - public static MessagePartData createMmsMessagePart(final DatabaseMessages.MmsPart part) { - MessagePartData messagePart = null; - if (part.isText()) { - final int mmsTextLengthLimit = - BugleGservices.get().getInt(BugleGservicesKeys.MMS_TEXT_LIMIT, - BugleGservicesKeys.MMS_TEXT_LIMIT_DEFAULT); - String text = part.mText; - if (text != null && text.length() > mmsTextLengthLimit) { - // Limit the text to a reasonable value. We ran into a situation where a vcard - // with a photo was sent as plain text. The massive amount of text caused the - // app to hang, ANR, and eventually crash in native text code. - text = text.substring(0, mmsTextLengthLimit); - } - messagePart = MessagePartData.createTextMessagePart(text); - } else if (part.isMedia()) { - messagePart = MessagePartData.createMediaMessagePart(part.mContentType, - part.getDataUri(), MessagePartData.UNSPECIFIED_SIZE, - MessagePartData.UNSPECIFIED_SIZE); - } - return messagePart; - } - - public static class StatusPlusUri { - // The request status to be as the result of the operation - // e.g. MMS_REQUEST_MANUAL_RETRY - public final int status; - // The raw telephony status - public final int rawStatus; - // The raw telephony URI - public final Uri uri; - // The operation result code from system api invocation (sent by system) - // or mapped from internal exception (sent by app) - public final int resultCode; - - public StatusPlusUri(final int status, final int rawStatus, final Uri uri) { - this.status = status; - this.rawStatus = rawStatus; - this.uri = uri; - resultCode = MessageData.UNKNOWN_RESULT_CODE; - } - - public StatusPlusUri(final int status, final int rawStatus, final Uri uri, - final int resultCode) { - this.status = status; - this.rawStatus = rawStatus; - this.uri = uri; - this.resultCode = resultCode; - } - } - - public static class SendReqResp { - public SendReq mSendReq; - public SendConf mSendConf; - - public SendReqResp(final SendReq sendReq, final SendConf sendConf) { - mSendReq = sendReq; - mSendConf = sendConf; - } - } - - /** - * Returned when sending/downloading MMS via platform APIs. In that case, we have to wait to - * receive the pending intent to determine status. - */ - public static final StatusPlusUri STATUS_PENDING = new StatusPlusUri(-1, -1, null); - - public static StatusPlusUri downloadMmsMessage(final Context context, final Uri notificationUri, - final int subId, final String subPhoneNumber, final String transactionId, - final String contentLocation, final boolean autoDownload, - final long receivedTimestampInSeconds, Bundle extras) { - if (TextUtils.isEmpty(contentLocation)) { - LogUtil.e(TAG, "MmsUtils: Download from empty content location URL"); - return new StatusPlusUri( - MMS_REQUEST_NO_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, null); - } - if (!isMmsDataAvailable(subId)) { - LogUtil.e(TAG, - "MmsUtils: failed to download message, no data available"); - return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY, - MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, - null, - SmsManager.MMS_ERROR_NO_DATA_NETWORK); - } - int status = MMS_REQUEST_MANUAL_RETRY; - try { - RetrieveConf retrieveConf = null; - if (DebugUtils.isDebugEnabled() && - MediaScratchFileProvider - .isMediaScratchSpaceUri(Uri.parse(contentLocation))) { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "MmsUtils: Reading MMS from dump file: " + contentLocation); - } - final String fileName = Uri.parse(contentLocation).getPathSegments().get(1); - final byte[] data = DebugUtils.receiveFromDumpFile(fileName); - retrieveConf = receiveFromDumpFile(data); - } else { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "MmsUtils: Downloading MMS via MMS lib API; notification " - + "message: " + notificationUri); - } - if (OsUtil.isAtLeastL_MR1()) { - if (subId < 0) { - LogUtil.e(TAG, "MmsUtils: Incoming MMS came from unknown SIM"); - throw new MmsFailureException(MMS_REQUEST_NO_RETRY, - "Message from unknown SIM"); - } - } else { - Assert.isTrue(subId == ParticipantData.DEFAULT_SELF_SUB_ID); - } - if (extras == null) { - extras = new Bundle(); - } - extras.putParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI, notificationUri); - extras.putInt(DownloadMmsAction.EXTRA_SUB_ID, subId); - extras.putString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER, subPhoneNumber); - extras.putString(DownloadMmsAction.EXTRA_TRANSACTION_ID, transactionId); - extras.putString(DownloadMmsAction.EXTRA_CONTENT_LOCATION, contentLocation); - extras.putBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD, autoDownload); - extras.putLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP, - receivedTimestampInSeconds); - - MmsSender.downloadMms(context, subId, contentLocation, extras); - return STATUS_PENDING; // Download happens asynchronously; no status to return - } - return insertDownloadedMessageAndSendResponse(context, notificationUri, subId, - subPhoneNumber, transactionId, contentLocation, autoDownload, - receivedTimestampInSeconds, retrieveConf); - - } catch (final MmsFailureException e) { - LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e); - status = e.retryHint; - } catch (final InvalidHeaderValueException e) { - LogUtil.e(TAG, "MmsUtils: failed to download message " + notificationUri, e); - } - return new StatusPlusUri(status, PDU_HEADER_VALUE_UNDEFINED, null); - } - - public static StatusPlusUri insertDownloadedMessageAndSendResponse(final Context context, - final Uri notificationUri, final int subId, final String subPhoneNumber, - final String transactionId, final String contentLocation, - final boolean autoDownload, final long receivedTimestampInSeconds, - final RetrieveConf retrieveConf) { - final byte[] transactionIdBytes = stringToBytes(transactionId, "UTF-8"); - Uri messageUri = null; - int status = MMS_REQUEST_MANUAL_RETRY; - int retrieveStatus = PDU_HEADER_VALUE_UNDEFINED; - - retrieveStatus = retrieveConf.getRetrieveStatus(); - if (retrieveStatus == PduHeaders.RETRIEVE_STATUS_OK) { - status = MMS_REQUEST_SUCCEEDED; - } else if (retrieveStatus >= PduHeaders.RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE && - retrieveStatus < PduHeaders.RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE) { - status = MMS_REQUEST_AUTO_RETRY; - } else { - // else not meant to retry download - status = MMS_REQUEST_NO_RETRY; - LogUtil.e(TAG, "MmsUtils: failed to retrieve message; retrieveStatus: " - + retrieveStatus); - } - final ContentValues values = new ContentValues(1); - values.put(Mms.RETRIEVE_STATUS, retrieveConf.getRetrieveStatus()); - SqliteWrapper.update(context, context.getContentResolver(), - notificationUri, values, null, null); - - if (status == MMS_REQUEST_SUCCEEDED) { - // Send response of the notification - if (autoDownload) { - sendNotifyResponseForMmsDownload(context, subId, transactionIdBytes, - contentLocation, PduHeaders.STATUS_RETRIEVED); - } else { - sendAcknowledgeForMmsDownload(context, subId, transactionIdBytes, contentLocation); - } - - // Insert downloaded message into telephony - final Uri inboxUri = MmsUtils.insertReceivedMmsMessage(context, retrieveConf, subId, - subPhoneNumber, receivedTimestampInSeconds, contentLocation); - messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, ContentUris.parseId(inboxUri)); - } else if (status == MMS_REQUEST_AUTO_RETRY) { - // For a retry do nothing - } else if (status == MMS_REQUEST_MANUAL_RETRY && autoDownload) { - // Failure from autodownload - just treat like manual download - sendNotifyResponseForMmsDownload(context, subId, transactionIdBytes, - contentLocation, PduHeaders.STATUS_DEFERRED); - } - return new StatusPlusUri(status, retrieveStatus, messageUri); - } - - /** - * Send response for MMS download - catches and ignores errors - */ - public static void sendNotifyResponseForMmsDownload(final Context context, final int subId, - final byte[] transactionId, final String contentLocation, final int status) { - try { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "MmsUtils: Sending M-NotifyResp.ind for received MMS, status: " - + String.format("0x%X", status)); - } - if (contentLocation == null) { - LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; contentLocation is null"); - return; - } - if (transactionId == null) { - LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; transaction id is null"); - return; - } - if (!isMmsDataAvailable(subId)) { - LogUtil.w(TAG, "MmsUtils: Can't send NotifyResp; no data available"); - return; - } - MmsSender.sendNotifyResponseForMmsDownload( - context, subId, transactionId, contentLocation, status); - } catch (final MmsFailureException e) { - LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e); - } catch (final InvalidHeaderValueException e) { - LogUtil.e(TAG, "sendNotifyResponseForMmsDownload: failed to retrieve message " + e, e); - } - } - - /** - * Send acknowledge for mms download - catched and ignores errors - */ - public static void sendAcknowledgeForMmsDownload(final Context context, final int subId, - final byte[] transactionId, final String contentLocation) { - try { - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "MmsUtils: Sending M-Acknowledge.ind for received MMS"); - } - if (contentLocation == null) { - LogUtil.w(TAG, "MmsUtils: Can't send AckInd; contentLocation is null"); - return; - } - if (transactionId == null) { - LogUtil.w(TAG, "MmsUtils: Can't send AckInd; transaction id is null"); - return; - } - if (!isMmsDataAvailable(subId)) { - LogUtil.w(TAG, "MmsUtils: Can't send AckInd; no data available"); - return; - } - MmsSender.sendAcknowledgeForMmsDownload(context, subId, transactionId, contentLocation); - } catch (final MmsFailureException e) { - LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e); - } catch (final InvalidHeaderValueException e) { - LogUtil.e(TAG, "sendAcknowledgeForMmsDownload: failed to retrieve message " + e, e); - } - } - - /** - * Try parsing a PDU without knowing the carrier. This is useful for importing - * MMS or storing draft when carrier info is not available - * - * @param data The PDU data - * @return Parsed PDU, null if failed to parse - */ - private static GenericPdu parsePduForAnyCarrier(final byte[] data) { - GenericPdu pdu = null; - try { - pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse(); - } catch (final RuntimeException e) { - LogUtil.d(TAG, "parsePduForAnyCarrier: Failed to parse PDU with content disposition", - e); - } - if (pdu == null) { - try { - pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse(); - } catch (final RuntimeException e) { - LogUtil.d(TAG, - "parsePduForAnyCarrier: Failed to parse PDU without content disposition", - e); - } - } - return pdu; - } - - private static RetrieveConf receiveFromDumpFile(final byte[] data) throws MmsFailureException { - final GenericPdu pdu = parsePduForAnyCarrier(data); - if (pdu == null || !(pdu instanceof RetrieveConf)) { - LogUtil.e(TAG, "receiveFromDumpFile: Parsing retrieved PDU failure"); - throw new MmsFailureException(MMS_REQUEST_MANUAL_RETRY, "Failed reading dump file"); - } - return (RetrieveConf) pdu; - } - - private static boolean isMmsDataAvailable(final int subId) { - if (OsUtil.isAtLeastL_MR1()) { - // L_MR1 above may support sending mms via wifi - return true; - } - final PhoneUtils phoneUtils = PhoneUtils.get(subId); - return !phoneUtils.isAirplaneModeOn() && phoneUtils.isMobileDataEnabled(); - } - - private static boolean isSmsDataAvailable(final int subId) { - if (OsUtil.isAtLeastL_MR1()) { - // L_MR1 above may support sending sms via wifi - return true; - } - final PhoneUtils phoneUtils = PhoneUtils.get(subId); - return !phoneUtils.isAirplaneModeOn(); - } - - public static boolean isMobileDataEnabled(final int subId) { - final PhoneUtils phoneUtils = PhoneUtils.get(subId); - return phoneUtils.isMobileDataEnabled(); - } - - public static boolean isAirplaneModeOn(final int subId) { - final PhoneUtils phoneUtils = PhoneUtils.get(subId); - return phoneUtils.isAirplaneModeOn(); - } - - public static StatusPlusUri sendMmsMessage(final Context context, final int subId, - final Uri messageUri, final Bundle extras) { - int status = MMS_REQUEST_MANUAL_RETRY; - int rawStatus = MessageData.RAW_TELEPHONY_STATUS_UNDEFINED; - if (!isMmsDataAvailable(subId)) { - LogUtil.w(TAG, "MmsUtils: failed to send message, no data available"); - return new StatusPlusUri(MMS_REQUEST_MANUAL_RETRY, - MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, - messageUri, - SmsManager.MMS_ERROR_NO_DATA_NETWORK); - } - final PduPersister persister = PduPersister.getPduPersister(context); - try { - final SendReq sendReq = (SendReq) persister.load(messageUri); - if (sendReq == null) { - LogUtil.w(TAG, "MmsUtils: Sending MMS was deleted; uri = " + messageUri); - return new StatusPlusUri(MMS_REQUEST_NO_RETRY, - MessageData.RAW_TELEPHONY_STATUS_UNDEFINED, messageUri); - } - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, String.format("MmsUtils: Sending MMS, message uri: %s", messageUri)); - } - extras.putInt(SendMessageAction.KEY_SUB_ID, subId); - MmsSender.sendMms(context, subId, messageUri, sendReq, extras); - return STATUS_PENDING; - } catch (final MmsFailureException e) { - status = e.retryHint; - rawStatus = e.rawStatus; - LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); - } catch (final InvalidHeaderValueException e) { - LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); - } catch (final IllegalArgumentException e) { - LogUtil.e(TAG, "MmsUtils: invalid message to send " + e, e); - } catch (final MmsException e) { - LogUtil.e(TAG, "MmsUtils: failed to send message " + e, e); - } - // If we get here, some exception occurred - return new StatusPlusUri(status, rawStatus, messageUri); - } - - public static StatusPlusUri updateSentMmsMessageStatus(final Context context, - final Uri messageUri, final SendConf sendConf) { - int status = MMS_REQUEST_MANUAL_RETRY; - final int respStatus = sendConf.getResponseStatus(); - - final ContentValues values = new ContentValues(2); - values.put(Mms.RESPONSE_STATUS, respStatus); - final byte[] messageId = sendConf.getMessageId(); - if (messageId != null && messageId.length > 0) { - values.put(Mms.MESSAGE_ID, PduPersister.toIsoString(messageId)); - } - SqliteWrapper.update(context, context.getContentResolver(), - messageUri, values, null, null); - if (respStatus == PduHeaders.RESPONSE_STATUS_OK) { - status = MMS_REQUEST_SUCCEEDED; - } else if (respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE || - respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM || - respStatus == PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS) { - status = MMS_REQUEST_AUTO_RETRY; - } else { - // else permanent failure - LogUtil.e(TAG, "MmsUtils: failed to send message; respStatus = " - + String.format("0x%X", respStatus)); - } - return new StatusPlusUri(status, respStatus, messageUri); - } - - public static void clearMmsStatus(final Context context, final Uri uri) { - // Messaging application can leave invalid values in STATUS field of M-Notification.ind - // messages. Take this opportunity to clear it. - // Downloading status just kept in local db and not reflected into telephony. - final ContentValues values = new ContentValues(1); - values.putNull(Mms.STATUS); - SqliteWrapper.update(context, context.getContentResolver(), - uri, values, null, null); - } - - // Selection for new dedup algorithm: - // ((m_type<>130) OR (exp>NOW)) AND (date>NOW-7d) AND (date<NOW+7d) AND (ct_l=xxxxxx) - // i.e. If it is NotificationInd and not expired or not NotificationInd - // AND message is received with +/- 7 days from now - // AND content location is the input URL - private static final String DUP_NOTIFICATION_QUERY_SELECTION = - "((" + Mms.MESSAGE_TYPE + "<>?) OR (" + Mms.EXPIRY + ">?)) AND (" - + Mms.DATE + ">?) AND (" + Mms.DATE + "<?) AND (" + Mms.CONTENT_LOCATION + - "=?)"; - // Selection for old behavior: only checks NotificationInd and its content location - private static final String DUP_NOTIFICATION_QUERY_SELECTION_OLD = - "(" + Mms.MESSAGE_TYPE + "=?) AND (" + Mms.CONTENT_LOCATION + "=?)"; - - private static final int MAX_RETURN = 32; - private static String[] getDupNotifications(final Context context, final NotificationInd nInd) { - final byte[] rawLocation = nInd.getContentLocation(); - if (rawLocation != null) { - final String location = new String(rawLocation); - // We can not be sure if the content location of an MMS is globally and historically - // unique. So we limit the dedup time within the last 7 days - // (or configured by gservices remotely). If the same content location shows up after - // that, we will download regardless. Duplicated message is better than no message. - String selection; - String[] selectionArgs; - final long timeLimit = BugleGservices.get().getLong( - BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS, - BugleGservicesKeys.MMS_WAP_PUSH_DEDUP_TIME_LIMIT_SECS_DEFAULT); - if (timeLimit > 0) { - // New dedup algorithm - selection = DUP_NOTIFICATION_QUERY_SELECTION; - final long nowSecs = System.currentTimeMillis() / 1000; - final long timeLowerBoundSecs = nowSecs - timeLimit; - // Need upper bound to protect against clock change so that a message has a time - // stamp in the future - final long timeUpperBoundSecs = nowSecs + timeLimit; - selectionArgs = new String[] { - Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND), - Long.toString(nowSecs), - Long.toString(timeLowerBoundSecs), - Long.toString(timeUpperBoundSecs), - location - }; - } else { - // If time limit is 0, we revert back to old behavior in case the new - // dedup algorithm behaves badly - selection = DUP_NOTIFICATION_QUERY_SELECTION_OLD; - selectionArgs = new String[] { - Integer.toString(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND), - location - }; - } - Cursor cursor = null; - try { - cursor = SqliteWrapper.query( - context, context.getContentResolver(), - Mms.CONTENT_URI, new String[] { Mms._ID }, - selection, selectionArgs, null); - final int dupCount = cursor.getCount(); - if (dupCount > 0) { - // We already received the same notification before. - // Don't want to return too many dups. It is only for debugging. - final int returnCount = dupCount < MAX_RETURN ? dupCount : MAX_RETURN; - final String[] dups = new String[returnCount]; - for (int i = 0; cursor.moveToNext() && i < returnCount; i++) { - dups[i] = cursor.getString(0); - } - return dups; - } - } catch (final SQLiteException e) { - LogUtil.e(TAG, "query failure: " + e, e); - } finally { - cursor.close(); - } - } - return null; - } - - /** - * Try parse the address using RFC822 format. If it fails to parse, then return the - * original address - * - * @param address The MMS ind sender address to parse - * @return The real address. If in RFC822 format, returns the correct email. - */ - private static String parsePotentialRfc822EmailAddress(final String address) { - if (address == null || !address.contains("@") || !address.contains("<")) { - return address; - } - final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address); - if (tokens != null && tokens.length > 0) { - for (final Rfc822Token token : tokens) { - if (token != null && !TextUtils.isEmpty(token.getAddress())) { - return token.getAddress(); - } - } - } - return address; - } - - public static DatabaseMessages.MmsMessage processReceivedPdu(final Context context, - final byte[] pushData, final int subId, final String subPhoneNumber) { - // Parse data - - // Insert placeholder row to telephony and local db - // Get raw PDU push-data from the message and parse it - final PduParser parser = new PduParser(pushData, - MmsConfig.get(subId).getSupportMmsContentDisposition()); - final GenericPdu pdu = parser.parse(); - - if (null == pdu) { - LogUtil.e(TAG, "Invalid PUSH data"); - return null; - } - - final PduPersister p = PduPersister.getPduPersister(context); - final int type = pdu.getMessageType(); - - Uri messageUri = null; - switch (type) { - case PduHeaders.MESSAGE_TYPE_DELIVERY_IND: - case PduHeaders.MESSAGE_TYPE_READ_ORIG_IND: { - // TODO: Should this be commented out? -// threadId = findThreadId(context, pdu, type); -// if (threadId == -1) { -// // The associated SendReq isn't found, therefore skip -// // processing this PDU. -// break; -// } - -// Uri uri = p.persist(pdu, Inbox.CONTENT_URI, true, -// MessagingPreferenceActivity.getIsGroupMmsEnabled(mContext), null); -// // Update thread ID for ReadOrigInd & DeliveryInd. -// ContentValues values = new ContentValues(1); -// values.put(Mms.THREAD_ID, threadId); -// SqliteWrapper.update(mContext, cr, uri, values, null, null); - LogUtil.w(TAG, "Received unsupported WAP Push, type=" + type); - break; - } - case PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND: { - final NotificationInd nInd = (NotificationInd) pdu; - - if (MmsConfig.get(subId).getTransIdEnabled()) { - final byte [] contentLocationTemp = nInd.getContentLocation(); - if ('=' == contentLocationTemp[contentLocationTemp.length - 1]) { - final byte [] transactionIdTemp = nInd.getTransactionId(); - final byte [] contentLocationWithId = - new byte [contentLocationTemp.length - + transactionIdTemp.length]; - System.arraycopy(contentLocationTemp, 0, contentLocationWithId, - 0, contentLocationTemp.length); - System.arraycopy(transactionIdTemp, 0, contentLocationWithId, - contentLocationTemp.length, transactionIdTemp.length); - nInd.setContentLocation(contentLocationWithId); - } - } - final String[] dups = getDupNotifications(context, nInd); - if (dups == null) { - // TODO: Do we handle Rfc822 Email Addresses? - //final String contentLocation = - // MmsUtils.bytesToString(nInd.getContentLocation(), "UTF-8"); - //final byte[] transactionId = nInd.getTransactionId(); - //final long messageSize = nInd.getMessageSize(); - //final long expiry = nInd.getExpiry(); - //final String transactionIdString = - // MmsUtils.bytesToString(transactionId, "UTF-8"); - - //final EncodedStringValue fromEncoded = nInd.getFrom(); - // An mms ind received from email address will have from address shown as - // "John Doe <johndoe@foobar.com>" but the actual received message will only - // have the email address. So let's try to parse the RFC822 format to get the - // real email. Otherwise we will create two conversations for the MMS - // notification and the actual MMS message if auto retrieve is disabled. - //final String from = parsePotentialRfc822EmailAddress( - // fromEncoded != null ? fromEncoded.getString() : null); - - Uri inboxUri = null; - try { - inboxUri = p.persist(pdu, Mms.Inbox.CONTENT_URI, subId, subPhoneNumber, - null); - messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, - ContentUris.parseId(inboxUri)); - } catch (final MmsException e) { - LogUtil.e(TAG, "Failed to save the data from PUSH: type=" + type, e); - } - } else { - LogUtil.w(TAG, "Received WAP Push is a dup: " + Joiner.on(',').join(dups)); - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - LogUtil.w(TAG, "Dup WAP Push url=" + new String(nInd.getContentLocation())); - } - } - break; - } - default: - LogUtil.e(TAG, "Received unrecognized WAP Push, type=" + type); - } - - DatabaseMessages.MmsMessage mms = null; - if (messageUri != null) { - mms = MmsUtils.loadMms(messageUri); - } - return mms; - } - - public static Uri insertSendingMmsMessage(final Context context, final List<String> recipients, - final MessageData content, final int subId, final String subPhoneNumber, - final long timestamp) { - final SendReq sendReq = createMmsSendReq( - context, subId, recipients.toArray(new String[recipients.size()]), content, - DEFAULT_DELIVERY_REPORT_MODE, - DEFAULT_READ_REPORT_MODE, - DEFAULT_EXPIRY_TIME_IN_SECONDS, - DEFAULT_PRIORITY, - timestamp); - Uri messageUri = null; - if (sendReq != null) { - final Uri outboxUri = MmsUtils.insertSendReq(context, sendReq, subId, subPhoneNumber); - if (outboxUri != null) { - messageUri = ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI, - ContentUris.parseId(outboxUri)); - if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) { - LogUtil.d(TAG, "Mmsutils: Inserted sending MMS message into telephony, uri: " - + outboxUri); - } - } else { - LogUtil.e(TAG, "insertSendingMmsMessage: failed to persist message into telephony"); - } - } - return messageUri; - } - - public static MessageData readSendingMmsMessage(final Uri messageUri, - final String conversationId, final String participantId, final String selfId) { - MessageData message = null; - if (messageUri != null) { - final DatabaseMessages.MmsMessage mms = MmsUtils.loadMms(messageUri); - - // Make sure that the message has not been deleted from the Telephony DB - if (mms != null) { - // Transform the message - message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId, - MessageData.BUGLE_STATUS_OUTGOING_RESENDING); - } - } - return message; - } - - /** - * Create an MMS message with subject, text and image - * - * @return Both the M-Send.req and the M-Send.conf for processing in the caller - * @throws MmsException - */ - private static SendReq createMmsSendReq(final Context context, final int subId, - final String[] recipients, final MessageData message, - final boolean requireDeliveryReport, final boolean requireReadReport, - final long expiryTime, final int priority, final long timestampMillis) { - Assert.notNull(context); - if (recipients == null || recipients.length < 1) { - throw new IllegalArgumentException("MMS sendReq no recipient"); - } - - // Make a copy so we don't propagate changes to recipients to outside of this method - final String[] recipientsCopy = new String[recipients.length]; - // Don't send phone number as is since some received phone number is malformed - // for sending. We need to strip the separators. - for (int i = 0; i < recipients.length; i++) { - final String recipient = recipients[i]; - if (EmailAddress.isValidEmail(recipients[i])) { - // Don't do stripping for emails - recipientsCopy[i] = recipient; - } else { - recipientsCopy[i] = stripPhoneNumberSeparators(recipient); - } - } - - SendReq sendReq = null; - try { - sendReq = createSendReq(context, subId, recipientsCopy, - message, requireDeliveryReport, - requireReadReport, expiryTime, priority, timestampMillis); - } catch (final InvalidHeaderValueException e) { - LogUtil.e(TAG, "InvalidHeaderValue creating sendReq PDU"); - } catch (final OutOfMemoryError e) { - LogUtil.e(TAG, "Out of memory error creating sendReq PDU"); - } - return sendReq; - } - - /** - * Stripping out the invalid characters in a phone number before sending - * MMS. We only keep alphanumeric and '*', '#', '+'. - */ - private static String stripPhoneNumberSeparators(final String phoneNumber) { - if (phoneNumber == null) { - return null; - } - final int len = phoneNumber.length(); - final StringBuilder ret = new StringBuilder(len); - for (int i = 0; i < len; i++) { - final char c = phoneNumber.charAt(i); - if (Character.isLetterOrDigit(c) || c == '+' || c == '*' || c == '#') { - ret.append(c); - } - } - return ret.toString(); - } - - /** - * Create M-Send.req for the MMS message to be sent. - * - * @return the M-Send.req - * @throws InvalidHeaderValueException if there is any error in parsing the input - */ - static SendReq createSendReq(final Context context, final int subId, - final String[] recipients, final MessageData message, - final boolean requireDeliveryReport, - final boolean requireReadReport, final long expiryTime, final int priority, - final long timestampMillis) - throws InvalidHeaderValueException { - final SendReq req = new SendReq(); - // From, per spec - final String lineNumber = PhoneUtils.get(subId).getCanonicalForSelf(true/*allowOverride*/); - if (!TextUtils.isEmpty(lineNumber)) { - req.setFrom(new EncodedStringValue(lineNumber)); - } - // To - final EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(recipients); - if (encodedNumbers != null) { - req.setTo(encodedNumbers); - } - // Subject - if (!TextUtils.isEmpty(message.getMmsSubject())) { - req.setSubject(new EncodedStringValue(message.getMmsSubject())); - } - // Date - req.setDate(timestampMillis / 1000L); - // Body - final MmsInfo bodyInfo = MmsUtils.makePduBody(context, message, subId); - req.setBody(bodyInfo.mPduBody); - // Message size - req.setMessageSize(bodyInfo.mMessageSize); - // Message class - req.setMessageClass(PduHeaders.MESSAGE_CLASS_PERSONAL_STR.getBytes()); - // Expiry - req.setExpiry(expiryTime); - // Priority - req.setPriority(priority); - // Delivery report - req.setDeliveryReport(requireDeliveryReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO); - // Read report - req.setReadReport(requireReadReport ? PduHeaders.VALUE_YES : PduHeaders.VALUE_NO); - return req; - } - - public static boolean isDeliveryReportRequired(final int subId) { - if (!MmsConfig.get(subId).getSMSDeliveryReportsEnabled()) { - return false; - } - final Context context = Factory.get().getApplicationContext(); - final Resources res = context.getResources(); - final BuglePrefs prefs = BuglePrefs.getSubscriptionPrefs(subId); - final String deliveryReportKey = res.getString(R.string.delivery_reports_pref_key); - final boolean defaultValue = res.getBoolean(R.bool.delivery_reports_pref_default); - return prefs.getBoolean(deliveryReportKey, defaultValue); - } - - public static int sendSmsMessage(final String recipient, final String messageText, - final Uri requestUri, final int subId, - final String smsServiceCenter, final boolean requireDeliveryReport) { - if (!isSmsDataAvailable(subId)) { - LogUtil.w(TAG, "MmsUtils: can't send SMS without radio"); - return MMS_REQUEST_MANUAL_RETRY; - } - final Context context = Factory.get().getApplicationContext(); - int status = MMS_REQUEST_MANUAL_RETRY; - try { - // Send a single message - final SendResult result = SmsSender.sendMessage( - context, - subId, - recipient, - messageText, - smsServiceCenter, - requireDeliveryReport, - requestUri); - if (!result.hasPending()) { - // not timed out, check failures - final int failureLevel = result.getHighestFailureLevel(); - switch (failureLevel) { - case SendResult.FAILURE_LEVEL_NONE: - status = MMS_REQUEST_SUCCEEDED; - break; - case SendResult.FAILURE_LEVEL_TEMPORARY: - status = MMS_REQUEST_AUTO_RETRY; - LogUtil.e(TAG, "MmsUtils: SMS temporary failure"); - break; - case SendResult.FAILURE_LEVEL_PERMANENT: - LogUtil.e(TAG, "MmsUtils: SMS permanent failure"); - break; - } - } else { - // Timed out - LogUtil.e(TAG, "MmsUtils: sending SMS timed out"); - } - } catch (final Exception e) { - LogUtil.e(TAG, "MmsUtils: failed to send SMS " + e, e); - } - return status; - } - - /** - * Delete SMS and MMS messages in a particular thread - * - * @return the number of messages deleted - */ - public static int deleteThread(final long threadId, final long cutOffTimestampInMillis) { - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - final Uri threadUri = ContentUris.withAppendedId(Telephony.Threads.CONTENT_URI, threadId); - if (cutOffTimestampInMillis < Long.MAX_VALUE) { - return resolver.delete(threadUri, Sms.DATE + "<=?", - new String[] { Long.toString(cutOffTimestampInMillis) }); - } else { - return resolver.delete(threadUri, null /* smsSelection */, null /* selectionArgs */); - } - } - - /** - * Delete single SMS and MMS message - * - * @return number of rows deleted (should be 1 or 0) - */ - public static int deleteMessage(final Uri messageUri) { - final ContentResolver resolver = Factory.get().getApplicationContext().getContentResolver(); - return resolver.delete(messageUri, null /* selection */, null /* selectionArgs */); - } - - public static byte[] createDebugNotificationInd(final String fileName) { - byte[] pduData = null; - try { - final Context context = Factory.get().getApplicationContext(); - // Load the message file - final byte[] data = DebugUtils.receiveFromDumpFile(fileName); - final RetrieveConf retrieveConf = receiveFromDumpFile(data); - // Create the notification - final NotificationInd notification = new NotificationInd(); - final long expiry = System.currentTimeMillis() / 1000 + 600; - notification.setTransactionId(fileName.getBytes()); - notification.setMmsVersion(retrieveConf.getMmsVersion()); - notification.setFrom(retrieveConf.getFrom()); - notification.setSubject(retrieveConf.getSubject()); - notification.setExpiry(expiry); - notification.setMessageSize(data.length); - notification.setMessageClass(retrieveConf.getMessageClass()); - - final Uri.Builder builder = MediaScratchFileProvider.getUriBuilder(); - builder.appendPath(fileName); - final Uri contentLocation = builder.build(); - notification.setContentLocation(contentLocation.toString().getBytes()); - - // Serialize - pduData = new PduComposer(context, notification).make(); - if (pduData == null || pduData.length < 1) { - throw new IllegalArgumentException("Empty or zero length PDU data"); - } - } catch (final MmsFailureException e) { - // Nothing to do - } catch (final InvalidHeaderValueException e) { - // Nothing to do - } - return pduData; - } - - public static int mapRawStatusToErrorResourceId(final int bugleStatus, final int rawStatus) { - int stringResId = R.string.message_status_send_failed; - switch (rawStatus) { - case PduHeaders.RESPONSE_STATUS_ERROR_SERVICE_DENIED: - case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_LIMITATIONS_NOT_MET: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_REQUEST_NOT_ACCEPTED: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_FORWARDING_DENIED: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_NOT_SUPPORTED: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_ADDRESS_HIDING_NOT_SUPPORTED: - //case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID: - stringResId = R.string.mms_failure_outgoing_service; - break; - case PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED: - case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_SENDNG_ADDRESS_UNRESOLVED: - case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED: - stringResId = R.string.mms_failure_outgoing_address; - break; - case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_FORMAT_CORRUPT: - case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT: - stringResId = R.string.mms_failure_outgoing_corrupt; - break; - case PduHeaders.RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED: - case PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED: - stringResId = R.string.mms_failure_outgoing_content; - break; - case PduHeaders.RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE: - //case PduHeaders.RESPONSE_STATUS_ERROR_MESSAGE_NOT_FOUND: - //case PduHeaders.RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND: - stringResId = R.string.mms_failure_outgoing_unsupported; - break; - case MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG: - stringResId = R.string.mms_failure_outgoing_too_large; - break; - } - return stringResId; - } - - /** - * The absence of a connection type. - */ - public static final int TYPE_NONE = -1; - - public static int getConnectivityEventNetworkType(final Context context, final Intent intent) { - final ConnectivityManager connMgr = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (OsUtil.isAtLeastJB_MR1()) { - return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE); - } else { - final NetworkInfo info = (NetworkInfo) intent.getParcelableExtra( - ConnectivityManager.EXTRA_NETWORK_INFO); - if (info != null) { - return info.getType(); - } - } - return TYPE_NONE; - } - - /** - * Dump the raw MMS data into a file - * - * @param rawPdu The raw pdu data - * @param pdu The parsed pdu, used to construct a dump file name - */ - public static void dumpPdu(final byte[] rawPdu, final GenericPdu pdu) { - if (rawPdu == null || rawPdu.length < 1) { - return; - } - final String dumpFileName = MmsUtils.MMS_DUMP_PREFIX + getDumpFileId(pdu); - final File dumpFile = DebugUtils.getDebugFile(dumpFileName, true); - if (dumpFile != null) { - try { - final FileOutputStream fos = new FileOutputStream(dumpFile); - final BufferedOutputStream bos = new BufferedOutputStream(fos); - try { - bos.write(rawPdu); - bos.flush(); - } finally { - bos.close(); - } - DebugUtils.ensureReadable(dumpFile); - } catch (final IOException e) { - LogUtil.e(TAG, "dumpPdu: " + e, e); - } - } - } - - /** - * Get the dump file id based on the parsed PDU - * 1. Use message id if not empty - * 2. Use transaction id if message id is empty - * 3. If all above is empty, use random UUID - * - * @param pdu the parsed PDU - * @return the id of the dump file - */ - private static String getDumpFileId(final GenericPdu pdu) { - String fileId = null; - if (pdu != null && pdu instanceof RetrieveConf) { - final RetrieveConf retrieveConf = (RetrieveConf) pdu; - if (retrieveConf.getMessageId() != null) { - fileId = new String(retrieveConf.getMessageId()); - } else if (retrieveConf.getTransactionId() != null) { - fileId = new String(retrieveConf.getTransactionId()); - } - } - if (TextUtils.isEmpty(fileId)) { - fileId = UUID.randomUUID().toString(); - } - return fileId; - } -} diff --git a/src/com/android/messaging/sms/SmsException.java b/src/com/android/messaging/sms/SmsException.java deleted file mode 100644 index 728db8c..0000000 --- a/src/com/android/messaging/sms/SmsException.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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); - } -} diff --git a/src/com/android/messaging/sms/SmsReleaseStorage.java b/src/com/android/messaging/sms/SmsReleaseStorage.java deleted file mode 100644 index 13a6284..0000000 --- a/src/com/android/messaging/sms/SmsReleaseStorage.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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(); - } - } -} diff --git a/src/com/android/messaging/sms/SmsSender.java b/src/com/android/messaging/sms/SmsSender.java deleted file mode 100644 index 889973f..0000000 --- a/src/com/android/messaging/sms/SmsSender.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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; - } -} diff --git a/src/com/android/messaging/sms/SmsStorageStatusManager.java b/src/com/android/messaging/sms/SmsStorageStatusManager.java deleted file mode 100644 index ff7b79d..0000000 --- a/src/com/android/messaging/sms/SmsStorageStatusManager.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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"; - } -} diff --git a/src/com/android/messaging/sms/SystemProperties.java b/src/com/android/messaging/sms/SystemProperties.java deleted file mode 100644 index 669e448..0000000 --- a/src/com/android/messaging/sms/SystemProperties.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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; - } -} |