Revert "Initial checkin of AOSP Messaging app."

This reverts commit 461a34b466.

Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
Mike Dodd
2015-08-12 18:48:39 +00:00
parent 461a34b466
commit ef8c7abbcf
1645 changed files with 0 additions and 186271 deletions
@@ -1,39 +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 android.support.v7.mms;
/**
* APN exception
*/
public class ApnException extends Exception {
public ApnException() {
super();
}
public ApnException(String message) {
super(message);
}
public ApnException(Throwable cause) {
super(cause);
}
public ApnException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,63 +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 android.support.v7.mms;
import java.util.List;
/**
* Interface for loading APNs for default SMS SIM
*/
public interface ApnSettingsLoader {
/**
* Interface to represent the minimal information MMS lib needs from an APN
*/
interface Apn {
/**
* Get the MMSC URL string
*
* @return MMSC URL
*/
String getMmsc();
/**
* Get the MMS proxy host address
*
* @return MMS proxy
*/
String getMmsProxy();
/**
* Get the MMS proxy host port
*
* @return the port of MMS proxy
*/
int getMmsProxyPort();
/**
* Flag the APN as a successful APN to use
*/
void setSuccess();
}
/**
* Get a list possible APN matching the subId and APN name
*
* @param apnName the APN name
* @return a list of possible APNs
*/
List<Apn> get(String apnName);
}
@@ -1,73 +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 android.support.v7.mms;
import android.content.ContentValues;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* Parser for built-in XML resource file for APN list
*/
class ApnsXmlParser extends MmsXmlResourceParser {
interface ApnProcessor {
void process(ContentValues apnValues);
}
private static final String TAG_APNS = "apns";
private static final String TAG_APN = "apn";
private final ApnProcessor mApnProcessor;
private final ContentValues mValues = new ContentValues();
ApnsXmlParser(final XmlPullParser parser, final ApnProcessor apnProcessor) {
super(parser);
mApnProcessor = apnProcessor;
}
// Parse one APN
@Override
protected void parseRecord() throws IOException, XmlPullParserException {
if (TAG_APN.equals(mInputParser.getName())) {
mValues.clear();
// Collect all the attributes
for (int i = 0; i < mInputParser.getAttributeCount(); i++) {
final String key = mInputParser.getAttributeName(i);
if (key != null) {
mValues.put(key, mInputParser.getAttributeValue(i));
}
}
// We are done parsing one APN, call the handler
if (mApnProcessor != null) {
mApnProcessor.process(mValues);
}
}
// We are at the end tag
if (mInputParser.next() != XmlPullParser.END_TAG) {
throw new XmlPullParserException("Expecting end tag @" + xmlParserDebugContext());
}
}
@Override
protected String getRootTag() {
return TAG_APNS;
}
}
@@ -1,198 +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 android.support.v7.mms;
import android.os.Bundle;
/**
* Loader for carrier dependent configuration values
*/
public interface CarrierConfigValuesLoader {
/**
* Get the carrier config values in a bundle
*
* @param subId the associated subscription ID for the carrier configuration
* @return a bundle of all the values
*/
Bundle get(int subId);
// Configuration keys and default values
/** Boolean value: if MMS is enabled */
public static final String CONFIG_ENABLED_MMS = "enabledMMS";
public static final boolean CONFIG_ENABLED_MMS_DEFAULT = true;
/**
* Boolean value: if transaction ID should be appended to
* the download URL of a single segment WAP push message
*/
public static final String CONFIG_ENABLED_TRANS_ID = "enabledTransID";
public static final boolean CONFIG_ENABLED_TRANS_ID_DEFAULT = false;
/**
* Boolean value: if acknowledge or notify response to a download
* should be sent to the WAP push message's download URL
*/
public static final String CONFIG_ENABLED_NOTIFY_WAP_MMSC = "enabledNotifyWapMMSC";
public static final boolean CONFIG_ENABLED_NOTIFY_WAP_MMSC_DEFAULT = false;
/**
* Boolean value: if phone number alias can be used
*/
public static final String CONFIG_ALIAS_ENABLED = "aliasEnabled";
public static final boolean CONFIG_ALIAS_ENABLED_DEFAULT = false;
/**
* Boolean value: if audio is allowed in attachment
*/
public static final String CONFIG_ALLOW_ATTACH_AUDIO = "allowAttachAudio";
public static final boolean CONFIG_ALLOW_ATTACH_AUDIO_DEFAULT = true;
/**
* Boolean value: if true, long sms messages are always sent as multi-part sms
* messages, with no checked limit on the number of segments. If false, then
* as soon as the user types a message longer than a single segment (i.e. 140 chars),
* the message will turn into and be sent as an mms message or separate,
* independent SMS messages (dependent on CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES flag).
* This feature exists for carriers that don't support multi-part sms.
*/
public static final String CONFIG_ENABLE_MULTIPART_SMS = "enableMultipartSMS";
public static final boolean CONFIG_ENABLE_MULTIPART_SMS_DEFAULT = true;
/**
* Boolean value: if SMS delivery report is supported
*/
public static final String CONFIG_ENABLE_SMS_DELIVERY_REPORTS = "enableSMSDeliveryReports";
public static final boolean CONFIG_ENABLE_SMS_DELIVERY_REPORTS_DEFAULT = true;
/**
* Boolean value: if group MMS is supported
*/
public static final String CONFIG_ENABLE_GROUP_MMS = "enableGroupMms";
public static final boolean CONFIG_ENABLE_GROUP_MMS_DEFAULT = true;
/**
* Boolean value: if the content_disposition field of an MMS part should be parsed
* Check wap-230-wsp-20010705-a.pdf, chapter 8.4.2.21. Most carriers support it except some.
*/
public static final String CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION =
"supportMmsContentDisposition";
public static final boolean CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT = true;
/**
* Boolean value: if the sms app should support a link to the system settings
* where amber alerts are configured.
*/
public static final String CONFIG_CELL_BROADCAST_APP_LINKS = "config_cellBroadcastAppLinks";
public static final boolean CONFIG_CELL_BROADCAST_APP_LINKS_DEFAULT = true;
/**
* Boolean value: if multipart SMS should be sent as separate SMS messages
*/
public static final String CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES =
"sendMultipartSmsAsSeparateMessages";
public static final boolean CONFIG_SEND_MULTIPART_SMS_AS_SEPARATE_MESSAGES_DEFAULT = false;
/**
* Boolean value: if MMS read report is supported
*/
public static final String CONFIG_ENABLE_MMS_READ_REPORTS = "enableMMSReadReports";
public static final boolean CONFIG_ENABLE_MMS_READ_REPORTS_DEFAULT = false;
/**
* Boolean value: if MMS delivery report is supported
*/
public static final String CONFIG_ENABLE_MMS_DELIVERY_REPORTS = "enableMMSDeliveryReports";
public static final boolean CONFIG_ENABLE_MMS_DELIVERY_REPORTS_DEFAULT = false;
/**
* Boolean value: if "charset" value is supported in the "Content-Type" HTTP header
*/
public static final String CONFIG_SUPPORT_HTTP_CHARSET_HEADER = "supportHttpCharsetHeader";
public static final boolean CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT = false;
/**
* Integer value: maximal MMS message size in bytes
*/
public static final String CONFIG_MAX_MESSAGE_SIZE = "maxMessageSize";
public static final int CONFIG_MAX_MESSAGE_SIZE_DEFAULT = 300 * 1024;
/**
* Integer value: maximal MMS image height in pixels
*/
public static final String CONFIG_MAX_IMAGE_HEIGHT = "maxImageHeight";
public static final int CONFIG_MAX_IMAGE_HEIGHT_DEFAULT = 480;
/**
* Integer value: maximal MMS image width in pixels
*/
public static final String CONFIG_MAX_IMAGE_WIDTH = "maxImageWidth";
public static final int CONFIG_MAX_IMAGE_WIDTH_DEFAULT = 640;
/**
* Integer value: limit on recipient list of an MMS message
*/
public static final String CONFIG_RECIPIENT_LIMIT = "recipientLimit";
public static final int CONFIG_RECIPIENT_LIMIT_DEFAULT = Integer.MAX_VALUE;
/**
* Integer value: HTTP socket timeout in milliseconds for MMS
*/
public static final String CONFIG_HTTP_SOCKET_TIMEOUT = "httpSocketTimeout";
public static final int CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT = 60 * 1000;
/**
* Integer value: minimal number of characters of an alias
*/
public static final String CONFIG_ALIAS_MIN_CHARS = "aliasMinChars";
public static final int CONFIG_ALIAS_MIN_CHARS_DEFAULT = 2;
/**
* Integer value: maximal number of characters of an alias
*/
public static final String CONFIG_ALIAS_MAX_CHARS = "aliasMaxChars";
public static final int CONFIG_ALIAS_MAX_CHARS_DEFAULT = 48;
/**
* Integer value: the threshold of number of SMS parts when an multipart SMS will be
* converted into an MMS, e.g. if this is "4", when an multipart SMS message has 5
* parts, then it will be sent as MMS message instead. "-1" indicates no such conversion
* can happen.
*/
public static final String CONFIG_SMS_TO_MMS_TEXT_THRESHOLD = "smsToMmsTextThreshold";
public static final int CONFIG_SMS_TO_MMS_TEXT_THRESHOLD_DEFAULT = -1;
/**
* Integer value: the threshold of SMS length when it will be converted into an MMS.
* "-1" indicates no such conversion can happen.
*/
public static final String CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD =
"smsToMmsTextLengthThreshold";
public static final int CONFIG_SMS_TO_MMS_TEXT_LENGTH_THRESHOLD_DEFAULT = -1;
/**
* Integer value: maximal length in bytes of SMS message
*/
public static final String CONFIG_MAX_MESSAGE_TEXT_SIZE = "maxMessageTextSize";
public static final int CONFIG_MAX_MESSAGE_TEXT_SIZE_DEFAULT = -1;
/**
* Integer value: maximum number of characters allowed for mms subject
*/
public static final String CONFIG_MAX_SUBJECT_LENGTH = "maxSubjectLength";
public static final int CONFIG_MAX_SUBJECT_LENGTH_DEFAULT = 40;
/**
* String value: name for the user agent profile HTTP header
*/
public static final String CONFIG_UA_PROF_TAG_NAME = "mUaProfTagName";
public static final String CONFIG_UA_PROF_TAG_NAME_DEFAULT = "x-wap-profile";
/**
* String value: additional HTTP headers for MMS HTTP requests.
* The format is
* header_1:header_value_1|header_2:header_value_2|...
* Each value can contain macros.
*/
public static final String CONFIG_HTTP_PARAMS = "httpParams";
public static final String CONFIG_HTTP_PARAMS_DEFAULT = null;
/**
* String value: number of email gateway
*/
public static final String CONFIG_EMAIL_GATEWAY_NUMBER = "emailGatewayNumber";
public static final String CONFIG_EMAIL_GATEWAY_NUMBER_DEFAULT = null;
/**
* String value: suffix for the NAI HTTP header value, e.g. ":pcs"
* (NAI is used as authentication in HTTP headers for some carriers)
*/
public static final String CONFIG_NAI_SUFFIX = "naiSuffix";
public static final String CONFIG_NAI_SUFFIX_DEFAULT = null;
}
@@ -1,68 +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 android.support.v7.mms;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* XML parser for carrier config (i.e. mms_config)
*/
class CarrierConfigXmlParser extends MmsXmlResourceParser {
interface KeyValueProcessor {
void process(String type, String key, String value);
}
private static final String TAG_MMS_CONFIG = "mms_config";
private final KeyValueProcessor mKeyValueProcessor;
CarrierConfigXmlParser(final XmlPullParser parser, final KeyValueProcessor keyValueProcessor) {
super(parser);
mKeyValueProcessor = keyValueProcessor;
}
// Parse one key/value
@Override
protected void parseRecord() 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("Expecting end tag @" + xmlParserDebugContext());
}
// We are done parsing one mms_config key/value, call the handler
if (mKeyValueProcessor != null) {
mKeyValueProcessor.process(type, key, value);
}
}
@Override
protected String getRootTag() {
return TAG_MMS_CONFIG;
}
}
@@ -1,497 +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 android.support.v7.mms;
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.SQLiteException;
import android.net.Uri;
import android.provider.Telephony;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.messaging.R;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation of APN settings loader
*/
class DefaultApnSettingsLoader 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();
}
}
/**
* An in-memory implementation of an APN. These APNs are organized into an in-memory list.
* The order of the list can be changed by the setSuccess method.
*/
private static class MemoryApn implements Apn {
/**
* Create an in-memory APN loaded from resources
*
* @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
* @return an in-memory APN instance, null if there is invalid parameter
*/
public static MemoryApn from(final List<Apn> apns, final String typesIn,
final String mmscIn, final String proxyIn, final String portIn) {
if (apns == null) {
return null;
}
final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
if (base == null) {
return null;
}
for (final Apn apn : apns) {
if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) {
return null;
}
}
return new MemoryApn(apns, base);
}
private final List<Apn> mApns;
private final BaseApn mBase;
public MemoryApn(final List<Apn> apns, final BaseApn base) {
mApns = apns;
mBase = base;
}
@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() {
// 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) {
Log.d(MmsService.TAG, "Set APN ["
+ "MMSC=" + getMmsc() + ", "
+ "PROXY=" + getMmsProxy() + ", "
+ "PORT=" + getMmsProxyPort() + "] to be first");
}
}
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 = {
Telephony.Carriers.TYPE,
Telephony.Carriers.MMSC,
Telephony.Carriers.MMSPROXY,
Telephony.Carriers.MMSPORT,
};
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 String APN_MCC = "mcc";
private static final String APN_MNC = "mnc";
private static final String APN_APN = "apn";
private static final String APN_TYPE = "type";
private static final String APN_MMSC = "mmsc";
private static final String APN_MMSPROXY = "mmsproxy";
private static final String APN_MMSPORT = "mmsport";
private final Context mContext;
// Cached APNs for subIds
private final SparseArray<List<Apn>> mApnsCache;
DefaultApnSettingsLoader(final Context context) {
mContext = context;
mApnsCache = new SparseArray<>();
}
@Override
public List<Apn> get(final String apnName) {
final int subId = Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID);
List<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) {
Log.i(MmsService.TAG, "Loaded " + apns.size() + " APNs");
}
return apns;
}
private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
// Try system APN table first
loadFromSystem(subId, apnName, apns);
if (apns.size() > 0) {
return;
}
// Try loading from apns.xml in resources
loadFromResources(subId, apnName, apns);
if (apns.size() > 0) {
return;
}
// Try resources but without APN name
loadFromResources(subId, null/*apnName*/, apns);
}
/**
* 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 (Utils.supportMSim() && 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 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) {
Log.i(MmsService.TAG, "Loading APNs from system, "
+ "checkCurrent=" + checkCurrent + " apnName=" + apnName);
final StringBuilder selectionBuilder = new StringBuilder();
String[] selectionArgs = null;
if (checkCurrent) {
selectionBuilder.append(Telephony.Carriers.CURRENT).append(" IS NOT NULL");
}
apnName = trimWithNullCheck(apnName);
if (!TextUtils.isEmpty(apnName)) {
if (selectionBuilder.length() > 0) {
selectionBuilder.append(" AND ");
}
selectionBuilder.append(Telephony.Carriers.APN).append("=?");
selectionArgs = new String[] { apnName };
}
try {
final Cursor cursor = mContext.getContentResolver().query(
uri,
APN_PROJECTION,
selectionBuilder.toString(),
selectionArgs,
null/*sortOrder*/);
if (cursor == null || cursor.getCount() < 1) {
if (cursor != null) {
cursor.close();
}
Log.w(MmsService.TAG, "Query " + uri + " with apn " + apnName + " and "
+ (checkCurrent ? "checking CURRENT" : "not checking CURRENT")
+ " returned empty");
return null;
}
return cursor;
} catch (final SQLiteException e) {
Log.w(MmsService.TAG, "APN table query exception: " + e);
} catch (final SecurityException e) {
Log.w(MmsService.TAG, "Platform restricts APN table access: " + e);
throw e;
}
return null;
}
/**
* Find matching APNs using builtin APN list resource
*
* @param subId the SIM subId
* @param apnName the APN name to match
* @param apns the list for returning results
*/
private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) {
Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName);
final int[] mccMnc = Utils.getMccMnc(mContext, subId);
if (mccMnc[0] == 0 && mccMnc[0] == 0) {
Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system");
return;
}
// MCC/MNC is good, loading/querying APNs from XML
XmlResourceParser xml = null;
try {
xml = mContext.getResources().getXml(R.xml.apns);
new ApnsXmlParser(xml, new ApnsXmlParser.ApnProcessor() {
@Override
public void process(ContentValues apnValues) {
final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC));
final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC));
final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN));
try {
if (mccMnc[0] == Integer.parseInt(mcc) &&
mccMnc[1] == Integer.parseInt(mnc) &&
(TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) {
final String type = apnValues.getAsString(APN_TYPE);
final String mmsc = apnValues.getAsString(APN_MMSC);
final String mmsproxy = apnValues.getAsString(APN_MMSPROXY);
final String mmsport = apnValues.getAsString(APN_MMSPORT);
final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport);
if (newApn != null) {
apns.add(newApn);
}
}
} catch (final NumberFormatException e) {
// Ignore
}
}
}).parse();
} catch (final Resources.NotFoundException e) {
Log.w(MmsService.TAG, "Can not get apns.xml " + e);
} finally {
if (xml != null) {
xml.close();
}
}
}
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;
}
}
@@ -1,126 +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 android.support.v7.mms;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.os.Bundle;
import android.util.Log;
import android.util.SparseArray;
import com.android.messaging.R;
/**
* The default implementation of loader for carrier config values
*/
class DefaultCarrierConfigValuesLoader 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;
DefaultCarrierConfigValuesLoader(final Context context) {
mContext = context;
mValuesCache = new SparseArray<>();
}
@Override
public Bundle get(int subId) {
subId = Utils.getEffectiveSubscriptionId(subId);
Bundle values;
boolean didLoad = false;
synchronized (this) {
values = mValuesCache.get(subId);
if (values == null) {
values = new Bundle();
mValuesCache.put(subId, values);
loadLocked(subId, values);
didLoad = true;
}
}
if (didLoad) {
Log.i(MmsService.TAG, "Carrier configs loaded: " + values);
}
return values;
}
private void loadLocked(final int subId, final Bundle values) {
// For K and earlier, load from resources
loadFromResources(subId, values);
if (Utils.hasMmsApi()) {
// For L and later, also load from system MMS service
loadFromSystem(subId, values);
}
}
/**
* 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 = Utils.getSmsManager(subId).getCarrierConfigValues();
if (systemValues != null) {
values.putAll(systemValues);
}
} catch (final Exception e) {
Log.w(MmsService.TAG, "Calling system getCarrierConfigValues exception", e);
}
}
private void loadFromResources(final int subId, final Bundle values) {
// Get a subscription-dependent context for loading the mms_config.xml
final Context subContext = Utils.getSubDepContext(mContext, subId);
XmlResourceParser xml = null;
try {
xml = subContext.getResources().getXml(R.xml.mms_config);
new CarrierConfigXmlParser(xml, new CarrierConfigXmlParser.KeyValueProcessor() {
@Override
public void process(String type, String key, 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) {
Log.w(MmsService.TAG, "Load carrier value from resources: "
+ "invalid " + key + "," + value + "," + type);
}
}
}).parse();
} catch (final Resources.NotFoundException e) {
Log.w(MmsService.TAG, "Can not get mms_config.xml");
} finally {
if (xml != null) {
xml.close();
}
}
}
}
@@ -1,88 +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 android.support.v7.mms;
import android.content.Context;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
/**
* The default implementation of loader of UA and UAProfUrl
*/
class DefaultUserAgentInfoLoader implements UserAgentInfoLoader {
// Default values to be used as user agent info
private static final String DEFAULT_USER_AGENT = "Android MmsLib/1.0";
private static final String DEFAULT_UA_PROF_URL =
"http://www.gstatic.com/android/sms/mms_ua_profile.xml";
private Context mContext;
private boolean mLoaded;
private String mUserAgent;
private String mUAProfUrl;
DefaultUserAgentInfoLoader(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) {
Log.i(MmsService.TAG, "Loaded user agent info: "
+ "UA=" + mUserAgent + ", UAProfUrl=" + mUAProfUrl);
}
}
private void loadLocked() {
if (Utils.hasUserAgentApi()) {
// 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 (TextUtils.isEmpty(mUserAgent)) {
mUserAgent = DEFAULT_USER_AGENT;
}
if (TextUtils.isEmpty(mUAProfUrl)) {
mUAProfUrl = DEFAULT_UA_PROF_URL;
}
}
}
@@ -1,132 +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 android.support.v7.mms;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Request to download an MMS
*/
class DownloadRequest extends MmsRequest {
DownloadRequest(final String locationUrl, final Uri pduUri,
final PendingIntent sentIntent) {
super(locationUrl, pduUri, sentIntent);
}
@Override
protected boolean loadRequest(final Context context, final Bundle mmsConfig) {
// No need to load PDU from app. Always true.
return true;
}
@Override
protected boolean transferResponse(Context context, Intent fillIn, byte[] response) {
return writePduToContentUri(context, mPduUri, response);
}
@Override
protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettingsLoader.Apn apn,
Bundle mmsConfig, String userAgent, String uaProfUrl) throws MmsHttpException {
final MmsHttpClient httpClient = netMgr.getHttpClient();
return httpClient.execute(getHttpRequestUrl(apn), null/*pdu*/, MmsHttpClient.METHOD_GET,
!TextUtils.isEmpty(apn.getMmsProxy()), apn.getMmsProxy(), apn.getMmsProxyPort(),
mmsConfig, userAgent, uaProfUrl);
}
@Override
protected String getHttpRequestUrl(final ApnSettingsLoader.Apn apn) {
return mLocationUrl;
}
/**
* Write pdu bytes to content provider uri
*
* @param contentUri content provider uri to which bytes should be written
* @param pdu Bytes to write
* @return true if all bytes successfully written else false
*/
public boolean writePduToContentUri(final Context context, final Uri contentUri,
final byte[] pdu) {
if (contentUri == null || pdu == null) {
return false;
}
final Callable<Boolean> copyDownloadedPduToOutput = new Callable<Boolean>() {
public Boolean call() {
ParcelFileDescriptor.AutoCloseOutputStream outStream = null;
try {
final ContentResolver cr = context.getContentResolver();
final ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "w");
outStream = new ParcelFileDescriptor.AutoCloseOutputStream(pduFd);
outStream.write(pdu);
return true;
} catch (IOException e) {
Log.e(MmsService.TAG, "Writing PDU to downloader: IO exception", e);
return false;
} finally {
if (outStream != null) {
try {
outStream.close();
} catch (IOException ex) {
// Ignore
}
}
}
}
};
final Future<Boolean> pendingResult =
mPduTransferExecutor.submit(copyDownloadedPduToOutput);
try {
return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// Typically a timeout occurred - cancel task
pendingResult.cancel(true);
}
return false;
}
public static final Parcelable.Creator<DownloadRequest> CREATOR
= new Parcelable.Creator<DownloadRequest>() {
public DownloadRequest createFromParcel(Parcel in) {
return new DownloadRequest(in);
}
public DownloadRequest[] newArray(int size) {
return new DownloadRequest[size];
}
};
private DownloadRequest(Parcel in) {
super(in);
}
}
@@ -1,523 +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 android.support.v7.mms;
import android.content.Context;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* MMS HTTP client for sending and downloading MMS messages
*/
public class MmsHttpClient {
static final String METHOD_POST = "POST";
static final String METHOD_GET = "GET";
private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String HEADER_ACCEPT = "Accept";
private static final String HEADER_ACCEPT_LANGUAGE = "Accept-Language";
private static final String HEADER_USER_AGENT = "User-Agent";
// The "Accept" header value
private static final String HEADER_VALUE_ACCEPT =
"*/*, application/vnd.wap.mms-message, application/vnd.wap.sic";
// The "Content-Type" header value
private static final String HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET =
"application/vnd.wap.mms-message; charset=utf-8";
private static final String HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET =
"application/vnd.wap.mms-message";
/*
* Macro names
*/
// The raw phone number
private static final String MACRO_LINE1 = "LINE1";
// The phone number without country code
private static final String MACRO_LINE1NOCOUNTRYCODE = "LINE1NOCOUNTRYCODE";
// NAI (Network Access Identifier)
private static final String MACRO_NAI = "NAI";
// The possible NAI system property name
private static final String NAI_PROPERTY = "persist.radio.cdma.nai";
private final Context mContext;
private final TelephonyManager mTelephonyManager;
/**
* Constructor
*
* @param context The Context object
*/
MmsHttpClient(Context context) {
mContext = context;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
}
/**
* Execute an MMS HTTP request, either a POST (sending) or a GET (downloading)
*
* @param urlString The request URL, for sending it is usually the MMSC, and for downloading
* it is the message URL
* @param pdu For POST (sending) only, the PDU to send
* @param method HTTP method, POST for sending and GET for downloading
* @param isProxySet Is there a proxy for the MMSC
* @param proxyHost The proxy host
* @param proxyPort The proxy port
* @param mmsConfig The MMS config to use
* @param userAgent The user agent header value
* @param uaProfUrl The UA Prof URL header value
* @return The HTTP response body
* @throws MmsHttpException For any failures
*/
public byte[] execute(String urlString, byte[] pdu, String method, boolean isProxySet,
String proxyHost, int proxyPort, Bundle mmsConfig, String userAgent, String uaProfUrl)
throws MmsHttpException {
Log.d(MmsService.TAG, "HTTP: " + method + " " + Utils.redactUrlForNonVerbose(urlString)
+ (isProxySet ? (", proxy=" + proxyHost + ":" + proxyPort) : "")
+ ", PDU size=" + (pdu != null ? pdu.length : 0));
checkMethod(method);
HttpURLConnection connection = null;
try {
Proxy proxy = Proxy.NO_PROXY;
if (isProxySet) {
proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
}
final URL url = new URL(urlString);
// Now get the connection
connection = (HttpURLConnection) url.openConnection(proxy);
connection.setDoInput(true);
connection.setConnectTimeout(
mmsConfig.getInt(CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT,
CarrierConfigValuesLoader.CONFIG_HTTP_SOCKET_TIMEOUT_DEFAULT));
// ------- COMMON HEADERS ---------
// Header: Accept
connection.setRequestProperty(HEADER_ACCEPT, HEADER_VALUE_ACCEPT);
// Header: Accept-Language
connection.setRequestProperty(
HEADER_ACCEPT_LANGUAGE, getCurrentAcceptLanguage(Locale.getDefault()));
// Header: User-Agent
Log.i(MmsService.TAG, "HTTP: User-Agent=" + userAgent);
connection.setRequestProperty(HEADER_USER_AGENT, userAgent);
// Header: x-wap-profile
final String uaProfUrlTagName = mmsConfig.getString(
CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME,
CarrierConfigValuesLoader.CONFIG_UA_PROF_TAG_NAME_DEFAULT);
if (uaProfUrl != null) {
Log.i(MmsService.TAG, "HTTP: UaProfUrl=" + uaProfUrl);
connection.setRequestProperty(uaProfUrlTagName, uaProfUrl);
}
// Add extra headers specified by mms_config.xml's httpparams
addExtraHeaders(connection, mmsConfig);
// Different stuff for GET and POST
if (METHOD_POST.equals(method)) {
if (pdu == null || pdu.length < 1) {
Log.e(MmsService.TAG, "HTTP: empty pdu");
throw new MmsHttpException(0/*statusCode*/, "Sending empty PDU");
}
connection.setDoOutput(true);
connection.setRequestMethod(METHOD_POST);
if (mmsConfig.getBoolean(
CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER,
CarrierConfigValuesLoader.CONFIG_SUPPORT_HTTP_CHARSET_HEADER_DEFAULT)) {
connection.setRequestProperty(HEADER_CONTENT_TYPE,
HEADER_VALUE_CONTENT_TYPE_WITH_CHARSET);
} else {
connection.setRequestProperty(HEADER_CONTENT_TYPE,
HEADER_VALUE_CONTENT_TYPE_WITHOUT_CHARSET);
}
if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
logHttpHeaders(connection.getRequestProperties());
}
connection.setFixedLengthStreamingMode(pdu.length);
// Sending request body
final OutputStream out =
new BufferedOutputStream(connection.getOutputStream());
out.write(pdu);
out.flush();
out.close();
} else if (METHOD_GET.equals(method)) {
if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
logHttpHeaders(connection.getRequestProperties());
}
connection.setRequestMethod(METHOD_GET);
}
// Get response
final int responseCode = connection.getResponseCode();
final String responseMessage = connection.getResponseMessage();
Log.d(MmsService.TAG, "HTTP: " + responseCode + " " + responseMessage);
if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
logHttpHeaders(connection.getHeaderFields());
}
if (responseCode / 100 != 2) {
throw new MmsHttpException(responseCode, responseMessage);
}
final InputStream in = new BufferedInputStream(connection.getInputStream());
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final byte[] buf = new byte[4096];
int count = 0;
while ((count = in.read(buf)) > 0) {
byteOut.write(buf, 0, count);
}
in.close();
final byte[] responseBody = byteOut.toByteArray();
Log.d(MmsService.TAG, "HTTP: response size="
+ (responseBody != null ? responseBody.length : 0));
return responseBody;
} catch (MalformedURLException e) {
final String redactedUrl = Utils.redactUrlForNonVerbose(urlString);
Log.e(MmsService.TAG, "HTTP: invalid URL " + redactedUrl, e);
throw new MmsHttpException(0/*statusCode*/, "Invalid URL " + redactedUrl, e);
} catch (ProtocolException e) {
final String redactedUrl = Utils.redactUrlForNonVerbose(urlString);
Log.e(MmsService.TAG, "HTTP: invalid URL protocol " + redactedUrl, e);
throw new MmsHttpException(0/*statusCode*/, "Invalid URL protocol " + redactedUrl, e);
} catch (IOException e) {
Log.e(MmsService.TAG, "HTTP: IO failure", e);
throw new MmsHttpException(0/*statusCode*/, e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
private static void logHttpHeaders(Map<String, List<String>> headers) {
final StringBuilder sb = new StringBuilder();
if (headers != null) {
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
final String key = entry.getKey();
final List<String> values = entry.getValue();
if (values != null) {
for (String value : values) {
sb.append(key).append('=').append(value).append('\n');
}
}
}
Log.v(MmsService.TAG, "HTTP: headers\n" + sb.toString());
}
}
private static void checkMethod(String method) throws MmsHttpException {
if (!METHOD_GET.equals(method) && !METHOD_POST.equals(method)) {
throw new MmsHttpException(0/*statusCode*/, "Invalid method " + method);
}
}
private static final String ACCEPT_LANG_FOR_US_LOCALE = "en-US";
/**
* Return the Accept-Language header. Use the current locale plus
* US if we are in a different locale than US.
* This code copied from the browser's WebSettings.java
*
* @return Current AcceptLanguage String.
*/
public static String getCurrentAcceptLanguage(Locale locale) {
final StringBuilder buffer = new StringBuilder();
addLocaleToHttpAcceptLanguage(buffer, locale);
if (!Locale.US.equals(locale)) {
if (buffer.length() > 0) {
buffer.append(", ");
}
buffer.append(ACCEPT_LANG_FOR_US_LOCALE);
}
return buffer.toString();
}
/**
* Convert obsolete language codes, including Hebrew/Indonesian/Yiddish,
* to new standard.
*/
private static String convertObsoleteLanguageCodeToNew(String langCode) {
if (langCode == null) {
return null;
}
if ("iw".equals(langCode)) {
// Hebrew
return "he";
} else if ("in".equals(langCode)) {
// Indonesian
return "id";
} else if ("ji".equals(langCode)) {
// Yiddish
return "yi";
}
return langCode;
}
private static void addLocaleToHttpAcceptLanguage(StringBuilder builder, Locale locale) {
final String language = convertObsoleteLanguageCodeToNew(locale.getLanguage());
if (language != null) {
builder.append(language);
final String country = locale.getCountry();
if (country != null) {
builder.append("-");
builder.append(country);
}
}
}
private static final Pattern MACRO_P = Pattern.compile("##(\\S+)##");
/**
* Resolve the macro in HTTP param value text
* For example, "something##LINE1##something" is resolved to "something9139531419something"
*
* @param value The HTTP param value possibly containing macros
* @return The HTTP param with macro resolved to real value
*/
private String resolveMacro(String value, Bundle mmsConfig) {
if (TextUtils.isEmpty(value)) {
return value;
}
final Matcher matcher = MACRO_P.matcher(value);
int nextStart = 0;
StringBuilder replaced = null;
while (matcher.find()) {
if (replaced == null) {
replaced = new StringBuilder();
}
final int matchedStart = matcher.start();
if (matchedStart > nextStart) {
replaced.append(value.substring(nextStart, matchedStart));
}
final String macro = matcher.group(1);
final String macroValue = getHttpParamMacro(macro, mmsConfig);
if (macroValue != null) {
replaced.append(macroValue);
}
nextStart = matcher.end();
}
if (replaced != null && nextStart < value.length()) {
replaced.append(value.substring(nextStart));
}
return replaced == null ? value : replaced.toString();
}
/**
* Add extra HTTP headers from mms_config.xml's httpParams, which is a list of key/value
* pairs separated by "|". Each key/value pair is separated by ":". Value may contain
* macros like "##LINE1##" or "##NAI##" which is resolved with methods in this class
*
* @param connection The HttpURLConnection that we add headers to
* @param mmsConfig The MmsConfig object
*/
private void addExtraHeaders(HttpURLConnection connection, Bundle mmsConfig) {
final String extraHttpParams = mmsConfig.getString(
CarrierConfigValuesLoader.CONFIG_HTTP_PARAMS);
if (!TextUtils.isEmpty(extraHttpParams)) {
// Parse the parameter list
String paramList[] = extraHttpParams.split("\\|");
for (String paramPair : paramList) {
String splitPair[] = paramPair.split(":", 2);
if (splitPair.length == 2) {
final String name = splitPair[0].trim();
final String value = resolveMacro(splitPair[1].trim(), mmsConfig);
if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)) {
// Add the header if the param is valid
connection.setRequestProperty(name, value);
}
}
}
}
}
/**
* Return the HTTP param macro value.
* Example: LINE1 returns the phone number, etc.
*
* @param macro The macro name
* @param mmsConfig The carrier configuration values
* @return The value of the defined macro
*/
private String getHttpParamMacro(final String macro, final Bundle mmsConfig) {
if (MACRO_LINE1.equals(macro)) {
return getSelfNumber();
} else if (MACRO_LINE1NOCOUNTRYCODE.equals(macro)) {
return PhoneNumberHelper.getNumberNoCountryCode(
getSelfNumber(), getSimOrLocaleCountry());
} else if (MACRO_NAI.equals(macro)) {
return getEncodedNai(mmsConfig.getString(
CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX,
CarrierConfigValuesLoader.CONFIG_NAI_SUFFIX_DEFAULT));
}
return null;
}
/**
* Get the device phone number
*
* @return the phone number text
*/
private String getSelfNumber() {
if (Utils.supportMSim()) {
final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext);
final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(
SmsManager.getDefaultSmsSubscriptionId());
if (info != null) {
return info.getNumber();
} else {
return null;
}
} else {
return mTelephonyManager.getLine1Number();
}
}
/**
* Get the country ISO code from SIM or system locale
*
* @return the country ISO code
*/
private String getSimOrLocaleCountry() {
String country = null;
if (Utils.supportMSim()) {
final SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext);
final SubscriptionInfo info = subscriptionManager.getActiveSubscriptionInfo(
SmsManager.getDefaultSmsSubscriptionId());
if (info != null) {
country = info.getCountryIso();
}
} else {
country = mTelephonyManager.getSimCountryIso();
}
if (!TextUtils.isEmpty(country)) {
return country.toUpperCase();
} else {
return Locale.getDefault().getCountry();
}
}
/**
* Get encoded NAI string to use as the HTTP header for some carriers.
* On L-MR1+, we call the hidden system API to get this
* On L-MR1-, we try to find it via system property.
*
* @param naiSuffix the suffix to append to NAI before encoding
* @return the Base64 encoded NAI string to use as HTTP header
*/
private String getEncodedNai(final String naiSuffix) {
String nai;
if (Utils.supportMSim()) {
nai = getNaiBySystemApi(
getSlotId(Utils.getEffectiveSubscriptionId(MmsManager.DEFAULT_SUB_ID)));
} else {
nai = getNaiBySystemProperty();
}
if (!TextUtils.isEmpty(nai)) {
Log.i(MmsService.TAG, "NAI is not empty");
if (!TextUtils.isEmpty(naiSuffix)) {
nai = nai + naiSuffix;
}
byte[] encoded = null;
try {
encoded = Base64.encode(nai.getBytes("UTF-8"), Base64.NO_WRAP);
} catch (UnsupportedEncodingException e) {
encoded = Base64.encode(nai.getBytes(), Base64.NO_WRAP);
}
try {
return new String(encoded, "UTF-8");
} catch (UnsupportedEncodingException e) {
return new String(encoded);
}
}
return null;
}
/**
* Invoke hidden SubscriptionManager.getSlotId(int)
*
* @param subId the subId
* @return the SIM slot ID
*/
private static int getSlotId(final int subId) {
try {
final Method method = SubscriptionManager.class.getMethod("getSlotId", Integer.TYPE);
if (method != null) {
return (Integer) method.invoke(null, subId);
}
} catch (Exception e) {
Log.w(MmsService.TAG, "SubscriptionManager.getSlotId failed " + e);
}
return -1;
}
/**
* Get NAI using hidden TelephonyManager.getNai(int)
*
* @param slotId the SIM slot ID
* @return the NAI string
*/
private String getNaiBySystemApi(final int slotId) {
try {
final Method method = mTelephonyManager.getClass().getMethod("getNai", Integer.TYPE);
if (method != null) {
return (String) method.invoke(mTelephonyManager, slotId);
}
} catch (Exception e) {
Log.w(MmsService.TAG, "TelephonyManager.getNai failed " + e);
}
return null;
}
/**
* Get NAI using hidden SystemProperties.get(String)
*
* @return the NAI string as system property
*/
private static String getNaiBySystemProperty() {
try {
final Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
if (systemPropertiesClass != null) {
final Method method = systemPropertiesClass.getMethod("get", String.class);
if (method != null) {
return (String) method.invoke(null, NAI_PROPERTY);
}
}
} catch (Exception e) {
Log.w(MmsService.TAG, "SystemProperties.get failed " + e);
}
return null;
}
}
@@ -1,50 +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 android.support.v7.mms;
/**
* HTTP exception
*/
public class MmsHttpException extends Exception {
// Optional HTTP status code. 0 means ignore. Otherwise this
// should be a valid HTTP status code.
private final int mStatusCode;
public MmsHttpException(int statusCode) {
super();
mStatusCode = statusCode;
}
public MmsHttpException(int statusCode, String message) {
super(message);
mStatusCode = statusCode;
}
public MmsHttpException(int statusCode, Throwable cause) {
super(cause);
mStatusCode = statusCode;
}
public MmsHttpException(int statusCode, String message, Throwable cause) {
super(message, cause);
mStatusCode = statusCode;
}
public int getStatusCode() {
return mStatusCode;
}
}
-256
View File
@@ -1,256 +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 android.support.v7.mms;
import android.app.PendingIntent;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.util.SparseArray;
/**
* The public interface of MMS library
*/
public class MmsManager {
/**
* Default subscription ID
*/
public static final int DEFAULT_SUB_ID = -1;
// Whether to force legacy MMS sending
private static volatile boolean sForceLegacyMms = false;
// Cached computed overrides for carrier configuration values
private static SparseArray<Bundle> sConfigOverridesMap = new SparseArray<>();
/**
* Set the flag about whether to force to use legacy system APIs instead of system MMS API
*
* @param forceLegacyMms value to set
*/
public static void setForceLegacyMms(boolean forceLegacyMms) {
sForceLegacyMms = forceLegacyMms;
}
/**
* Set the size of thread pool for request execution.
*
* Default is 4
*
* Note: if system MMS API is used, this has no effect
*
* @param size thread pool size
*/
public static void setThreadPoolSize(int size) {
MmsService.setThreadPoolSize(size);
}
/**
* Set whether to use wake lock while sending or downloading MMS.
*
* Default value is true
*
* Note: if system MMS API is used, this has no effect
*
* @param useWakeLock true to use wake lock, false otherwise
*/
public static void setUseWakeLock(final boolean useWakeLock) {
MmsService.setUseWakeLock(useWakeLock);
}
/**
* Set the optional carrier config values loader
*
* Note: if system MMS API is used, this is used to compute the overrides
* of carrier configuration values
*
* @param loader the carrier config values loader
*/
public static void setCarrierConfigValuesLoader(CarrierConfigValuesLoader loader) {
if (loader == null) {
throw new IllegalArgumentException("Carrier configuration loader can not be empty");
}
synchronized (sConfigOverridesMap) {
MmsService.setCarrierConfigValuesLoader(loader);
sConfigOverridesMap.clear();
}
}
/**
* Set the optional APN settings loader
*
* Note: if system MMS API is used, this has no effect
*
* @param loader the APN settings loader
*/
public static void setApnSettingsLoader(ApnSettingsLoader loader) {
if (loader == null) {
throw new IllegalArgumentException("APN settings loader can not be empty");
}
MmsService.setApnSettingsLoader(loader);
}
/**
* Set user agent info loader
*
* Note: if system MMS API is used, this is used to compute the overrides
* of carrier configuration values
* @param loader the user agent info loader
*/
public static void setUserAgentInfoLoader(final UserAgentInfoLoader loader) {
if (loader == null) {
throw new IllegalArgumentException("User agent info loader can not be empty");
}
synchronized (sConfigOverridesMap) {
MmsService.setUserAgentInfoLoader(loader);
sConfigOverridesMap.clear();
}
}
/**
* Send MMS via platform MMS API (if platform supports and not forced to
* use legacy APIs) or legacy APIs
*
* @param subId the subscription ID of the SIM to use
* @param context the Context to use
* @param contentUri the content URI of the PDU to be sent
* @param locationUrl the optional location URL to use for sending
* @param sentIntent the pending intent for returning results
*/
public static void sendMultimediaMessage(int subId, Context context, Uri contentUri,
String locationUrl, PendingIntent sentIntent) {
if (Utils.hasMmsApi() && !sForceLegacyMms) {
subId = Utils.getEffectiveSubscriptionId(subId);
final SmsManager smsManager = Utils.getSmsManager(subId);
smsManager.sendMultimediaMessage(context, contentUri, locationUrl,
getConfigOverrides(subId), sentIntent);
} else {
MmsService.startRequest(context, new SendRequest(locationUrl, contentUri, sentIntent));
}
}
/**
* Download MMS via platform MMS API (if platform supports and not forced to
* use legacy APIs) or legacy APIs
*
* @param subId the subscription ID of the SIM to use
* @param context the Context to use
* @param contentUri the content URI of the PDU to be sent
* @param locationUrl the optional location URL to use for sending
* @param downloadedIntent the pending intent for returning results
*/
public static void downloadMultimediaMessage(int subId, Context context, String locationUrl,
Uri contentUri, PendingIntent downloadedIntent) {
if (Utils.hasMmsApi() && !sForceLegacyMms) {
subId = Utils.getEffectiveSubscriptionId(subId);
final SmsManager smsManager = Utils.getSmsManager(subId);
smsManager.downloadMultimediaMessage(context, locationUrl, contentUri,
getConfigOverrides(subId), downloadedIntent);
} else {
MmsService.startRequest(context,
new DownloadRequest(locationUrl, contentUri, downloadedIntent));
}
}
/**
* Get carrier configuration values overrides when platform MMS API is called.
* We only need to compute this if customized carrier config values loader or
* user agent info loader are set
*
* @param subId the ID of the SIM to use
* @return a Bundle containing the overrides
*/
private static Bundle getConfigOverrides(final int subId) {
if (!Utils.hasMmsApi()) {
// If MMS API is not present, it is not necessary to compute overrides
return null;
}
Bundle overrides = null;
synchronized (sConfigOverridesMap) {
overrides = sConfigOverridesMap.get(subId);
if (overrides == null) {
overrides = new Bundle();
sConfigOverridesMap.put(subId, overrides);
computeOverridesLocked(subId, overrides);
}
}
return overrides;
}
/**
* Compute the overrides, incorporating the user agent info
*
* @param subId the subId of the SIM to use
* @param overrides the computed values overrides
*/
private static void computeOverridesLocked(final int subId, final Bundle overrides) {
// Overrides not computed yet
final CarrierConfigValuesLoader carrierConfigValuesLoader =
MmsService.getCarrierConfigValuesLoader();
if (carrierConfigValuesLoader != null &&
!(carrierConfigValuesLoader instanceof DefaultCarrierConfigValuesLoader)) {
// Compute the overrides for carrier config values first if the config loader
// is not the default one.
final Bundle systemValues = Utils.getSmsManager(subId).getCarrierConfigValues();
final Bundle callerValues =
MmsService.getCarrierConfigValuesLoader().get(subId);
if (systemValues != null && callerValues != null) {
computeConfigDelta(systemValues, callerValues, overrides);
} else if (systemValues == null && callerValues != null) {
overrides.putAll(callerValues);
}
}
final UserAgentInfoLoader userAgentInfoLoader = MmsService.getUserAgentInfoLoader();
if (userAgentInfoLoader != null &&
!(userAgentInfoLoader instanceof DefaultUserAgentInfoLoader)) {
// Also set the user agent and ua prof url via the overrides
// if the user agent loader is not the default one.
overrides.putString(UserAgentInfoLoader.CONFIG_USER_AGENT,
userAgentInfoLoader.getUserAgent());
overrides.putString(UserAgentInfoLoader.CONFIG_UA_PROF_URL,
userAgentInfoLoader.getUAProfUrl());
}
}
/**
* Compute the delta between two sets of carrier configuration values: system and caller
*
* @param systemValues the system config values
* @param callerValues the caller's config values
* @param delta the delta of values (caller - system), using caller value to override system's
*/
private static void computeConfigDelta(final Bundle systemValues, final Bundle callerValues,
final Bundle delta) {
for (final String key : callerValues.keySet()) {
final Object callerValue = callerValues.get(key);
final Object systemValue = systemValues.get(key);
if ((callerValue != null && systemValue != null && !callerValue.equals(systemValue)) ||
(callerValue != null && systemValue == null) ||
(callerValue == null && systemValue != null)) {
if (callerValue == null || callerValue instanceof String) {
delta.putString(key, (String) callerValue);
} else if (callerValue instanceof Integer) {
delta.putInt(key, (Integer) callerValue);
} else if (callerValue instanceof Boolean) {
delta.putBoolean(key, (Boolean) callerValue);
}
}
}
}
}
@@ -1,39 +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 android.support.v7.mms;
/**
* MMS network exception
*/
class MmsNetworkException extends Exception {
public MmsNetworkException() {
super();
}
public MmsNetworkException(String message) {
super(message);
}
public MmsNetworkException(Throwable cause) {
super(cause);
}
public MmsNetworkException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,382 +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 android.support.v7.mms;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import java.lang.reflect.Method;
import java.util.Timer;
import java.util.TimerTask;
/**
* Class manages MMS network connectivity using legacy platform APIs
* (deprecated since Android L) on pre-L devices (or when forced to
* be used on L and later)
*/
class MmsNetworkManager {
// Hidden platform constants
private static final String FEATURE_ENABLE_MMS = "enableMMS";
private static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded";
private static final int APN_ALREADY_ACTIVE = 0;
private static final int APN_REQUEST_STARTED = 1;
private static final int APN_TYPE_NOT_AVAILABLE = 2;
private static final int APN_REQUEST_FAILED = 3;
private static final int APN_ALREADY_INACTIVE = 4;
// A map from platform APN constant to text string
private static final String[] APN_RESULT_STRING = new String[]{
"already active",
"request started",
"type not available",
"request failed",
"already inactive",
"unknown",
};
private static final long NETWORK_ACQUIRE_WAIT_INTERVAL_MS = 15000;
private static final long DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS = 180000;
private static final String MMS_NETWORK_EXTENSION_TIMER = "mms_network_extension_timer";
private static final long MMS_NETWORK_EXTENSION_TIMER_WAIT_MS = 30000;
private static volatile long sNetworkAcquireTimeoutMs = DEFAULT_NETWORK_ACQUIRE_TIMEOUT_MS;
/**
* Set the network acquire timeout
*
* @param timeoutMs timeout in millisecond
*/
static void setNetworkAcquireTimeout(final long timeoutMs) {
sNetworkAcquireTimeoutMs = timeoutMs;
}
private final Context mContext;
private final ConnectivityManager mConnectivityManager;
// If the connectivity intent receiver is registered
private boolean mReceiverRegistered;
// Count of requests that are using the MMS network
private int mUseCount;
// Count of requests that are waiting for connectivity (i.e. in acquireNetwork wait loop)
private int mWaitCount;
// Timer to extend the network connectivity
private Timer mExtensionTimer;
private final MmsHttpClient mHttpClient;
private final IntentFilter mConnectivityIntentFilter;
private final BroadcastReceiver mConnectivityChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
return;
}
final int networkType = getConnectivityChangeNetworkType(intent);
if (networkType != ConnectivityManager.TYPE_MOBILE_MMS) {
return;
}
onMmsConnectivityChange(context, intent);
}
};
MmsNetworkManager(final Context context) {
mContext = context;
mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
Context.CONNECTIVITY_SERVICE);
mHttpClient = new MmsHttpClient(mContext);
mConnectivityIntentFilter = new IntentFilter();
mConnectivityIntentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
mUseCount = 0;
mWaitCount = 0;
}
ConnectivityManager getConnectivityManager() {
return mConnectivityManager;
}
MmsHttpClient getHttpClient() {
return mHttpClient;
}
/**
* Synchronously acquire MMS network connectivity
*
* @throws MmsNetworkException If failed permanently or timed out
*/
void acquireNetwork() throws MmsNetworkException {
Log.i(MmsService.TAG, "Acquire MMS network");
synchronized (this) {
try {
mUseCount++;
mWaitCount++;
if (mWaitCount == 1) {
// Register the receiver for the first waiting request
registerConnectivityChangeReceiverLocked();
}
long waitMs = sNetworkAcquireTimeoutMs;
final long beginMs = SystemClock.elapsedRealtime();
do {
if (!isMobileDataEnabled()) {
// Fast fail if mobile data is not enabled
throw new MmsNetworkException("Mobile data is disabled");
}
// Always try to extend and check the MMS network connectivity
// before we start waiting to make sure we don't miss the change
// of MMS connectivity. As one example, some devices fail to send
// connectivity change intent. So this would make sure we catch
// the state change.
if (extendMmsConnectivityLocked()) {
// Connected
return;
}
try {
wait(Math.min(waitMs, NETWORK_ACQUIRE_WAIT_INTERVAL_MS));
} catch (final InterruptedException e) {
Log.w(MmsService.TAG, "Unexpected exception", e);
}
// Calculate the remaining time to wait
waitMs = sNetworkAcquireTimeoutMs - (SystemClock.elapsedRealtime() - beginMs);
} while (waitMs > 0);
// Last check
if (extendMmsConnectivityLocked()) {
return;
} else {
// Reaching here means timed out.
throw new MmsNetworkException("Acquiring MMS network timed out");
}
} finally {
mWaitCount--;
if (mWaitCount == 0) {
// Receiver is used to listen to connectivity change and unblock
// the waiting requests. If nobody's waiting on change, there is
// no need for the receiver. The auto extension timer will try
// to maintain the connectivity periodically.
unregisterConnectivityChangeReceiverLocked();
}
}
}
}
/**
* Release MMS network connectivity. This is ref counted. So it only disconnect
* when the ref count is 0.
*/
void releaseNetwork() {
Log.i(MmsService.TAG, "release MMS network");
synchronized (this) {
mUseCount--;
if (mUseCount == 0) {
stopNetworkExtensionTimerLocked();
endMmsConnectivity();
}
}
}
String getApnName() {
String apnName = null;
final NetworkInfo mmsNetworkInfo = mConnectivityManager.getNetworkInfo(
ConnectivityManager.TYPE_MOBILE_MMS);
if (mmsNetworkInfo != null) {
apnName = mmsNetworkInfo.getExtraInfo();
}
return apnName;
}
// Process mobile MMS connectivity change, waking up the waiting request thread
// in certain conditions:
// - Successfully connected
// - Failed permanently
// - Required another kickoff
// We don't initiate connection here but just notifyAll so the waiting request
// would wake up and retry connection before next wait.
private void onMmsConnectivityChange(final Context context, final Intent intent) {
if (mUseCount < 1) {
return;
}
final NetworkInfo mmsNetworkInfo =
mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_MMS);
// Check availability of the mobile network.
if (mmsNetworkInfo != null) {
if (REASON_VOICE_CALL_ENDED.equals(mmsNetworkInfo.getReason())) {
// This is a very specific fix to handle the case where the phone receives an
// incoming call during the time we're trying to setup the mms connection.
// When the call ends, restart the process of mms connectivity.
// Once the waiting request is unblocked, before the next wait, we would start
// MMS network again.
unblockWait();
} else {
final NetworkInfo.State state = mmsNetworkInfo.getState();
if (state == NetworkInfo.State.CONNECTED ||
(state == NetworkInfo.State.DISCONNECTED && !isMobileDataEnabled())) {
// Unblock the waiting request when we either connected
// OR
// disconnected due to mobile data disabled therefore needs to fast fail
// (on some devices if mobile data disabled and starting MMS would cause
// an immediate state change to disconnected, so causing a tight loop of
// trying and failing)
// Once the waiting request is unblocked, before the next wait, we would
// check mobile data and start MMS network again. So we should catch
// both the success and the fast failure.
unblockWait();
}
}
}
}
private void unblockWait() {
synchronized (this) {
notifyAll();
}
}
private void startNetworkExtensionTimerLocked() {
if (mExtensionTimer == null) {
mExtensionTimer = new Timer(MMS_NETWORK_EXTENSION_TIMER, true/*daemon*/);
mExtensionTimer.schedule(
new TimerTask() {
@Override
public void run() {
synchronized (this) {
if (mUseCount > 0) {
try {
// Try extending the connectivity
extendMmsConnectivityLocked();
} catch (final MmsNetworkException e) {
// Ignore the exception
}
}
}
}
},
MMS_NETWORK_EXTENSION_TIMER_WAIT_MS);
}
}
private void stopNetworkExtensionTimerLocked() {
if (mExtensionTimer != null) {
mExtensionTimer.cancel();
mExtensionTimer = null;
}
}
private boolean extendMmsConnectivityLocked() throws MmsNetworkException {
final int result = startMmsConnectivity();
if (result == APN_ALREADY_ACTIVE) {
// Already active
startNetworkExtensionTimerLocked();
return true;
} else if (result != APN_REQUEST_STARTED) {
stopNetworkExtensionTimerLocked();
throw new MmsNetworkException("Cannot acquire MMS network: " +
result + " - " + getMmsConnectivityResultString(result));
}
return false;
}
private int startMmsConnectivity() {
Log.i(MmsService.TAG, "Start MMS connectivity");
try {
final Method method = mConnectivityManager.getClass().getMethod(
"startUsingNetworkFeature", Integer.TYPE, String.class);
if (method != null) {
return (Integer) method.invoke(
mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
}
} catch (final Exception e) {
Log.w(MmsService.TAG, "ConnectivityManager.startUsingNetworkFeature failed " + e);
}
return APN_REQUEST_FAILED;
}
private void endMmsConnectivity() {
Log.i(MmsService.TAG, "End MMS connectivity");
try {
final Method method = mConnectivityManager.getClass().getMethod(
"stopUsingNetworkFeature", Integer.TYPE, String.class);
if (method != null) {
method.invoke(
mConnectivityManager, ConnectivityManager.TYPE_MOBILE, FEATURE_ENABLE_MMS);
}
} catch (final Exception e) {
Log.w(MmsService.TAG, "ConnectivityManager.stopUsingNetworkFeature failed " + e);
}
}
private void registerConnectivityChangeReceiverLocked() {
if (!mReceiverRegistered) {
mContext.registerReceiver(mConnectivityChangeReceiver, mConnectivityIntentFilter);
mReceiverRegistered = true;
}
}
private void unregisterConnectivityChangeReceiverLocked() {
if (mReceiverRegistered) {
mContext.unregisterReceiver(mConnectivityChangeReceiver);
mReceiverRegistered = false;
}
}
/**
* The absence of a connection type.
*/
private static final int TYPE_NONE = -1;
/**
* Get the network type of the connectivity change
*
* @param intent the broadcast intent of connectivity change
* @return The change's network type
*/
private static int getConnectivityChangeNetworkType(final Intent intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
return intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, TYPE_NONE);
} else {
final NetworkInfo info = intent.getParcelableExtra(
ConnectivityManager.EXTRA_NETWORK_INFO);
if (info != null) {
return info.getType();
}
}
return TYPE_NONE;
}
private static String getMmsConnectivityResultString(int result) {
if (result < 0 || result >= APN_RESULT_STRING.length) {
result = APN_RESULT_STRING.length - 1;
}
return APN_RESULT_STRING[result];
}
private boolean isMobileDataEnabled() {
try {
final Class cmClass = mConnectivityManager.getClass();
final Method method = cmClass.getDeclaredMethod("getMobileDataEnabled");
method.setAccessible(true); // Make the method callable
// get the setting for "mobile data"
return (Boolean) method.invoke(mConnectivityManager);
} catch (final Exception e) {
Log.w(MmsService.TAG, "TelephonyManager.getMobileDataEnabled failed", e);
}
return false;
}
}
-391
View File
@@ -1,391 +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 android.support.v7.mms;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v7.mms.pdu.GenericPdu;
import android.support.v7.mms.pdu.PduHeaders;
import android.support.v7.mms.pdu.PduParser;
import android.support.v7.mms.pdu.SendConf;
import android.telephony.SmsManager;
import android.text.TextUtils;
import android.util.Log;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* MMS request base class. This handles the execution of any MMS request.
*/
abstract class MmsRequest implements Parcelable {
/**
* Prepare to make the HTTP request - will download message for sending
*
* @param context the Context
* @param mmsConfig carrier config values to use
* @return true if loading request PDU from calling app succeeds, false otherwise
*/
protected abstract boolean loadRequest(Context context, Bundle mmsConfig);
/**
* Transfer the received response to the caller
*
* @param context the Context
* @param fillIn the content of pending intent to be returned
* @param response the pdu to transfer
* @return true if transferring response PDU to calling app succeeds, false otherwise
*/
protected abstract boolean transferResponse(Context context, Intent fillIn, byte[] response);
/**
* Making the HTTP request to MMSC
*
* @param context The context
* @param netMgr The current {@link MmsNetworkManager}
* @param apn The APN
* @param mmsConfig The carrier configuration values to use
* @param userAgent The User-Agent header value
* @param uaProfUrl The UA Prof URL header value
* @return The HTTP response data
* @throws MmsHttpException If any network error happens
*/
protected abstract byte[] doHttp(Context context, MmsNetworkManager netMgr,
ApnSettingsLoader.Apn apn, Bundle mmsConfig, String userAgent, String uaProfUrl)
throws MmsHttpException;
/**
* Get the HTTP request URL for this MMS request
*
* @param apn The APN to use
* @return The HTTP request URL in text
*/
protected abstract String getHttpRequestUrl(ApnSettingsLoader.Apn apn);
// Maximum time to spend waiting to read data from a content provider before failing with error.
protected static final int TASK_TIMEOUT_MS = 30 * 1000;
protected final String mLocationUrl;
protected final Uri mPduUri;
protected final PendingIntent mPendingIntent;
// Thread pool for transferring PDU with MMS apps
protected final ExecutorService mPduTransferExecutor = Executors.newCachedThreadPool();
// Whether this request should acquire wake lock
private boolean mUseWakeLock;
protected MmsRequest(final String locationUrl, final Uri pduUri,
final PendingIntent pendingIntent) {
mLocationUrl = locationUrl;
mPduUri = pduUri;
mPendingIntent = pendingIntent;
mUseWakeLock = true;
}
void setUseWakeLock(final boolean useWakeLock) {
mUseWakeLock = useWakeLock;
}
boolean getUseWakeLock() {
return mUseWakeLock;
}
/**
* Run the MMS request.
*
* @param context the context to use
* @param networkManager the MmsNetworkManager to use to setup MMS network
* @param apnSettingsLoader the APN loader
* @param carrierConfigValuesLoader the carrier config loader
* @param userAgentInfoLoader the user agent info loader
*/
void execute(final Context context, final MmsNetworkManager networkManager,
final ApnSettingsLoader apnSettingsLoader,
final CarrierConfigValuesLoader carrierConfigValuesLoader,
final UserAgentInfoLoader userAgentInfoLoader) {
Log.i(MmsService.TAG, "Execute " + this.getClass().getSimpleName());
int result = SmsManager.MMS_ERROR_UNSPECIFIED;
int httpStatusCode = 0;
byte[] response = null;
final Bundle mmsConfig = carrierConfigValuesLoader.get(MmsManager.DEFAULT_SUB_ID);
if (mmsConfig == null) {
Log.e(MmsService.TAG, "Failed to load carrier configuration values");
result = SmsManager.MMS_ERROR_CONFIGURATION_ERROR;
} else if (!loadRequest(context, mmsConfig)) {
Log.e(MmsService.TAG, "Failed to load PDU");
result = SmsManager.MMS_ERROR_IO_ERROR;
} else {
// Everything's OK. Now execute the request.
try {
// Acquire the MMS network
networkManager.acquireNetwork();
// Load the potential APNs. In most cases there should be only one APN available.
// On some devices on which we can't obtain APN from system, we look up our own
// APN list. Since we don't have exact information, we may get a list of potential
// APNs to try. Whenever we found a successful APN, we signal it and return.
final String apnName = networkManager.getApnName();
final List<ApnSettingsLoader.Apn> apns = apnSettingsLoader.get(apnName);
if (apns.size() < 1) {
throw new ApnException("No valid APN");
} else {
Log.d(MmsService.TAG, "Trying " + apns.size() + " APNs");
}
final String userAgent = userAgentInfoLoader.getUserAgent();
final String uaProfUrl = userAgentInfoLoader.getUAProfUrl();
MmsHttpException lastException = null;
for (ApnSettingsLoader.Apn apn : apns) {
Log.i(MmsService.TAG, "Using APN ["
+ "MMSC=" + apn.getMmsc() + ", "
+ "PROXY=" + apn.getMmsProxy() + ", "
+ "PORT=" + apn.getMmsProxyPort() + "]");
try {
final String url = getHttpRequestUrl(apn);
// Request a global route for the host to connect
requestRoute(networkManager.getConnectivityManager(), apn, url);
// Perform the HTTP request
response = doHttp(
context, networkManager, apn, mmsConfig, userAgent, uaProfUrl);
// Additional check of whether this is a success
if (isWrongApnResponse(response, mmsConfig)) {
throw new MmsHttpException(0/*statusCode*/, "Invalid sending address");
}
// Notify APN loader this is a valid APN
apn.setSuccess();
result = Activity.RESULT_OK;
break;
} catch (MmsHttpException e) {
Log.w(MmsService.TAG, "HTTP or network failure", e);
lastException = e;
}
}
if (lastException != null) {
throw lastException;
}
} catch (ApnException e) {
Log.e(MmsService.TAG, "MmsRequest: APN failure", e);
result = SmsManager.MMS_ERROR_INVALID_APN;
} catch (MmsNetworkException e) {
Log.e(MmsService.TAG, "MmsRequest: MMS network acquiring failure", e);
result = SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS;
} catch (MmsHttpException e) {
Log.e(MmsService.TAG, "MmsRequest: HTTP or network I/O failure", e);
result = SmsManager.MMS_ERROR_HTTP_FAILURE;
httpStatusCode = e.getStatusCode();
} catch (Exception e) {
Log.e(MmsService.TAG, "MmsRequest: unexpected failure", e);
result = SmsManager.MMS_ERROR_UNSPECIFIED;
} finally {
// Release MMS network
networkManager.releaseNetwork();
}
}
// Process result and send back via PendingIntent
returnResult(context, result, response, httpStatusCode);
}
/**
* Check if the response indicates a failure when we send to wrong APN.
* Sometimes even if you send to the wrong APN, a response in valid PDU format can still
* be sent back but with an error status. Check one specific case here.
*
* TODO: maybe there are other possibilities.
*
* @param response the response data
* @param mmsConfig the carrier configuration values to use
* @return false if we find an invalid response case, otherwise true
*/
static boolean isWrongApnResponse(final byte[] response, final Bundle mmsConfig) {
if (response != null && response.length > 0) {
try {
final GenericPdu pdu = new PduParser(
response,
mmsConfig.getBoolean(
CarrierConfigValuesLoader
.CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION,
CarrierConfigValuesLoader
.CONFIG_SUPPORT_MMS_CONTENT_DISPOSITION_DEFAULT))
.parse();
if (pdu != null && pdu instanceof SendConf) {
final SendConf sendConf = (SendConf) pdu;
final int responseStatus = sendConf.getResponseStatus();
return responseStatus ==
PduHeaders.RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED ||
responseStatus ==
PduHeaders.RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED;
}
} catch (RuntimeException e) {
Log.w(MmsService.TAG, "Parsing response failed", e);
}
}
return false;
}
/**
* Return the result back via pending intent
*
* @param context The context
* @param result The result code of execution
* @param response The response body
* @param httpStatusCode The optional http status code in case of http failure
*/
void returnResult(final Context context, int result, final byte[] response,
final int httpStatusCode) {
if (mPendingIntent == null) {
// Result not needed
return;
}
// Extra information to send back with the pending intent
final Intent fillIn = new Intent();
if (response != null) {
if (!transferResponse(context, fillIn, response)) {
// Failed to send PDU data back to caller
result = SmsManager.MMS_ERROR_IO_ERROR;
}
}
if (result == SmsManager.MMS_ERROR_HTTP_FAILURE && httpStatusCode != 0) {
// For HTTP failure, fill in the status code for more information
fillIn.putExtra(SmsManager.EXTRA_MMS_HTTP_STATUS, httpStatusCode);
}
try {
mPendingIntent.send(context, result, fillIn);
} catch (PendingIntent.CanceledException e) {
Log.e(MmsService.TAG, "Sending pending intent canceled", e);
}
}
/**
* Request the route to the APN (either proxy host or the MMSC host)
*
* @param connectivityManager the ConnectivityManager to use
* @param apn the current APN
* @param url the URL to connect to
* @throws MmsHttpException for unknown host or route failure
*/
private static void requestRoute(final ConnectivityManager connectivityManager,
final ApnSettingsLoader.Apn apn, final String url) throws MmsHttpException {
String host = apn.getMmsProxy();
if (TextUtils.isEmpty(host)) {
final Uri uri = Uri.parse(url);
host = uri.getHost();
}
boolean success = false;
// Request route to all resolved host addresses
try {
for (final InetAddress addr : InetAddress.getAllByName(host)) {
final boolean requested = requestRouteToHostAddress(connectivityManager, addr);
if (requested) {
success = true;
Log.i(MmsService.TAG, "Requested route to " + addr);
} else {
Log.i(MmsService.TAG, "Could not requested route to " + addr);
}
}
if (!success) {
throw new MmsHttpException(0/*statusCode*/, "No route requested");
}
} catch (UnknownHostException e) {
Log.w(MmsService.TAG, "Unknown host " + host);
throw new MmsHttpException(0/*statusCode*/, "Unknown host");
}
}
private static final Integer TYPE_MOBILE_MMS =
Integer.valueOf(ConnectivityManager.TYPE_MOBILE_MMS);
/**
* Wrapper for platform API requestRouteToHostAddress
*
* We first try the hidden but correct method on ConnectivityManager. If we can't, use
* the old but buggy one
*
* @param connMgr the ConnectivityManager instance
* @param inetAddr the InetAddress to request
* @return true if route is successfully setup, false otherwise
*/
private static boolean requestRouteToHostAddress(final ConnectivityManager connMgr,
final InetAddress inetAddr) {
// First try the good method using reflection
try {
final Method method = connMgr.getClass().getMethod("requestRouteToHostAddress",
Integer.TYPE, InetAddress.class);
if (method != null) {
return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS, inetAddr);
}
} catch (Exception e) {
Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHostAddress failed " + e);
}
// If we fail, try the old but buggy one
if (inetAddr instanceof Inet4Address) {
try {
final Method method = connMgr.getClass().getMethod("requestRouteToHost",
Integer.TYPE, Integer.TYPE);
if (method != null) {
return (Boolean) method.invoke(connMgr, TYPE_MOBILE_MMS,
inetAddressToInt(inetAddr));
}
} catch (Exception e) {
Log.w(MmsService.TAG, "ConnectivityManager.requestRouteToHost failed " + e);
}
}
return false;
}
/**
* Convert a IPv4 address from an InetAddress to an integer
*
* @param inetAddr is an InetAddress corresponding to the IPv4 address
* @return the IP address as an integer in network byte order
*/
private static int inetAddressToInt(final InetAddress inetAddr)
throws IllegalArgumentException {
final byte [] addr = inetAddr.getAddress();
return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) |
((addr[1] & 0xff) << 8) | (addr[0] & 0xff);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeByte((byte) (mUseWakeLock ? 1 : 0));
parcel.writeString(mLocationUrl);
parcel.writeParcelable(mPduUri, 0);
parcel.writeParcelable(mPendingIntent, 0);
}
protected MmsRequest(final Parcel in) {
final ClassLoader classLoader = MmsRequest.class.getClassLoader();
mUseWakeLock = in.readByte() != 0;
mLocationUrl = in.readString();
mPduUri = in.readParcelable(classLoader);
mPendingIntent = in.readParcelable(classLoader);
}
}
-465
View File
@@ -1,465 +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 android.support.v7.mms;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.Process;
import android.telephony.SmsManager;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
/**
* Service to execute MMS requests using deprecated legacy APIs on older platform (prior to L)
*/
public class MmsService extends Service {
static final String TAG = "MmsLib";
//The default number of threads allowed to run MMS requests
private static final int DEFAULT_THREAD_POOL_SIZE = 4;
// Delay before stopping the service
private static final int SERVICE_STOP_DELAY_MILLIS = 2000;
private static final String EXTRA_REQUEST = "request";
private static final String EXTRA_MYPID = "mypid";
private static final String WAKELOCK_ID = "mmslib_wakelock";
/**
* Thread pool size for each request queue
*/
private static volatile int sThreadPoolSize = DEFAULT_THREAD_POOL_SIZE;
/**
* Optional wake lock to use
*/
private static volatile boolean sUseWakeLock = true;
private static volatile PowerManager.WakeLock sWakeLock = null;
private static final Object sWakeLockLock = new Object();
/**
* Carrier configuration values loader
*/
private static volatile CarrierConfigValuesLoader sCarrierConfigValuesLoader = null;
/**
* APN loader
*/
private static volatile ApnSettingsLoader sApnSettingsLoader = null;
/**
* UserAgent and UA Prof URL loader
*/
private static volatile UserAgentInfoLoader sUserAgentInfoLoader = null;
/**
* Set the size of thread pool for request execution.
* Default is DEFAULT_THREAD_POOL_SIZE
*
* @param size thread pool size
*/
static void setThreadPoolSize(final int size) {
sThreadPoolSize = size;
}
/**
* Set whether to use wake lock
*
* @param useWakeLock true to use wake lock, false otherwise
*/
static void setUseWakeLock(final boolean useWakeLock) {
sUseWakeLock = useWakeLock;
}
/**
* Set the optional carrier config values
*
* @param loader the carrier config values loader
*/
static void setCarrierConfigValuesLoader(final CarrierConfigValuesLoader loader) {
sCarrierConfigValuesLoader = loader;
}
/**
* Get the current carrier config values loader
*
* @return the carrier config values loader currently set
*/
static CarrierConfigValuesLoader getCarrierConfigValuesLoader() {
return sCarrierConfigValuesLoader;
}
/**
* Set APN settings loader
*
* @param loader the APN settings loader
*/
static void setApnSettingsLoader(final ApnSettingsLoader loader) {
sApnSettingsLoader = loader;
}
/**
* Get the current APN settings loader
*
* @return the APN settings loader currently set
*/
static ApnSettingsLoader getApnSettingsLoader() {
return sApnSettingsLoader;
}
/**
* Set user agent info loader
*
* @param loader the user agent info loader
*/
static void setUserAgentInfoLoader(final UserAgentInfoLoader loader) {
sUserAgentInfoLoader = loader;
}
/**
* Get the current user agent info loader
*
* @return the user agent info loader currently set
*/
static UserAgentInfoLoader getUserAgentInfoLoader() {
return sUserAgentInfoLoader;
}
/**
* Make sure loaders are not null. Set to default if that's the case
*
* @param context the Context to use
*/
private static void ensureLoaders(final Context context) {
if (sUserAgentInfoLoader == null) {
sUserAgentInfoLoader = new DefaultUserAgentInfoLoader(context);
}
if (sCarrierConfigValuesLoader == null) {
sCarrierConfigValuesLoader = new DefaultCarrierConfigValuesLoader(context);
}
if (sApnSettingsLoader == null) {
sApnSettingsLoader = new DefaultApnSettingsLoader(context);
}
}
/**
* Acquire the wake lock
*
* @param context the context to use
*/
private static void acquireWakeLock(final Context context) {
synchronized (sWakeLockLock) {
if (sWakeLock == null) {
final PowerManager pm =
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_ID);
}
sWakeLock.acquire();
}
}
/**
* Release the wake lock
*/
private static void releaseWakeLock() {
boolean releasedEmptyWakeLock = false;
synchronized (sWakeLockLock) {
if (sWakeLock != null) {
sWakeLock.release();
} else {
releasedEmptyWakeLock = true;
}
}
if (releasedEmptyWakeLock) {
Log.w(TAG, "Releasing empty wake lock");
}
}
/**
* Check if wake lock is not held (e.g. when service stops)
*/
private static void verifyWakeLockNotHeld() {
boolean wakeLockHeld = false;
synchronized (sWakeLockLock) {
wakeLockHeld = sWakeLock != null && sWakeLock.isHeld();
}
if (wakeLockHeld) {
Log.e(TAG, "Wake lock still held!");
}
}
// Remember my PID to discard restarted intent
private static volatile int sMyPid = -1;
/**
* Get the current PID
*
* @return the current PID
*/
private static int getMyPid() {
if (sMyPid < 0) {
sMyPid = Process.myPid();
}
return sMyPid;
}
/**
* Check if the intent is coming from this process
*
* @param intent the incoming intent for the service
* @return true if the intent is from the current process
*/
private static boolean fromThisProcess(final Intent intent) {
final int pid = intent.getIntExtra(EXTRA_MYPID, -1);
return pid == getMyPid();
}
// Request execution thread pools. One thread pool for sending and one for downloading.
// The size of the thread pool controls the parallelism of request execution.
// See {@link setThreadPoolSize}
private ExecutorService[] mExecutors = new ExecutorService[2];
// Active request count
private int mActiveRequestCount;
// The latest intent startId, used for safely stopping service
private int mLastStartId;
private MmsNetworkManager mNetworkManager;
// Handler for scheduling service stop
private final Handler mHandler = new Handler();
// Service stop task
private final Runnable mServiceStopRunnable = new Runnable() {
@Override
public void run() {
tryStopService();
}
};
/**
* Start the service with a request
*
* @param context the Context to use
* @param request the request to start
*/
public static void startRequest(final Context context, final MmsRequest request) {
final boolean useWakeLock = sUseWakeLock;
request.setUseWakeLock(useWakeLock);
final Intent intent = new Intent(context, MmsService.class);
intent.putExtra(EXTRA_REQUEST, request);
intent.putExtra(EXTRA_MYPID, getMyPid());
if (useWakeLock) {
acquireWakeLock(context);
}
if (context.startService(intent) == null) {
if (useWakeLock) {
releaseWakeLock();
}
}
}
@Override
public void onCreate() {
super.onCreate();
ensureLoaders(this);
for (int i = 0; i < mExecutors.length; i++) {
mExecutors[i] = Executors.newFixedThreadPool(sThreadPoolSize);
}
mNetworkManager = new MmsNetworkManager(this);
synchronized (this) {
mActiveRequestCount = 0;
mLastStartId = -1;
}
}
@Override
public void onDestroy() {
super.onDestroy();
for (ExecutorService executor : mExecutors) {
executor.shutdown();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Always remember the latest startId for use when we try releasing the service
synchronized (this) {
mLastStartId = startId;
}
boolean scheduled = false;
if (intent != null) {
// There is a rare situation that right after a intent is started,
// the service gets killed. Then the service will restart with
// the old intent which we don't want it to run since it will
// break our assumption for wake lock. Check the process ID
// embedded in the intent to make sure it is indeed from the
// the current life of this service.
if (fromThisProcess(intent)) {
final MmsRequest request = intent.getParcelableExtra(EXTRA_REQUEST);
if (request != null) {
try {
retainService(request, new Runnable() {
@Override
public void run() {
try {
request.execute(
MmsService.this,
mNetworkManager,
getApnSettingsLoader(),
getCarrierConfigValuesLoader(),
getUserAgentInfoLoader());
} catch (Exception e) {
Log.w(TAG, "Unexpected execution failure", e);
} finally {
if (request.getUseWakeLock()) {
releaseWakeLock();
}
releaseService();
}
}
});
scheduled = true;
} catch (RejectedExecutionException e) {
// Rare thing happened. Send back failure using the pending intent
// and also release the wake lock.
Log.w(TAG, "Executing request failed " + e);
request.returnResult(this, SmsManager.MMS_ERROR_UNSPECIFIED,
null/*response*/, 0/*httpStatusCode*/);
if (request.getUseWakeLock()) {
releaseWakeLock();
}
}
} else {
Log.w(TAG, "Empty request");
}
} else {
Log.w(TAG, "Got a restarted intent from previous incarnation");
}
} else {
Log.w(TAG, "Empty intent");
}
if (!scheduled) {
// If the request is not started successfully, we need to try shutdown the service
// if nobody is using it.
tryScheduleStop();
}
return START_NOT_STICKY;
}
/**
* Retain the service for executing the request in service thread pool
*
* @param request The request to execute
* @param runnable The runnable to run the request in thread pool
*/
private void retainService(final MmsRequest request, final Runnable runnable) {
final ExecutorService executor = getRequestExecutor(request);
synchronized (this) {
executor.execute(runnable);
mActiveRequestCount++;
}
}
/**
* Release the service from the request. If nobody is using it, schedule service stop.
*/
private void releaseService() {
synchronized (this) {
mActiveRequestCount--;
if (mActiveRequestCount <= 0) {
mActiveRequestCount = 0;
rescheduleServiceStop();
}
}
}
/**
* Schedule the service stop if there is no active request
*/
private void tryScheduleStop() {
synchronized (this) {
if (mActiveRequestCount == 0) {
rescheduleServiceStop();
}
}
}
/**
* Reschedule service stop task
*/
private void rescheduleServiceStop() {
mHandler.removeCallbacks(mServiceStopRunnable);
mHandler.postDelayed(mServiceStopRunnable, SERVICE_STOP_DELAY_MILLIS);
}
/**
* Really try to stop the service if there is not active request
*/
private void tryStopService() {
Boolean stopped = null;
synchronized (this) {
if (mActiveRequestCount == 0) {
stopped = stopSelfResult(mLastStartId);
}
}
logServiceStop(stopped);
}
/**
* Log the result of service stopping. Also check wake lock status when service stops.
*
* @param stopped Not empty if service stop is performed: true if really stopped, false
* if cancelled.
*/
private void logServiceStop(final Boolean stopped) {
if (stopped != null) {
if (stopped) {
Log.i(TAG, "Service successfully stopped");
verifyWakeLockNotHeld();
} else {
Log.i(TAG, "Service stopping cancelled");
}
}
}
private ExecutorService getRequestExecutor(final MmsRequest request) {
if (request instanceof SendRequest) {
// Send
return mExecutors[0];
} else {
// Download
return mExecutors[1];
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
@@ -1,143 +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 android.support.v7.mms;
import android.util.Log;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
/**
* Base class for a parser of XML resources
*/
abstract class MmsXmlResourceParser {
/**
* Parse the content
*
* @throws IOException
* @throws XmlPullParserException
*/
protected abstract void parseRecord() throws IOException, XmlPullParserException;
/**
* Get the root tag of the content
*
* @return the text of root tag
*/
protected abstract String getRootTag();
private final StringBuilder mLogStringBuilder = new StringBuilder();
protected final XmlPullParser mInputParser;
protected MmsXmlResourceParser(XmlPullParser parser) {
mInputParser = parser;
}
void parse() {
try {
// Find the first element
if (advanceToNextEvent(XmlPullParser.START_TAG) != XmlPullParser.START_TAG) {
throw new XmlPullParserException("ApnsXmlProcessor: expecting start tag @"
+ xmlParserDebugContext());
}
if (!getRootTag().equals(mInputParser.getName())) {
Log.w(MmsService.TAG, "Carrier config does not start with " + getRootTag());
return;
}
// We are at the start tag
for (;;) {
int nextEvent;
// Skipping spaces
while ((nextEvent = mInputParser.next()) == XmlPullParser.TEXT);
if (nextEvent == XmlPullParser.START_TAG) {
// Parse one record
parseRecord();
} else if (nextEvent == XmlPullParser.END_TAG) {
break;
} else {
throw new XmlPullParserException("Expecting start or end tag @"
+ xmlParserDebugContext());
}
}
} catch (IOException e) {
Log.w(MmsService.TAG, "XmlResourceParser: I/O failure", e);
} catch (XmlPullParserException e) {
Log.w(MmsService.TAG, "XmlResourceParser: parsing failure", e);
}
}
/**
* 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
*/
protected int advanceToNextEvent(int eventType) throws XmlPullParserException, IOException {
for (;;) {
int nextEvent = mInputParser.next();
if (nextEvent == eventType
|| nextEvent == XmlPullParser.END_DOCUMENT) {
return nextEvent;
}
}
}
/**
* @return The debugging information of the parser's current position
*/
protected 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) {
Log.w(MmsService.TAG, "XmlResourceParser exception", e);
}
}
return "Unknown";
}
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);
}
}
@@ -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 android.support.v7.mms;
import android.text.TextUtils;
import android.util.Log;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.i18n.phonenumbers.Phonenumber;
/**
* Helper methods for phone number formatting
* This is isolated into a standalone class since it depends on libphonenumber
*/
public class PhoneNumberHelper {
/**
* Given a phone number, get its national part without country code
*
* @param number the original number
* @param country the country ISO code
* @return the national number
*/
static String getNumberNoCountryCode(final String number, final String country) {
if (!TextUtils.isEmpty(number)) {
final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
try {
final Phonenumber.PhoneNumber phoneNumber = phoneNumberUtil.parse(number, country);
if (phoneNumber != null && phoneNumberUtil.isValidNumber(phoneNumber)) {
return phoneNumberUtil
.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL)
.replaceAll("\\D", "");
}
} catch (final NumberParseException e) {
Log.w(MmsService.TAG, "getNumberNoCountryCode: invalid number " + e);
}
}
return number;
}
}
-163
View File
@@ -1,163 +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 android.support.v7.mms;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.telephony.SmsManager;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* Request to send an MMS
*/
class SendRequest extends MmsRequest {
// Max send response PDU size in bytes (exceeding this may cause problem with
// system intent delivery).
private static final int MAX_SEND_RESPONSE_SIZE = 1000 * 1024;
private byte[] mPduData;
SendRequest(final String locationUrl, final Uri pduUri, final PendingIntent sentIntent) {
super(locationUrl, pduUri, sentIntent);
}
@Override
protected boolean loadRequest(final Context context, final Bundle mmsConfig) {
mPduData = readPduFromContentUri(
context,
mPduUri,
mmsConfig.getInt(
CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE,
CarrierConfigValuesLoader.CONFIG_MAX_MESSAGE_SIZE_DEFAULT));
return (mPduData != null);
}
@Override
protected boolean transferResponse(final Context context, final Intent fillIn,
final byte[] response) {
// SendConf pdus are always small and can be included in the intent
if (response != null && fillIn != null) {
if (response.length > MAX_SEND_RESPONSE_SIZE) {
// If the response PDU is too large, it won't be able to fit in
// the PendingIntent to be transferred via system IPC.
return false;
}
fillIn.putExtra(SmsManager.EXTRA_MMS_DATA, response);
}
return true;
}
@Override
protected byte[] doHttp(Context context, MmsNetworkManager netMgr, ApnSettingsLoader.Apn apn,
Bundle mmsConfig, String userAgent, String uaProfUrl) throws MmsHttpException {
final MmsHttpClient httpClient = netMgr.getHttpClient();
return httpClient.execute(getHttpRequestUrl(apn), mPduData, MmsHttpClient.METHOD_POST,
!TextUtils.isEmpty(apn.getMmsProxy()), apn.getMmsProxy(), apn.getMmsProxyPort(),
mmsConfig, userAgent, uaProfUrl);
}
@Override
protected String getHttpRequestUrl(final ApnSettingsLoader.Apn apn) {
return !TextUtils.isEmpty(mLocationUrl) ? mLocationUrl : apn.getMmsc();
}
/**
* Read pdu from content provider uri
*
* @param contentUri content provider uri from which to read
* @param maxSize maximum number of bytes to read
* @return pdu bytes if succeeded else null
*/
public byte[] readPduFromContentUri(final Context context, final Uri contentUri,
final int maxSize) {
if (contentUri == null) {
return null;
}
final Callable<byte[]> copyPduToArray = new Callable<byte[]>() {
public byte[] call() {
ParcelFileDescriptor.AutoCloseInputStream inStream = null;
try {
final ContentResolver cr = context.getContentResolver();
final ParcelFileDescriptor pduFd = cr.openFileDescriptor(contentUri, "r");
inStream = new ParcelFileDescriptor.AutoCloseInputStream(pduFd);
// Request one extra byte to make sure file not bigger than maxSize
final byte[] readBuf = new byte[maxSize+1];
final int bytesRead = inStream.read(readBuf, 0, maxSize+1);
if (bytesRead <= 0) {
Log.e(MmsService.TAG, "Reading PDU from sender: empty PDU");
return null;
}
if (bytesRead > maxSize) {
Log.e(MmsService.TAG, "Reading PDU from sender: PDU too large");
return null;
}
// Copy and return the exact length of bytes
final byte[] result = new byte[bytesRead];
System.arraycopy(readBuf, 0, result, 0, bytesRead);
return result;
} catch (IOException e) {
Log.e(MmsService.TAG, "Reading PDU from sender: IO exception", e);
return null;
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (IOException ex) {
// Ignore
}
}
}
}
};
final Future<byte[]> pendingResult = mPduTransferExecutor.submit(copyPduToArray);
try {
return pendingResult.get(TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (Exception e) {
// Typically a timeout occurred - cancel task
pendingResult.cancel(true);
}
return null;
}
public static final Parcelable.Creator<SendRequest> CREATOR
= new Parcelable.Creator<SendRequest>() {
public SendRequest createFromParcel(Parcel in) {
return new SendRequest(in);
}
public SendRequest[] newArray(int size) {
return new SendRequest[size];
}
};
private SendRequest(Parcel in) {
super(in);
}
}
@@ -1,40 +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 android.support.v7.mms;
/**
* Interface to load UserAgent and UA Prof URL
*/
public interface UserAgentInfoLoader {
// Carrier configuration keys for passing as config overrides into system MMS service
public static final String CONFIG_USER_AGENT = "userAgent";
public static final String CONFIG_UA_PROF_URL = "uaProfUrl";
/**
* Get UserAgent value
*
* @return the text of UserAgent
*/
String getUserAgent();
/**
* Get UA Profile URL
*
* @return the URL of UA profile
*/
String getUAProfUrl();
}
-180
View File
@@ -1,180 +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 android.support.v7.mms;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.telephony.SmsManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import java.net.MalformedURLException;
import java.net.URL;
/**
* Utility methods
*/
class Utils {
/**
* Check if MMS API is available
*
* @return true if MMS API is available, false otherwise
*/
static boolean hasMmsApi() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
/**
* Check if support multi-SIM
*
* @return true if MSIM is supported, false otherwise
*/
static boolean supportMSim() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1;
}
/**
* Check if support APIs for getting UserAgent and UAProfUrl
*
* @return true if those APIs are supported, false otherwise
*/
static boolean hasUserAgentApi() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
}
/**
* Get system SmsManager
*
* @param subId the subscription ID of the SmsManager
* @return the SmsManager for the input subId
*/
static SmsManager getSmsManager(final int subId) {
if (supportMSim()) {
return SmsManager.getSmsManagerForSubscriptionId(subId);
} else {
return SmsManager.getDefault();
}
}
/**
* Get the real subscription ID if the input is -1
*
* @param subId input subscription ID
* @return the default SMS subscription ID if the input is -1, otherwise the original
*/
static int getEffectiveSubscriptionId(int subId) {
if (supportMSim()) {
if (subId == MmsManager.DEFAULT_SUB_ID) {
subId = SmsManager.getDefaultSmsSubscriptionId();
}
}
if (subId < 0) {
subId = MmsManager.DEFAULT_SUB_ID;
}
return subId;
}
/**
* Get MCC/MNC of an SIM subscription
*
* @param context the Context to use
* @param subId the SIM subId
* @return a non-empty array with exactly two elements, first is mcc and last is mnc.
*/
static int[] getMccMnc(final Context context, final int subId) {
final int[] mccMnc = new int[] { 0, 0 };
if (Utils.supportMSim()) {
final SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
final SubscriptionInfo subInfo = subscriptionManager.getActiveSubscriptionInfo(subId);
if (subInfo != null) {
mccMnc[0] = subInfo.getMcc();
mccMnc[1] = subInfo.getMnc();
}
} else {
final TelephonyManager telephonyManager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
final String mccMncString = telephonyManager.getSimOperator();
try {
mccMnc[0] = Integer.parseInt(mccMncString.substring(0, 3));
mccMnc[1] = Integer.parseInt(mccMncString.substring(3));
} catch (Exception e) {
Log.w(MmsService.TAG, "Invalid mcc/mnc from system " + mccMncString + ": " + e);
mccMnc[0] = 0;
mccMnc[1] = 0;
}
}
return mccMnc;
}
/**
* 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
*/
static Context getSubDepContext(final Context context, final int subId) {
if (!supportMSim()) {
return context;
}
final int[] mccMnc = getMccMnc(context, subId);
final int mcc = mccMnc[0];
final int mnc = mccMnc[1];
if (mcc == 0 && mnc == 0) {
return context;
}
final Configuration subConfig = new Configuration();
subConfig.mcc = mcc;
subConfig.mnc = mnc;
return context.createConfigurationContext(subConfig);
}
/**
* Redact the URL for non-VERBOSE logging. Replace url with only the host part and the length
* of the input URL string.
*
* @param urlString
* @return
*/
static String redactUrlForNonVerbose(String urlString) {
if (Log.isLoggable(MmsService.TAG, Log.VERBOSE)) {
// Don't redact for VERBOSE level logging
return urlString;
}
if (TextUtils.isEmpty(urlString)) {
return urlString;
}
String protocol = "http";
String host = "";
try {
final URL url = new URL(urlString);
protocol = url.getProtocol();
host = url.getHost();
} catch (MalformedURLException e) {
// Ignore
}
// Print "http://host[length]"
final StringBuilder sb = new StringBuilder();
sb.append(protocol).append("://").append(host)
.append("[").append(urlString.length()).append("]");
return sb.toString();
}
}
@@ -1,87 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* M-Acknowledge.ind PDU.
*/
public class AcknowledgeInd extends GenericPdu {
/**
* Constructor, used when composing a M-Acknowledge.ind pdu.
*
* @param mmsVersion current viersion of mms
* @param transactionId the transaction-id value
* @throws InvalidHeaderValueException if parameters are invalid.
* NullPointerException if transactionId is null.
*/
public AcknowledgeInd(int mmsVersion, byte[] transactionId)
throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_ACKNOWLEDGE_IND);
setMmsVersion(mmsVersion);
setTransactionId(transactionId);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
AcknowledgeInd(PduHeaders headers) {
super(headers);
}
/**
* Get X-Mms-Report-Allowed field value.
*
* @return the X-Mms-Report-Allowed value
*/
public int getReportAllowed() {
return mPduHeaders.getOctet(PduHeaders.REPORT_ALLOWED);
}
/**
* Set X-Mms-Report-Allowed field value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setReportAllowed(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.REPORT_ALLOWED);
}
/**
* Get X-Mms-Transaction-Id field value.
*
* @return the X-Mms-Report-Allowed value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id field value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
}
-167
View File
@@ -1,167 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class Base64 {
/**
* Used to get the number of Quadruples.
*/
static final int FOURBYTE = 4;
/**
* Byte used to pad output.
*/
static final byte PAD = (byte) '=';
/**
* The base length.
*/
static final int BASELENGTH = 255;
// Create arrays to hold the base64 characters
private static byte[] base64Alphabet = new byte[BASELENGTH];
// Populating the character arrays
static {
for (int i = 0; i < BASELENGTH; i++) {
base64Alphabet[i] = (byte) -1;
}
for (int i = 'Z'; i >= 'A'; i--) {
base64Alphabet[i] = (byte) (i - 'A');
}
for (int i = 'z'; i >= 'a'; i--) {
base64Alphabet[i] = (byte) (i - 'a' + 26);
}
for (int i = '9'; i >= '0'; i--) {
base64Alphabet[i] = (byte) (i - '0' + 52);
}
base64Alphabet['+'] = 62;
base64Alphabet['/'] = 63;
}
/**
* Decodes Base64 data into octects
*
* @param base64Data Byte array containing Base64 data
* @return Array containing decoded data.
*/
public static byte[] decodeBase64(byte[] base64Data) {
// RFC 2045 requires that we discard ALL non-Base64 characters
base64Data = discardNonBase64(base64Data);
// handle the edge case, so we don't have to worry about it later
if (base64Data.length == 0) {
return new byte[0];
}
int numberQuadruple = base64Data.length / FOURBYTE;
byte decodedData[] = null;
byte b1 = 0, b2 = 0, b3 = 0, b4 = 0, marker0 = 0, marker1 = 0;
// Throw away anything not in base64Data
int encodedIndex = 0;
int dataIndex = 0;
{
// this sizes the output array properly - rlw
int lastData = base64Data.length;
// ignore the '=' padding
while (base64Data[lastData - 1] == PAD) {
if (--lastData == 0) {
return new byte[0];
}
}
decodedData = new byte[lastData - numberQuadruple];
}
for (int i = 0; i < numberQuadruple; i++) {
dataIndex = i * 4;
marker0 = base64Data[dataIndex + 2];
marker1 = base64Data[dataIndex + 3];
b1 = base64Alphabet[base64Data[dataIndex]];
b2 = base64Alphabet[base64Data[dataIndex + 1]];
if (marker0 != PAD && marker1 != PAD) {
//No PAD e.g 3cQl
b3 = base64Alphabet[marker0];
b4 = base64Alphabet[marker1];
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex + 1] =
(byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
decodedData[encodedIndex + 2] = (byte) (b3 << 6 | b4);
} else if (marker0 == PAD) {
//Two PAD e.g. 3c[Pad][Pad]
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
} else if (marker1 == PAD) {
//One PAD e.g. 3cQ[Pad]
b3 = base64Alphabet[marker0];
decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
decodedData[encodedIndex + 1] =
(byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
}
encodedIndex += 3;
}
return decodedData;
}
/**
* Check octect wheter it is a base64 encoding.
*
* @param octect to be checked byte
* @return ture if it is base64 encoding, false otherwise.
*/
private static boolean isBase64(byte octect) {
if (octect == PAD) {
return true;
} else if (base64Alphabet[octect] == -1) {
return false;
} else {
return true;
}
}
/**
* Discards any characters outside of the base64 alphabet, per
* the requirements on page 25 of RFC 2045 - "Any characters
* outside of the base64 alphabet are to be ignored in base64
* encoded data."
*
* @param data The base-64 encoded data to groom
* @return The data, less non-base64 characters (see RFC 2045).
*/
static byte[] discardNonBase64(byte[] data) {
byte groomedData[] = new byte[data.length];
int bytesCopied = 0;
for (int i = 0; i < data.length; i++) {
if (isBase64(data[i])) {
groomedData[bytesCopied++] = data[i];
}
}
byte packedData[] = new byte[bytesCopied];
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
return packedData;
}
}
@@ -1,172 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
public class CharacterSets {
/**
* IANA assigned MIB enum numbers.
*
* From wap-230-wsp-20010705-a.pdf
* Any-charset = <Octet 128>
* Equivalent to the special RFC2616 charset value "*"
*/
public static final int ANY_CHARSET = 0x00;
public static final int US_ASCII = 0x03;
public static final int ISO_8859_1 = 0x04;
public static final int ISO_8859_2 = 0x05;
public static final int ISO_8859_3 = 0x06;
public static final int ISO_8859_4 = 0x07;
public static final int ISO_8859_5 = 0x08;
public static final int ISO_8859_6 = 0x09;
public static final int ISO_8859_7 = 0x0A;
public static final int ISO_8859_8 = 0x0B;
public static final int ISO_8859_9 = 0x0C;
public static final int SHIFT_JIS = 0x11;
public static final int UTF_8 = 0x6A;
public static final int BIG5 = 0x07EA;
public static final int UCS2 = 0x03E8;
public static final int UTF_16 = 0x03F7;
/**
* If the encoding of given data is unsupported, use UTF_8 to decode it.
*/
public static final int DEFAULT_CHARSET = UTF_8;
/**
* Array of MIB enum numbers.
*/
private static final int[] MIBENUM_NUMBERS = {
ANY_CHARSET,
US_ASCII,
ISO_8859_1,
ISO_8859_2,
ISO_8859_3,
ISO_8859_4,
ISO_8859_5,
ISO_8859_6,
ISO_8859_7,
ISO_8859_8,
ISO_8859_9,
SHIFT_JIS,
UTF_8,
BIG5,
UCS2,
UTF_16,
};
/**
* The Well-known-charset Mime name.
*/
public static final String MIMENAME_ANY_CHARSET = "*";
public static final String MIMENAME_US_ASCII = "us-ascii";
public static final String MIMENAME_ISO_8859_1 = "iso-8859-1";
public static final String MIMENAME_ISO_8859_2 = "iso-8859-2";
public static final String MIMENAME_ISO_8859_3 = "iso-8859-3";
public static final String MIMENAME_ISO_8859_4 = "iso-8859-4";
public static final String MIMENAME_ISO_8859_5 = "iso-8859-5";
public static final String MIMENAME_ISO_8859_6 = "iso-8859-6";
public static final String MIMENAME_ISO_8859_7 = "iso-8859-7";
public static final String MIMENAME_ISO_8859_8 = "iso-8859-8";
public static final String MIMENAME_ISO_8859_9 = "iso-8859-9";
public static final String MIMENAME_SHIFT_JIS = "shift_JIS";
public static final String MIMENAME_UTF_8 = "utf-8";
public static final String MIMENAME_BIG5 = "big5";
public static final String MIMENAME_UCS2 = "iso-10646-ucs-2";
public static final String MIMENAME_UTF_16 = "utf-16";
public static final String DEFAULT_CHARSET_NAME = MIMENAME_UTF_8;
/**
* Array of the names of character sets.
*/
private static final String[] MIME_NAMES = {
MIMENAME_ANY_CHARSET,
MIMENAME_US_ASCII,
MIMENAME_ISO_8859_1,
MIMENAME_ISO_8859_2,
MIMENAME_ISO_8859_3,
MIMENAME_ISO_8859_4,
MIMENAME_ISO_8859_5,
MIMENAME_ISO_8859_6,
MIMENAME_ISO_8859_7,
MIMENAME_ISO_8859_8,
MIMENAME_ISO_8859_9,
MIMENAME_SHIFT_JIS,
MIMENAME_UTF_8,
MIMENAME_BIG5,
MIMENAME_UCS2,
MIMENAME_UTF_16,
};
private static final HashMap<Integer, String> MIBENUM_TO_NAME_MAP;
private static final HashMap<String, Integer> NAME_TO_MIBENUM_MAP;
static {
// Create the HashMaps.
MIBENUM_TO_NAME_MAP = new HashMap<Integer, String>();
NAME_TO_MIBENUM_MAP = new HashMap<String, Integer>();
assert(MIBENUM_NUMBERS.length == MIME_NAMES.length);
int count = MIBENUM_NUMBERS.length - 1;
for(int i = 0; i <= count; i++) {
MIBENUM_TO_NAME_MAP.put(MIBENUM_NUMBERS[i], MIME_NAMES[i]);
NAME_TO_MIBENUM_MAP.put(MIME_NAMES[i], MIBENUM_NUMBERS[i]);
}
}
private CharacterSets() {} // Non-instantiatable
/**
* Map an MIBEnum number to the name of the charset which this number
* is assigned to by IANA.
*
* @param mibEnumValue An IANA assigned MIBEnum number.
* @return The name string of the charset.
* @throws UnsupportedEncodingException
*/
public static String getMimeName(int mibEnumValue)
throws UnsupportedEncodingException {
String name = MIBENUM_TO_NAME_MAP.get(mibEnumValue);
if (name == null) {
throw new UnsupportedEncodingException();
}
return name;
}
/**
* Map a well-known charset name to its assigned MIBEnum number.
*
* @param mimeName The charset name.
* @return The MIBEnum number assigned by IANA for this charset.
* @throws UnsupportedEncodingException
*/
public static int getMibEnumValue(String mimeName)
throws UnsupportedEncodingException {
if(null == mimeName) {
return -1;
}
Integer mibEnumValue = NAME_TO_MIBENUM_MAP.get(mimeName);
if (mibEnumValue == null) {
throw new UnsupportedEncodingException();
}
return mibEnumValue;
}
}
@@ -1,230 +0,0 @@
/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 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 android.support.v7.mms.pdu;
import java.util.ArrayList;
public class ContentType {
public static final String MMS_MESSAGE = "application/vnd.wap.mms-message";
// The phony content type for generic PDUs (e.g. ReadOrig.ind,
// Notification.ind, Delivery.ind).
public static final String MMS_GENERIC = "application/vnd.wap.mms-generic";
public static final String MULTIPART_MIXED = "application/vnd.wap.multipart.mixed";
public static final String MULTIPART_RELATED = "application/vnd.wap.multipart.related";
public static final String MULTIPART_ALTERNATIVE = "application/vnd.wap.multipart.alternative";
public static final String TEXT_PLAIN = "text/plain";
public static final String TEXT_HTML = "text/html";
public static final String TEXT_VCALENDAR = "text/x-vCalendar";
public static final String TEXT_VCARD = "text/x-vCard";
public static final String IMAGE_UNSPECIFIED = "image/*";
public static final String IMAGE_JPEG = "image/jpeg";
public static final String IMAGE_JPG = "image/jpg";
public static final String IMAGE_GIF = "image/gif";
public static final String IMAGE_WBMP = "image/vnd.wap.wbmp";
public static final String IMAGE_PNG = "image/png";
public static final String IMAGE_X_MS_BMP = "image/x-ms-bmp";
public static final String AUDIO_UNSPECIFIED = "audio/*";
public static final String AUDIO_AAC = "audio/aac";
public static final String AUDIO_AMR = "audio/amr";
public static final String AUDIO_IMELODY = "audio/imelody";
public static final String AUDIO_MID = "audio/mid";
public static final String AUDIO_MIDI = "audio/midi";
public static final String AUDIO_MP3 = "audio/mp3";
public static final String AUDIO_MPEG3 = "audio/mpeg3";
public static final String AUDIO_MPEG = "audio/mpeg";
public static final String AUDIO_MPG = "audio/mpg";
public static final String AUDIO_MP4 = "audio/mp4";
public static final String AUDIO_X_MID = "audio/x-mid";
public static final String AUDIO_X_MIDI = "audio/x-midi";
public static final String AUDIO_X_MP3 = "audio/x-mp3";
public static final String AUDIO_X_MPEG3 = "audio/x-mpeg3";
public static final String AUDIO_X_MPEG = "audio/x-mpeg";
public static final String AUDIO_X_MPG = "audio/x-mpg";
public static final String AUDIO_3GPP = "audio/3gpp";
public static final String AUDIO_X_WAV = "audio/x-wav";
public static final String AUDIO_OGG = "application/ogg";
public static final String VIDEO_UNSPECIFIED = "video/*";
public static final String VIDEO_3GPP = "video/3gpp";
public static final String VIDEO_3G2 = "video/3gpp2";
public static final String VIDEO_H263 = "video/h263";
public static final String VIDEO_MP4 = "video/mp4";
public static final String APP_SMIL = "application/smil";
public static final String APP_WAP_XHTML = "application/vnd.wap.xhtml+xml";
public static final String APP_XHTML = "application/xhtml+xml";
public static final String APP_DRM_CONTENT = "application/vnd.oma.drm.content";
public static final String APP_DRM_MESSAGE = "application/vnd.oma.drm.message";
private static final ArrayList<String> sSupportedContentTypes = new ArrayList<String>();
private static final ArrayList<String> sSupportedImageTypes = new ArrayList<String>();
private static final ArrayList<String> sSupportedAudioTypes = new ArrayList<String>();
private static final ArrayList<String> sSupportedVideoTypes = new ArrayList<String>();
static {
sSupportedContentTypes.add(TEXT_PLAIN);
sSupportedContentTypes.add(TEXT_HTML);
sSupportedContentTypes.add(TEXT_VCALENDAR);
sSupportedContentTypes.add(TEXT_VCARD);
sSupportedContentTypes.add(IMAGE_JPEG);
sSupportedContentTypes.add(IMAGE_GIF);
sSupportedContentTypes.add(IMAGE_WBMP);
sSupportedContentTypes.add(IMAGE_PNG);
sSupportedContentTypes.add(IMAGE_JPG);
sSupportedContentTypes.add(IMAGE_X_MS_BMP);
//supportedContentTypes.add(IMAGE_SVG); not yet supported.
sSupportedContentTypes.add(AUDIO_AAC);
sSupportedContentTypes.add(AUDIO_AMR);
sSupportedContentTypes.add(AUDIO_IMELODY);
sSupportedContentTypes.add(AUDIO_MID);
sSupportedContentTypes.add(AUDIO_MIDI);
sSupportedContentTypes.add(AUDIO_MP3);
sSupportedContentTypes.add(AUDIO_MP4);
sSupportedContentTypes.add(AUDIO_MPEG3);
sSupportedContentTypes.add(AUDIO_MPEG);
sSupportedContentTypes.add(AUDIO_MPG);
sSupportedContentTypes.add(AUDIO_X_MID);
sSupportedContentTypes.add(AUDIO_X_MIDI);
sSupportedContentTypes.add(AUDIO_X_MP3);
sSupportedContentTypes.add(AUDIO_X_MPEG3);
sSupportedContentTypes.add(AUDIO_X_MPEG);
sSupportedContentTypes.add(AUDIO_X_MPG);
sSupportedContentTypes.add(AUDIO_X_WAV);
sSupportedContentTypes.add(AUDIO_3GPP);
sSupportedContentTypes.add(AUDIO_OGG);
sSupportedContentTypes.add(VIDEO_3GPP);
sSupportedContentTypes.add(VIDEO_3G2);
sSupportedContentTypes.add(VIDEO_H263);
sSupportedContentTypes.add(VIDEO_MP4);
sSupportedContentTypes.add(APP_SMIL);
sSupportedContentTypes.add(APP_WAP_XHTML);
sSupportedContentTypes.add(APP_XHTML);
sSupportedContentTypes.add(APP_DRM_CONTENT);
sSupportedContentTypes.add(APP_DRM_MESSAGE);
// add supported image types
sSupportedImageTypes.add(IMAGE_JPEG);
sSupportedImageTypes.add(IMAGE_GIF);
sSupportedImageTypes.add(IMAGE_WBMP);
sSupportedImageTypes.add(IMAGE_PNG);
sSupportedImageTypes.add(IMAGE_JPG);
sSupportedImageTypes.add(IMAGE_X_MS_BMP);
// add supported audio types
sSupportedAudioTypes.add(AUDIO_AAC);
sSupportedAudioTypes.add(AUDIO_AMR);
sSupportedAudioTypes.add(AUDIO_IMELODY);
sSupportedAudioTypes.add(AUDIO_MID);
sSupportedAudioTypes.add(AUDIO_MIDI);
sSupportedAudioTypes.add(AUDIO_MP3);
sSupportedAudioTypes.add(AUDIO_MPEG3);
sSupportedAudioTypes.add(AUDIO_MPEG);
sSupportedAudioTypes.add(AUDIO_MPG);
sSupportedAudioTypes.add(AUDIO_MP4);
sSupportedAudioTypes.add(AUDIO_X_MID);
sSupportedAudioTypes.add(AUDIO_X_MIDI);
sSupportedAudioTypes.add(AUDIO_X_MP3);
sSupportedAudioTypes.add(AUDIO_X_MPEG3);
sSupportedAudioTypes.add(AUDIO_X_MPEG);
sSupportedAudioTypes.add(AUDIO_X_MPG);
sSupportedAudioTypes.add(AUDIO_X_WAV);
sSupportedAudioTypes.add(AUDIO_3GPP);
sSupportedAudioTypes.add(AUDIO_OGG);
// add supported video types
sSupportedVideoTypes.add(VIDEO_3GPP);
sSupportedVideoTypes.add(VIDEO_3G2);
sSupportedVideoTypes.add(VIDEO_H263);
sSupportedVideoTypes.add(VIDEO_MP4);
}
// This class should never be instantiated.
private ContentType() {
}
public static boolean isSupportedType(String contentType) {
return (null != contentType) && sSupportedContentTypes.contains(contentType);
}
public static boolean isSupportedImageType(String contentType) {
return isImageType(contentType) && isSupportedType(contentType);
}
public static boolean isSupportedAudioType(String contentType) {
return isAudioType(contentType) && isSupportedType(contentType);
}
public static boolean isSupportedVideoType(String contentType) {
return isVideoType(contentType) && isSupportedType(contentType);
}
public static boolean isTextType(String contentType) {
return (null != contentType) && contentType.startsWith("text/");
}
public static boolean isImageType(String contentType) {
return (null != contentType) && contentType.startsWith("image/");
}
public static boolean isAudioType(String contentType) {
return (null != contentType) && contentType.startsWith("audio/");
}
public static boolean isVideoType(String contentType) {
return (null != contentType) && contentType.startsWith("video/");
}
public static boolean isDrmType(String contentType) {
return (null != contentType)
&& (contentType.equals(APP_DRM_CONTENT)
|| contentType.equals(APP_DRM_MESSAGE));
}
public static boolean isUnspecified(String contentType) {
return (null != contentType) && contentType.endsWith("*");
}
@SuppressWarnings("unchecked")
public static ArrayList<String> getImageTypes() {
return (ArrayList<String>) sSupportedImageTypes.clone();
}
@SuppressWarnings("unchecked")
public static ArrayList<String> getAudioTypes() {
return (ArrayList<String>) sSupportedAudioTypes.clone();
}
@SuppressWarnings("unchecked")
public static ArrayList<String> getVideoTypes() {
return (ArrayList<String>) sSupportedVideoTypes.clone();
}
@SuppressWarnings("unchecked")
public static ArrayList<String> getSupportedTypes() {
return (ArrayList<String>) sSupportedContentTypes.clone();
}
}
@@ -1,136 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* M-Delivery.Ind Pdu.
*/
public class DeliveryInd extends GenericPdu {
/**
* Empty constructor.
* Since the Pdu corresponding to this class is constructed
* by the Proxy-Relay server, this class is only instantiated
* by the Pdu Parser.
*
* @throws InvalidHeaderValueException if error occurs.
*/
public DeliveryInd() throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_DELIVERY_IND);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
DeliveryInd(PduHeaders headers) {
super(headers);
}
/**
* Get Date value.
*
* @return the value
*/
public long getDate() {
return mPduHeaders.getLongInteger(PduHeaders.DATE);
}
/**
* Set Date value.
*
* @param value the value
*/
public void setDate(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.DATE);
}
/**
* Get Message-ID value.
*
* @return the value
*/
public byte[] getMessageId() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID);
}
/**
* Set Message-ID value.
*
* @param value the value, should not be null
* @throws NullPointerException if the value is null.
*/
public void setMessageId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID);
}
/**
* Get Status value.
*
* @return the value
*/
public int getStatus() {
return mPduHeaders.getOctet(PduHeaders.STATUS);
}
/**
* Set Status value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.STATUS);
}
/**
* Get To value.
*
* @return the value
*/
public EncodedStringValue[] getTo() {
return mPduHeaders.getEncodedStringValues(PduHeaders.TO);
}
/**
* set To value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTo(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.TO);
}
/*
* Optional, not supported header fields:
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*
* public EncodedStringValue getStatusText() {return null;}
* public void setStatusText(EncodedStringValue value) {}
*/
}
@@ -1,283 +0,0 @@
/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 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 android.support.v7.mms.pdu;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
/**
* Encoded-string-value = Text-string | Value-length Char-set Text-string
*/
public class EncodedStringValue implements Cloneable {
private static final String TAG = "EncodedStringValue";
private static final boolean DEBUG = false;
private static final boolean LOCAL_LOGV = false;
/**
* The Char-set value.
*/
private int mCharacterSet;
/**
* The Text-string value.
*/
private byte[] mData;
/**
* Constructor.
*
* @param charset the Char-set value
* @param data the Text-string value
* @throws NullPointerException if Text-string value is null.
*/
public EncodedStringValue(int charset, byte[] data) {
// TODO: CharSet needs to be validated against MIBEnum.
if(null == data) {
throw new NullPointerException("EncodedStringValue: Text-string is null.");
}
mCharacterSet = charset;
mData = new byte[data.length];
System.arraycopy(data, 0, mData, 0, data.length);
}
/**
* Constructor.
*
* @param data the Text-string value
* @throws NullPointerException if Text-string value is null.
*/
public EncodedStringValue(byte[] data) {
this(CharacterSets.DEFAULT_CHARSET, data);
}
public EncodedStringValue(String data) {
try {
mData = data.getBytes(CharacterSets.DEFAULT_CHARSET_NAME);
mCharacterSet = CharacterSets.DEFAULT_CHARSET;
} catch (UnsupportedEncodingException e) {
Log.e(TAG, "Default encoding must be supported.", e);
}
}
/**
* Get Char-set value.
*
* @return the value
*/
public int getCharacterSet() {
return mCharacterSet;
}
/**
* Set Char-set value.
*
* @param charset the Char-set value
*/
public void setCharacterSet(int charset) {
// TODO: CharSet needs to be validated against MIBEnum.
mCharacterSet = charset;
}
/**
* Get Text-string value.
*
* @return the value
*/
public byte[] getTextString() {
byte[] byteArray = new byte[mData.length];
System.arraycopy(mData, 0, byteArray, 0, mData.length);
return byteArray;
}
/**
* Set Text-string value.
*
* @param textString the Text-string value
* @throws NullPointerException if Text-string value is null.
*/
public void setTextString(byte[] textString) {
if(null == textString) {
throw new NullPointerException("EncodedStringValue: Text-string is null.");
}
mData = new byte[textString.length];
System.arraycopy(textString, 0, mData, 0, textString.length);
}
/**
* Convert this object to a {@link java.lang.String}. If the encoding of
* the EncodedStringValue is null or unsupported, it will be
* treated as iso-8859-1 encoding.
*
* @return The decoded String.
*/
public String getString() {
if (CharacterSets.ANY_CHARSET == mCharacterSet) {
return new String(mData); // system default encoding.
} else {
try {
String name = CharacterSets.getMimeName(mCharacterSet);
return new String(mData, name);
} catch (UnsupportedEncodingException e) {
if (LOCAL_LOGV) {
Log.v(TAG, e.getMessage(), e);
}
try {
return new String(mData, CharacterSets.MIMENAME_ISO_8859_1);
} catch (UnsupportedEncodingException _) {
return new String(mData); // system default encoding.
}
}
}
}
/**
* Append to Text-string.
*
* @param textString the textString to append
* @throws NullPointerException if the text String is null
* or an IOException occured.
*/
public void appendTextString(byte[] textString) {
if(null == textString) {
throw new NullPointerException("Text-string is null.");
}
if(null == mData) {
mData = new byte[textString.length];
System.arraycopy(textString, 0, mData, 0, textString.length);
} else {
ByteArrayOutputStream newTextString = new ByteArrayOutputStream();
try {
newTextString.write(mData);
newTextString.write(textString);
} catch (IOException e) {
e.printStackTrace();
throw new NullPointerException(
"appendTextString: failed when write a new Text-string");
}
mData = newTextString.toByteArray();
}
}
/*
* (non-Javadoc)
* @see java.lang.Object#clone()
*/
@Override
public Object clone() throws CloneNotSupportedException {
super.clone();
int len = mData.length;
byte[] dstBytes = new byte[len];
System.arraycopy(mData, 0, dstBytes, 0, len);
try {
return new EncodedStringValue(mCharacterSet, dstBytes);
} catch (Exception e) {
Log.e(TAG, "failed to clone an EncodedStringValue: " + this);
e.printStackTrace();
throw new CloneNotSupportedException(e.getMessage());
}
}
/**
* Split this encoded string around matches of the given pattern.
*
* @param pattern the delimiting pattern
* @return the array of encoded strings computed by splitting this encoded
* string around matches of the given pattern
*/
public EncodedStringValue[] split(String pattern) {
String[] temp = getString().split(pattern);
EncodedStringValue[] ret = new EncodedStringValue[temp.length];
for (int i = 0; i < ret.length; ++i) {
try {
ret[i] = new EncodedStringValue(mCharacterSet,
temp[i].getBytes());
} catch (NullPointerException _) {
// Can't arrive here
return null;
}
}
return ret;
}
/**
* Extract an EncodedStringValue[] from a given String.
*/
public static EncodedStringValue[] extract(String src) {
String[] values = src.split(";");
ArrayList<EncodedStringValue> list = new ArrayList<EncodedStringValue>();
for (int i = 0; i < values.length; i++) {
if (values[i].length() > 0) {
list.add(new EncodedStringValue(values[i]));
}
}
int len = list.size();
if (len > 0) {
return list.toArray(new EncodedStringValue[len]);
} else {
return null;
}
}
/**
* Concatenate an EncodedStringValue[] into a single String.
*/
public static String concat(EncodedStringValue[] addr) {
StringBuilder sb = new StringBuilder();
int maxIndex = addr.length - 1;
for (int i = 0; i <= maxIndex; i++) {
sb.append(addr[i].getString());
if (i < maxIndex) {
sb.append(";");
}
}
return sb.toString();
}
public static EncodedStringValue copy(EncodedStringValue value) {
if (value == null) {
return null;
}
return new EncodedStringValue(value.mCharacterSet, value.mData);
}
public static EncodedStringValue[] encodeStrings(String[] array) {
int count = array.length;
if (count > 0) {
EncodedStringValue[] encodedArray = new EncodedStringValue[count];
for (int i = 0; i < count; i++) {
encodedArray[i] = new EncodedStringValue(array[i]);
}
return encodedArray;
}
return null;
}
}
@@ -1,111 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class GenericPdu {
/**
* The headers of pdu.
*/
PduHeaders mPduHeaders = null;
/**
* Constructor.
*/
public GenericPdu() {
mPduHeaders = new PduHeaders();
}
/**
* Constructor.
*
* @param headers Headers for this PDU.
*/
GenericPdu(PduHeaders headers) {
mPduHeaders = headers;
}
/**
* Get the headers of this PDU.
*
* @return A PduHeaders of this PDU.
*/
PduHeaders getPduHeaders() {
return mPduHeaders;
}
/**
* Get X-Mms-Message-Type field value.
*
* @return the X-Mms-Report-Allowed value
*/
public int getMessageType() {
return mPduHeaders.getOctet(PduHeaders.MESSAGE_TYPE);
}
/**
* Set X-Mms-Message-Type field value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if field's value is not Octet.
*/
public void setMessageType(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.MESSAGE_TYPE);
}
/**
* Get X-Mms-MMS-Version field value.
*
* @return the X-Mms-MMS-Version value
*/
public int getMmsVersion() {
return mPduHeaders.getOctet(PduHeaders.MMS_VERSION);
}
/**
* Set X-Mms-MMS-Version field value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if field's value is not Octet.
*/
public void setMmsVersion(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.MMS_VERSION);
}
/**
* Get From value.
* From-value = Value-length
* (Address-present-token Encoded-string-value | Insert-address-token)
*
* @return the value
*/
public EncodedStringValue getFrom() {
return mPduHeaders.getEncodedStringValue(PduHeaders.FROM);
}
/**
* Set From value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setFrom(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM);
}
}
@@ -1,41 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* Thrown when an invalid header value was set.
*/
public class InvalidHeaderValueException extends MmsException {
private static final long serialVersionUID = -2053384496042052262L;
/**
* Constructs an InvalidHeaderValueException with no detailed message.
*/
public InvalidHeaderValueException() {
super();
}
/**
* Constructs an InvalidHeaderValueException with the specified detailed message.
*
* @param message the detailed message.
*/
public InvalidHeaderValueException(String message) {
super(message);
}
}
@@ -1,60 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* A generic exception that is thrown by the Mms client.
*/
public class MmsException extends Exception {
private static final long serialVersionUID = -7323249827281485390L;
/**
* Creates a new MmsException.
*/
public MmsException() {
super();
}
/**
* Creates a new MmsException with the specified detail message.
*
* @param message the detail message.
*/
public MmsException(String message) {
super(message);
}
/**
* Creates a new MmsException with the specified cause.
*
* @param cause the cause.
*/
public MmsException(Throwable cause) {
super(cause);
}
/**
* Creates a new MmsException with the specified detail message and cause.
*
* @param message the detail message.
* @param cause the cause.
*/
public MmsException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,148 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* Multimedia message PDU.
*/
public class MultimediaMessagePdu extends GenericPdu{
/**
* The body.
*/
private PduBody mMessageBody;
/**
* Constructor.
*/
public MultimediaMessagePdu() {
super();
}
/**
* Constructor.
*
* @param header the header of this PDU
* @param body the body of this PDU
*/
public MultimediaMessagePdu(PduHeaders header, PduBody body) {
super(header);
mMessageBody = body;
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
MultimediaMessagePdu(PduHeaders headers) {
super(headers);
}
/**
* Get body of the PDU.
*
* @return the body
*/
public PduBody getBody() {
return mMessageBody;
}
/**
* Set body of the PDU.
*
* @param body the body
*/
public void setBody(PduBody body) {
mMessageBody = body;
}
/**
* Get subject.
*
* @return the value
*/
public EncodedStringValue getSubject() {
return mPduHeaders.getEncodedStringValue(PduHeaders.SUBJECT);
}
/**
* Set subject.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setSubject(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.SUBJECT);
}
/**
* Get To value.
*
* @return the value
*/
public EncodedStringValue[] getTo() {
return mPduHeaders.getEncodedStringValues(PduHeaders.TO);
}
/**
* Add a "To" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void addTo(EncodedStringValue value) {
mPduHeaders.appendEncodedStringValue(value, PduHeaders.TO);
}
/**
* Get X-Mms-Priority value.
*
* @return the value
*/
public int getPriority() {
return mPduHeaders.getOctet(PduHeaders.PRIORITY);
}
/**
* Set X-Mms-Priority value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setPriority(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.PRIORITY);
}
/**
* Get Date value.
*
* @return the value
*/
public long getDate() {
return mPduHeaders.getLongInteger(PduHeaders.DATE);
}
/**
* Set Date value in seconds.
*
* @param value the value
*/
public void setDate(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.DATE);
}
}
@@ -1,283 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* M-Notification.ind PDU.
*/
public class NotificationInd extends GenericPdu {
/**
* Empty constructor.
* Since the Pdu corresponding to this class is constructed
* by the Proxy-Relay server, this class is only instantiated
* by the Pdu Parser.
*
* @throws InvalidHeaderValueException if error occurs.
* RuntimeException if an undeclared error occurs.
*/
public NotificationInd() throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
NotificationInd(PduHeaders headers) {
super(headers);
}
/**
* Get X-Mms-Content-Class Value.
*
* @return the value
*/
public int getContentClass() {
return mPduHeaders.getOctet(PduHeaders.CONTENT_CLASS);
}
/**
* Set X-Mms-Content-Class Value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if an undeclared error occurs.
*/
public void setContentClass(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.CONTENT_CLASS);
}
/**
* Get X-Mms-Content-Location value.
* When used in a PDU other than M-Mbox-Delete.conf and M-Delete.conf:
* Content-location-value = Uri-value
*
* @return the value
*/
public byte[] getContentLocation() {
return mPduHeaders.getTextString(PduHeaders.CONTENT_LOCATION);
}
/**
* Set X-Mms-Content-Location value.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setContentLocation(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.CONTENT_LOCATION);
}
/**
* Get X-Mms-Expiry value.
*
* Expiry-value = Value-length
* (Absolute-token Date-value | Relative-token Delta-seconds-value)
*
* @return the value
*/
public long getExpiry() {
return mPduHeaders.getLongInteger(PduHeaders.EXPIRY);
}
/**
* Set X-Mms-Expiry value.
*
* @param value the value
* @throws RuntimeException if an undeclared error occurs.
*/
public void setExpiry(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.EXPIRY);
}
/**
* Get From value.
* From-value = Value-length
* (Address-present-token Encoded-string-value | Insert-address-token)
*
* @return the value
*/
public EncodedStringValue getFrom() {
return mPduHeaders.getEncodedStringValue(PduHeaders.FROM);
}
/**
* Set From value.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setFrom(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM);
}
/**
* Get X-Mms-Message-Class value.
* Message-class-value = Class-identifier | Token-text
* Class-identifier = Personal | Advertisement | Informational | Auto
*
* @return the value
*/
public byte[] getMessageClass() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS);
}
/**
* Set X-Mms-Message-Class value.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setMessageClass(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS);
}
/**
* Get X-Mms-Message-Size value.
* Message-size-value = Long-integer
*
* @return the value
*/
public long getMessageSize() {
return mPduHeaders.getLongInteger(PduHeaders.MESSAGE_SIZE);
}
/**
* Set X-Mms-Message-Size value.
*
* @param value the value
* @throws RuntimeException if an undeclared error occurs.
*/
public void setMessageSize(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.MESSAGE_SIZE);
}
/**
* Get subject.
*
* @return the value
*/
public EncodedStringValue getSubject() {
return mPduHeaders.getEncodedStringValue(PduHeaders.SUBJECT);
}
/**
* Set subject.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setSubject(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.SUBJECT);
}
/**
* Get X-Mms-Transaction-Id.
*
* @return the value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
/**
* Get X-Mms-Delivery-Report Value.
*
* @return the value
*/
public int getDeliveryReport() {
return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT);
}
/**
* Set X-Mms-Delivery-Report Value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if an undeclared error occurs.
*/
public void setDeliveryReport(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT);
}
/*
* Optional, not supported header fields:
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte getDrmContent() {return 0x00;}
* public void setDrmContent(byte value) {}
*
* public byte getDistributionIndicator() {return 0x00;}
* public void setDistributionIndicator(byte value) {}
*
* public ElementDescriptorValue getElementDescriptor() {return null;}
* public void getElementDescriptor(ElementDescriptorValue value) {}
*
* public byte getPriority() {return 0x00;}
* public void setPriority(byte value) {}
*
* public byte getRecommendedRetrievalMode() {return 0x00;}
* public void setRecommendedRetrievalMode(byte value) {}
*
* public byte getRecommendedRetrievalModeText() {return 0x00;}
* public void setRecommendedRetrievalModeText(byte value) {}
*
* public byte[] getReplaceId() {return 0x00;}
* public void setReplaceId(byte[] value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*
* public byte getReplyCharging() {return 0x00;}
* public void setReplyCharging(byte value) {}
*
* public byte getReplyChargingDeadline() {return 0x00;}
* public void setReplyChargingDeadline(byte value) {}
*
* public byte[] getReplyChargingId() {return 0x00;}
* public void setReplyChargingId(byte[] value) {}
*
* public long getReplyChargingSize() {return 0;}
* public void setReplyChargingSize(long value) {}
*
* public byte getStored() {return 0x00;}
* public void setStored(byte value) {}
*/
}
@@ -1,112 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* M-NofifyResp.ind PDU.
*/
public class NotifyRespInd extends GenericPdu {
/**
* Constructor, used when composing a M-NotifyResp.ind pdu.
*
* @param mmsVersion current version of mms
* @param transactionId the transaction-id value
* @param status the status value
* @throws InvalidHeaderValueException if parameters are invalid.
* NullPointerException if transactionId is null.
* RuntimeException if an undeclared error occurs.
*/
public NotifyRespInd(int mmsVersion,
byte[] transactionId,
int status) throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_NOTIFYRESP_IND);
setMmsVersion(mmsVersion);
setTransactionId(transactionId);
setStatus(status);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
NotifyRespInd(PduHeaders headers) {
super(headers);
}
/**
* Get X-Mms-Report-Allowed field value.
*
* @return the X-Mms-Report-Allowed value
*/
public int getReportAllowed() {
return mPduHeaders.getOctet(PduHeaders.REPORT_ALLOWED);
}
/**
* Set X-Mms-Report-Allowed field value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if an undeclared error occurs.
*/
public void setReportAllowed(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.REPORT_ALLOWED);
}
/**
* Set X-Mms-Status field value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
* RuntimeException if an undeclared error occurs.
*/
public void setStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.STATUS);
}
/**
* GetX-Mms-Status field value.
*
* @return the X-Mms-Status value
*/
public int getStatus() {
return mPduHeaders.getOctet(PduHeaders.STATUS);
}
/**
* Get X-Mms-Transaction-Id field value.
*
* @return the X-Mms-Report-Allowed value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id field value.
*
* @param value the value
* @throws NullPointerException if the value is null.
* RuntimeException if an undeclared error occurs.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
}
-191
View File
@@ -1,191 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
public class PduBody {
private Vector<PduPart> mParts = null;
private Map<String, PduPart> mPartMapByContentId = null;
private Map<String, PduPart> mPartMapByContentLocation = null;
private Map<String, PduPart> mPartMapByName = null;
private Map<String, PduPart> mPartMapByFileName = null;
/**
* Constructor.
*/
public PduBody() {
mParts = new Vector<PduPart>();
mPartMapByContentId = new HashMap<String, PduPart>();
mPartMapByContentLocation = new HashMap<String, PduPart>();
mPartMapByName = new HashMap<String, PduPart>();
mPartMapByFileName = new HashMap<String, PduPart>();
}
private void putPartToMaps(PduPart part) {
// Put part to mPartMapByContentId.
byte[] contentId = part.getContentId();
if(null != contentId) {
mPartMapByContentId.put(new String(contentId), part);
}
// Put part to mPartMapByContentLocation.
byte[] contentLocation = part.getContentLocation();
if(null != contentLocation) {
String clc = new String(contentLocation);
mPartMapByContentLocation.put(clc, part);
}
// Put part to mPartMapByName.
byte[] name = part.getName();
if(null != name) {
String clc = new String(name);
mPartMapByName.put(clc, part);
}
// Put part to mPartMapByFileName.
byte[] fileName = part.getFilename();
if(null != fileName) {
String clc = new String(fileName);
mPartMapByFileName.put(clc, part);
}
}
/**
* Appends the specified part to the end of this body.
*
* @param part part to be appended
* @return true when success, false when fail
* @throws NullPointerException when part is null
*/
public boolean addPart(PduPart part) {
if(null == part) {
throw new NullPointerException();
}
putPartToMaps(part);
return mParts.add(part);
}
/**
* Inserts the specified part at the specified position.
*
* @param index index at which the specified part is to be inserted
* @param part part to be inserted
* @throws NullPointerException when part is null
*/
public void addPart(int index, PduPart part) {
if(null == part) {
throw new NullPointerException();
}
putPartToMaps(part);
mParts.add(index, part);
}
/**
* Removes the part at the specified position.
*
* @param index index of the part to return
* @return part at the specified index
*/
public PduPart removePart(int index) {
return mParts.remove(index);
}
/**
* Remove all of the parts.
*/
public void removeAll() {
mParts.clear();
}
/**
* Get the part at the specified position.
*
* @param index index of the part to return
* @return part at the specified index
*/
public PduPart getPart(int index) {
return mParts.get(index);
}
/**
* Get the index of the specified part.
*
* @param part the part object
* @return index the index of the first occurrence of the part in this body
*/
public int getPartIndex(PduPart part) {
return mParts.indexOf(part);
}
/**
* Get the number of parts.
*
* @return the number of parts
*/
public int getPartsNum() {
return mParts.size();
}
/**
* Get pdu part by content id.
*
* @param cid the value of content id.
* @return the pdu part.
*/
public PduPart getPartByContentId(String cid) {
return mPartMapByContentId.get(cid);
}
/**
* Get pdu part by Content-Location. Content-Location of part is
* the same as filename and name(param of content-type).
*
* @param contentLocation the content location.
* @return the pdu part.
*/
public PduPart getPartByContentLocation(String contentLocation) {
return mPartMapByContentLocation.get(contentLocation);
}
/**
* Get pdu part by name.
*
* @param name the value of filename.
* @return the pdu part.
*/
public PduPart getPartByName(String name) {
return mPartMapByName.get(name);
}
/**
* Get pdu part by filename.
*
* @param filename the value of filename.
* @return the pdu part.
*/
public PduPart getPartByFileName(String filename) {
return mPartMapByFileName.get(filename);
}
}
@@ -1,110 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class PduContentTypes {
/**
* All content types. From:
* http://www.openmobilealliance.org/tech/omna/omna-wsp-content-type.htm
*/
static final String[] contentTypes = {
"*/*", /* 0x00 */
"text/*", /* 0x01 */
"text/html", /* 0x02 */
"text/plain", /* 0x03 */
"text/x-hdml", /* 0x04 */
"text/x-ttml", /* 0x05 */
"text/x-vCalendar", /* 0x06 */
"text/x-vCard", /* 0x07 */
"text/vnd.wap.wml", /* 0x08 */
"text/vnd.wap.wmlscript", /* 0x09 */
"text/vnd.wap.wta-event", /* 0x0A */
"multipart/*", /* 0x0B */
"multipart/mixed", /* 0x0C */
"multipart/form-data", /* 0x0D */
"multipart/byterantes", /* 0x0E */
"multipart/alternative", /* 0x0F */
"application/*", /* 0x10 */
"application/java-vm", /* 0x11 */
"application/x-www-form-urlencoded", /* 0x12 */
"application/x-hdmlc", /* 0x13 */
"application/vnd.wap.wmlc", /* 0x14 */
"application/vnd.wap.wmlscriptc", /* 0x15 */
"application/vnd.wap.wta-eventc", /* 0x16 */
"application/vnd.wap.uaprof", /* 0x17 */
"application/vnd.wap.wtls-ca-certificate", /* 0x18 */
"application/vnd.wap.wtls-user-certificate", /* 0x19 */
"application/x-x509-ca-cert", /* 0x1A */
"application/x-x509-user-cert", /* 0x1B */
"image/*", /* 0x1C */
"image/gif", /* 0x1D */
"image/jpeg", /* 0x1E */
"image/tiff", /* 0x1F */
"image/png", /* 0x20 */
"image/vnd.wap.wbmp", /* 0x21 */
"application/vnd.wap.multipart.*", /* 0x22 */
"application/vnd.wap.multipart.mixed", /* 0x23 */
"application/vnd.wap.multipart.form-data", /* 0x24 */
"application/vnd.wap.multipart.byteranges", /* 0x25 */
"application/vnd.wap.multipart.alternative", /* 0x26 */
"application/xml", /* 0x27 */
"text/xml", /* 0x28 */
"application/vnd.wap.wbxml", /* 0x29 */
"application/x-x968-cross-cert", /* 0x2A */
"application/x-x968-ca-cert", /* 0x2B */
"application/x-x968-user-cert", /* 0x2C */
"text/vnd.wap.si", /* 0x2D */
"application/vnd.wap.sic", /* 0x2E */
"text/vnd.wap.sl", /* 0x2F */
"application/vnd.wap.slc", /* 0x30 */
"text/vnd.wap.co", /* 0x31 */
"application/vnd.wap.coc", /* 0x32 */
"application/vnd.wap.multipart.related", /* 0x33 */
"application/vnd.wap.sia", /* 0x34 */
"text/vnd.wap.connectivity-xml", /* 0x35 */
"application/vnd.wap.connectivity-wbxml", /* 0x36 */
"application/pkcs7-mime", /* 0x37 */
"application/vnd.wap.hashed-certificate", /* 0x38 */
"application/vnd.wap.signed-certificate", /* 0x39 */
"application/vnd.wap.cert-response", /* 0x3A */
"application/xhtml+xml", /* 0x3B */
"application/wml+xml", /* 0x3C */
"text/css", /* 0x3D */
"application/vnd.wap.mms-message", /* 0x3E */
"application/vnd.wap.rollover-certificate", /* 0x3F */
"application/vnd.wap.locc+wbxml", /* 0x40 */
"application/vnd.wap.loc+xml", /* 0x41 */
"application/vnd.syncml.dm+wbxml", /* 0x42 */
"application/vnd.syncml.dm+xml", /* 0x43 */
"application/vnd.syncml.notification", /* 0x44 */
"application/vnd.wap.xhtml+xml", /* 0x45 */
"application/vnd.wv.csp.cir", /* 0x46 */
"application/vnd.oma.dd+xml", /* 0x47 */
"application/vnd.oma.drm.message", /* 0x48 */
"application/vnd.oma.drm.content", /* 0x49 */
"application/vnd.oma.drm.rights+xml", /* 0x4A */
"application/vnd.oma.drm.rights+wbxml", /* 0x4B */
"application/vnd.wv.csp+xml", /* 0x4C */
"application/vnd.wv.csp+wbxml", /* 0x4D */
"application/vnd.syncml.ds.notification", /* 0x4E */
"audio/*", /* 0x4F */
"video/*", /* 0x50 */
"application/vnd.oma.dd2+xml", /* 0x51 */
"application/mikey" /* 0x52 */
};
}
@@ -1,719 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
import java.util.ArrayList;
import java.util.HashMap;
public class PduHeaders {
/**
* All pdu header fields.
*/
public static final int BCC = 0x81;
public static final int CC = 0x82;
public static final int CONTENT_LOCATION = 0x83;
public static final int CONTENT_TYPE = 0x84;
public static final int DATE = 0x85;
public static final int DELIVERY_REPORT = 0x86;
public static final int DELIVERY_TIME = 0x87;
public static final int EXPIRY = 0x88;
public static final int FROM = 0x89;
public static final int MESSAGE_CLASS = 0x8A;
public static final int MESSAGE_ID = 0x8B;
public static final int MESSAGE_TYPE = 0x8C;
public static final int MMS_VERSION = 0x8D;
public static final int MESSAGE_SIZE = 0x8E;
public static final int PRIORITY = 0x8F;
public static final int READ_REPLY = 0x90;
public static final int READ_REPORT = 0x90;
public static final int REPORT_ALLOWED = 0x91;
public static final int RESPONSE_STATUS = 0x92;
public static final int RESPONSE_TEXT = 0x93;
public static final int SENDER_VISIBILITY = 0x94;
public static final int STATUS = 0x95;
public static final int SUBJECT = 0x96;
public static final int TO = 0x97;
public static final int TRANSACTION_ID = 0x98;
public static final int RETRIEVE_STATUS = 0x99;
public static final int RETRIEVE_TEXT = 0x9A;
public static final int READ_STATUS = 0x9B;
public static final int REPLY_CHARGING = 0x9C;
public static final int REPLY_CHARGING_DEADLINE = 0x9D;
public static final int REPLY_CHARGING_ID = 0x9E;
public static final int REPLY_CHARGING_SIZE = 0x9F;
public static final int PREVIOUSLY_SENT_BY = 0xA0;
public static final int PREVIOUSLY_SENT_DATE = 0xA1;
public static final int STORE = 0xA2;
public static final int MM_STATE = 0xA3;
public static final int MM_FLAGS = 0xA4;
public static final int STORE_STATUS = 0xA5;
public static final int STORE_STATUS_TEXT = 0xA6;
public static final int STORED = 0xA7;
public static final int ATTRIBUTES = 0xA8;
public static final int TOTALS = 0xA9;
public static final int MBOX_TOTALS = 0xAA;
public static final int QUOTAS = 0xAB;
public static final int MBOX_QUOTAS = 0xAC;
public static final int MESSAGE_COUNT = 0xAD;
public static final int CONTENT = 0xAE;
public static final int START = 0xAF;
public static final int ADDITIONAL_HEADERS = 0xB0;
public static final int DISTRIBUTION_INDICATOR = 0xB1;
public static final int ELEMENT_DESCRIPTOR = 0xB2;
public static final int LIMIT = 0xB3;
public static final int RECOMMENDED_RETRIEVAL_MODE = 0xB4;
public static final int RECOMMENDED_RETRIEVAL_MODE_TEXT = 0xB5;
public static final int STATUS_TEXT = 0xB6;
public static final int APPLIC_ID = 0xB7;
public static final int REPLY_APPLIC_ID = 0xB8;
public static final int AUX_APPLIC_ID = 0xB9;
public static final int CONTENT_CLASS = 0xBA;
public static final int DRM_CONTENT = 0xBB;
public static final int ADAPTATION_ALLOWED = 0xBC;
public static final int REPLACE_ID = 0xBD;
public static final int CANCEL_ID = 0xBE;
public static final int CANCEL_STATUS = 0xBF;
/**
* X-Mms-Message-Type field types.
*/
public static final int MESSAGE_TYPE_SEND_REQ = 0x80;
public static final int MESSAGE_TYPE_SEND_CONF = 0x81;
public static final int MESSAGE_TYPE_NOTIFICATION_IND = 0x82;
public static final int MESSAGE_TYPE_NOTIFYRESP_IND = 0x83;
public static final int MESSAGE_TYPE_RETRIEVE_CONF = 0x84;
public static final int MESSAGE_TYPE_ACKNOWLEDGE_IND = 0x85;
public static final int MESSAGE_TYPE_DELIVERY_IND = 0x86;
public static final int MESSAGE_TYPE_READ_REC_IND = 0x87;
public static final int MESSAGE_TYPE_READ_ORIG_IND = 0x88;
public static final int MESSAGE_TYPE_FORWARD_REQ = 0x89;
public static final int MESSAGE_TYPE_FORWARD_CONF = 0x8A;
public static final int MESSAGE_TYPE_MBOX_STORE_REQ = 0x8B;
public static final int MESSAGE_TYPE_MBOX_STORE_CONF = 0x8C;
public static final int MESSAGE_TYPE_MBOX_VIEW_REQ = 0x8D;
public static final int MESSAGE_TYPE_MBOX_VIEW_CONF = 0x8E;
public static final int MESSAGE_TYPE_MBOX_UPLOAD_REQ = 0x8F;
public static final int MESSAGE_TYPE_MBOX_UPLOAD_CONF = 0x90;
public static final int MESSAGE_TYPE_MBOX_DELETE_REQ = 0x91;
public static final int MESSAGE_TYPE_MBOX_DELETE_CONF = 0x92;
public static final int MESSAGE_TYPE_MBOX_DESCR = 0x93;
public static final int MESSAGE_TYPE_DELETE_REQ = 0x94;
public static final int MESSAGE_TYPE_DELETE_CONF = 0x95;
public static final int MESSAGE_TYPE_CANCEL_REQ = 0x96;
public static final int MESSAGE_TYPE_CANCEL_CONF = 0x97;
/**
* X-Mms-Delivery-Report |
* X-Mms-Read-Report |
* X-Mms-Report-Allowed |
* X-Mms-Sender-Visibility |
* X-Mms-Store |
* X-Mms-Stored |
* X-Mms-Totals |
* X-Mms-Quotas |
* X-Mms-Distribution-Indicator |
* X-Mms-DRM-Content |
* X-Mms-Adaptation-Allowed |
* field types.
*/
public static final int VALUE_YES = 0x80;
public static final int VALUE_NO = 0x81;
/**
* Delivery-Time |
* Expiry and Reply-Charging-Deadline |
* field type components.
*/
public static final int VALUE_ABSOLUTE_TOKEN = 0x80;
public static final int VALUE_RELATIVE_TOKEN = 0x81;
/**
* X-Mms-MMS-Version field types.
*/
public static final int MMS_VERSION_1_3 = ((1 << 4) | 3);
public static final int MMS_VERSION_1_2 = ((1 << 4) | 2);
public static final int MMS_VERSION_1_1 = ((1 << 4) | 1);
public static final int MMS_VERSION_1_0 = ((1 << 4) | 0);
// Current version is 1.2.
public static final int CURRENT_MMS_VERSION = MMS_VERSION_1_2;
/**
* From field type components.
*/
public static final int FROM_ADDRESS_PRESENT_TOKEN = 0x80;
public static final int FROM_INSERT_ADDRESS_TOKEN = 0x81;
public static final String FROM_ADDRESS_PRESENT_TOKEN_STR = "address-present-token";
public static final String FROM_INSERT_ADDRESS_TOKEN_STR = "insert-address-token";
/**
* X-Mms-Status Field.
*/
public static final int STATUS_EXPIRED = 0x80;
public static final int STATUS_RETRIEVED = 0x81;
public static final int STATUS_REJECTED = 0x82;
public static final int STATUS_DEFERRED = 0x83;
public static final int STATUS_UNRECOGNIZED = 0x84;
public static final int STATUS_INDETERMINATE = 0x85;
public static final int STATUS_FORWARDED = 0x86;
public static final int STATUS_UNREACHABLE = 0x87;
/**
* MM-Flags field type components.
*/
public static final int MM_FLAGS_ADD_TOKEN = 0x80;
public static final int MM_FLAGS_REMOVE_TOKEN = 0x81;
public static final int MM_FLAGS_FILTER_TOKEN = 0x82;
/**
* X-Mms-Message-Class field types.
*/
public static final int MESSAGE_CLASS_PERSONAL = 0x80;
public static final int MESSAGE_CLASS_ADVERTISEMENT = 0x81;
public static final int MESSAGE_CLASS_INFORMATIONAL = 0x82;
public static final int MESSAGE_CLASS_AUTO = 0x83;
public static final String MESSAGE_CLASS_PERSONAL_STR = "personal";
public static final String MESSAGE_CLASS_ADVERTISEMENT_STR = "advertisement";
public static final String MESSAGE_CLASS_INFORMATIONAL_STR = "informational";
public static final String MESSAGE_CLASS_AUTO_STR = "auto";
/**
* X-Mms-Priority field types.
*/
public static final int PRIORITY_LOW = 0x80;
public static final int PRIORITY_NORMAL = 0x81;
public static final int PRIORITY_HIGH = 0x82;
/**
* X-Mms-Response-Status field types.
*/
public static final int RESPONSE_STATUS_OK = 0x80;
public static final int RESPONSE_STATUS_ERROR_UNSPECIFIED = 0x81;
public static final int RESPONSE_STATUS_ERROR_SERVICE_DENIED = 0x82;
public static final int RESPONSE_STATUS_ERROR_MESSAGE_FORMAT_CORRUPT = 0x83;
public static final int RESPONSE_STATUS_ERROR_SENDING_ADDRESS_UNRESOLVED = 0x84;
public static final int RESPONSE_STATUS_ERROR_MESSAGE_NOT_FOUND = 0x85;
public static final int RESPONSE_STATUS_ERROR_NETWORK_PROBLEM = 0x86;
public static final int RESPONSE_STATUS_ERROR_CONTENT_NOT_ACCEPTED = 0x87;
public static final int RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE = 0x88;
public static final int RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0;
public static final int RESPONSE_STATUS_ERROR_TRANSIENT_SENDNG_ADDRESS_UNRESOLVED = 0xC1;
public static final int RESPONSE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND = 0xC2;
public static final int RESPONSE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC3;
public static final int RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS = 0xC4;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT = 0xE2;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_SENDING_ADDRESS_UNRESOLVED = 0xE3;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE4;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_CONTENT_NOT_ACCEPTED = 0xE5;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_LIMITATIONS_NOT_MET = 0xE6;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_REQUEST_NOT_ACCEPTED = 0xE6;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_FORWARDING_DENIED = 0xE8;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_REPLY_CHARGING_NOT_SUPPORTED = 0xE9;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_ADDRESS_HIDING_NOT_SUPPORTED = 0xEA;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID = 0xEB;
public static final int RESPONSE_STATUS_ERROR_PERMANENT_END = 0xFF;
/**
* X-Mms-Retrieve-Status field types.
*/
public static final int RETRIEVE_STATUS_OK = 0x80;
public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0;
public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_MESSAGE_NOT_FOUND = 0xC1;
public static final int RETRIEVE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC2;
public static final int RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0;
public static final int RETRIEVE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1;
public static final int RETRIEVE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE2;
public static final int RETRIEVE_STATUS_ERROR_PERMANENT_CONTENT_UNSUPPORTED = 0xE3;
public static final int RETRIEVE_STATUS_ERROR_END = 0xFF;
/**
* X-Mms-Sender-Visibility field types.
*/
public static final int SENDER_VISIBILITY_HIDE = 0x80;
public static final int SENDER_VISIBILITY_SHOW = 0x81;
/**
* X-Mms-Read-Status field types.
*/
public static final int READ_STATUS_READ = 0x80;
public static final int READ_STATUS__DELETED_WITHOUT_BEING_READ = 0x81;
/**
* X-Mms-Cancel-Status field types.
*/
public static final int CANCEL_STATUS_REQUEST_SUCCESSFULLY_RECEIVED = 0x80;
public static final int CANCEL_STATUS_REQUEST_CORRUPTED = 0x81;
/**
* X-Mms-Reply-Charging field types.
*/
public static final int REPLY_CHARGING_REQUESTED = 0x80;
public static final int REPLY_CHARGING_REQUESTED_TEXT_ONLY = 0x81;
public static final int REPLY_CHARGING_ACCEPTED = 0x82;
public static final int REPLY_CHARGING_ACCEPTED_TEXT_ONLY = 0x83;
/**
* X-Mms-MM-State field types.
*/
public static final int MM_STATE_DRAFT = 0x80;
public static final int MM_STATE_SENT = 0x81;
public static final int MM_STATE_NEW = 0x82;
public static final int MM_STATE_RETRIEVED = 0x83;
public static final int MM_STATE_FORWARDED = 0x84;
/**
* X-Mms-Recommended-Retrieval-Mode field types.
*/
public static final int RECOMMENDED_RETRIEVAL_MODE_MANUAL = 0x80;
/**
* X-Mms-Content-Class field types.
*/
public static final int CONTENT_CLASS_TEXT = 0x80;
public static final int CONTENT_CLASS_IMAGE_BASIC = 0x81;
public static final int CONTENT_CLASS_IMAGE_RICH = 0x82;
public static final int CONTENT_CLASS_VIDEO_BASIC = 0x83;
public static final int CONTENT_CLASS_VIDEO_RICH = 0x84;
public static final int CONTENT_CLASS_MEGAPIXEL = 0x85;
public static final int CONTENT_CLASS_CONTENT_BASIC = 0x86;
public static final int CONTENT_CLASS_CONTENT_RICH = 0x87;
/**
* X-Mms-Store-Status field types.
*/
public static final int STORE_STATUS_SUCCESS = 0x80;
public static final int STORE_STATUS_ERROR_TRANSIENT_FAILURE = 0xC0;
public static final int STORE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM = 0xC1;
public static final int STORE_STATUS_ERROR_PERMANENT_FAILURE = 0xE0;
public static final int STORE_STATUS_ERROR_PERMANENT_SERVICE_DENIED = 0xE1;
public static final int STORE_STATUS_ERROR_PERMANENT_MESSAGE_FORMAT_CORRUPT = 0xE2;
public static final int STORE_STATUS_ERROR_PERMANENT_MESSAGE_NOT_FOUND = 0xE3;
public static final int STORE_STATUS_ERROR_PERMANENT_MMBOX_FULL = 0xE4;
public static final int STORE_STATUS_ERROR_END = 0xFF;
/**
* The map contains the value of all headers.
*/
private HashMap<Integer, Object> mHeaderMap = null;
/**
* Constructor of PduHeaders.
*/
public PduHeaders() {
mHeaderMap = new HashMap<Integer, Object>();
}
/**
* Get octet value by header field.
*
* @param field the field
* @return the octet value of the pdu header
* with specified header field. Return 0 if
* the value is not set.
*/
protected int getOctet(int field) {
Integer octet = (Integer) mHeaderMap.get(field);
if (null == octet) {
return 0;
}
return octet;
}
/**
* Set octet value to pdu header by header field.
*
* @param value the value
* @param field the field
* @throws InvalidHeaderValueException if the value is invalid.
*/
protected void setOctet(int value, int field)
throws InvalidHeaderValueException{
/**
* Check whether this field can be set for specific
* header and check validity of the field.
*/
switch (field) {
case REPORT_ALLOWED:
case ADAPTATION_ALLOWED:
case DELIVERY_REPORT:
case DRM_CONTENT:
case DISTRIBUTION_INDICATOR:
case QUOTAS:
case READ_REPORT:
case STORE:
case STORED:
case TOTALS:
case SENDER_VISIBILITY:
if ((VALUE_YES != value) && (VALUE_NO != value)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case READ_STATUS:
if ((READ_STATUS_READ != value) &&
(READ_STATUS__DELETED_WITHOUT_BEING_READ != value)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case CANCEL_STATUS:
if ((CANCEL_STATUS_REQUEST_SUCCESSFULLY_RECEIVED != value) &&
(CANCEL_STATUS_REQUEST_CORRUPTED != value)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case PRIORITY:
if ((value < PRIORITY_LOW) || (value > PRIORITY_HIGH)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case STATUS:
if ((value < STATUS_EXPIRED) || (value > STATUS_UNREACHABLE)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case REPLY_CHARGING:
if ((value < REPLY_CHARGING_REQUESTED)
|| (value > REPLY_CHARGING_ACCEPTED_TEXT_ONLY)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case MM_STATE:
if ((value < MM_STATE_DRAFT) || (value > MM_STATE_FORWARDED)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case RECOMMENDED_RETRIEVAL_MODE:
if (RECOMMENDED_RETRIEVAL_MODE_MANUAL != value) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case CONTENT_CLASS:
if ((value < CONTENT_CLASS_TEXT)
|| (value > CONTENT_CLASS_CONTENT_RICH)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
case RETRIEVE_STATUS:
// According to oma-ts-mms-enc-v1_3, section 7.3.50, we modify the invalid value.
if ((value > RETRIEVE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM) &&
(value < RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE)) {
value = RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE;
} else if ((value > RETRIEVE_STATUS_ERROR_PERMANENT_CONTENT_UNSUPPORTED) &&
(value <= RETRIEVE_STATUS_ERROR_END)) {
value = RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE;
} else if ((value < RETRIEVE_STATUS_OK) ||
((value > RETRIEVE_STATUS_OK) &&
(value < RETRIEVE_STATUS_ERROR_TRANSIENT_FAILURE)) ||
(value > RETRIEVE_STATUS_ERROR_END)) {
value = RETRIEVE_STATUS_ERROR_PERMANENT_FAILURE;
}
break;
case STORE_STATUS:
// According to oma-ts-mms-enc-v1_3, section 7.3.58, we modify the invalid value.
if ((value > STORE_STATUS_ERROR_TRANSIENT_NETWORK_PROBLEM) &&
(value < STORE_STATUS_ERROR_PERMANENT_FAILURE)) {
value = STORE_STATUS_ERROR_TRANSIENT_FAILURE;
} else if ((value > STORE_STATUS_ERROR_PERMANENT_MMBOX_FULL) &&
(value <= STORE_STATUS_ERROR_END)) {
value = STORE_STATUS_ERROR_PERMANENT_FAILURE;
} else if ((value < STORE_STATUS_SUCCESS) ||
((value > STORE_STATUS_SUCCESS) &&
(value < STORE_STATUS_ERROR_TRANSIENT_FAILURE)) ||
(value > STORE_STATUS_ERROR_END)) {
value = STORE_STATUS_ERROR_PERMANENT_FAILURE;
}
break;
case RESPONSE_STATUS:
// According to oma-ts-mms-enc-v1_3, section 7.3.48, we modify the invalid value.
if ((value > RESPONSE_STATUS_ERROR_TRANSIENT_PARTIAL_SUCCESS) &&
(value < RESPONSE_STATUS_ERROR_PERMANENT_FAILURE)) {
value = RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE;
} else if (((value > RESPONSE_STATUS_ERROR_PERMANENT_LACK_OF_PREPAID) &&
(value <= RESPONSE_STATUS_ERROR_PERMANENT_END)) ||
(value < RESPONSE_STATUS_OK) ||
((value > RESPONSE_STATUS_ERROR_UNSUPPORTED_MESSAGE) &&
(value < RESPONSE_STATUS_ERROR_TRANSIENT_FAILURE)) ||
(value > RESPONSE_STATUS_ERROR_PERMANENT_END)) {
value = RESPONSE_STATUS_ERROR_PERMANENT_FAILURE;
}
break;
case MMS_VERSION:
if ((value < MMS_VERSION_1_0)|| (value > MMS_VERSION_1_3)) {
value = CURRENT_MMS_VERSION; // Current version is the default value.
}
break;
case MESSAGE_TYPE:
if ((value < MESSAGE_TYPE_SEND_REQ) || (value > MESSAGE_TYPE_CANCEL_CONF)) {
// Invalid value.
throw new InvalidHeaderValueException("Invalid Octet value!");
}
break;
default:
// This header value should not be Octect.
throw new RuntimeException("Invalid header field!");
}
mHeaderMap.put(field, value);
}
/**
* Get TextString value by header field.
*
* @param field the field
* @return the TextString value of the pdu header
* with specified header field
*/
protected byte[] getTextString(int field) {
return (byte[]) mHeaderMap.get(field);
}
/**
* Set TextString value to pdu header by header field.
*
* @param value the value
* @param field the field
* @return the TextString value of the pdu header
* with specified header field
* @throws NullPointerException if the value is null.
*/
protected void setTextString(byte[] value, int field) {
/**
* Check whether this field can be set for specific
* header and check validity of the field.
*/
if (null == value) {
throw new NullPointerException();
}
switch (field) {
case TRANSACTION_ID:
case REPLY_CHARGING_ID:
case AUX_APPLIC_ID:
case APPLIC_ID:
case REPLY_APPLIC_ID:
case MESSAGE_ID:
case REPLACE_ID:
case CANCEL_ID:
case CONTENT_LOCATION:
case MESSAGE_CLASS:
case CONTENT_TYPE:
break;
default:
// This header value should not be Text-String.
throw new RuntimeException("Invalid header field!");
}
mHeaderMap.put(field, value);
}
/**
* Get EncodedStringValue value by header field.
*
* @param field the field
* @return the EncodedStringValue value of the pdu header
* with specified header field
*/
protected EncodedStringValue getEncodedStringValue(int field) {
return (EncodedStringValue) mHeaderMap.get(field);
}
/**
* Get TO, CC or BCC header value.
*
* @param field the field
* @return the EncodeStringValue array of the pdu header
* with specified header field
*/
protected EncodedStringValue[] getEncodedStringValues(int field) {
ArrayList<EncodedStringValue> list =
(ArrayList<EncodedStringValue>) mHeaderMap.get(field);
if (null == list) {
return null;
}
EncodedStringValue[] values = new EncodedStringValue[list.size()];
return list.toArray(values);
}
/**
* Set EncodedStringValue value to pdu header by header field.
*
* @param value the value
* @param field the field
* @return the EncodedStringValue value of the pdu header
* with specified header field
* @throws NullPointerException if the value is null.
*/
protected void setEncodedStringValue(EncodedStringValue value, int field) {
/**
* Check whether this field can be set for specific
* header and check validity of the field.
*/
if (null == value) {
throw new NullPointerException();
}
switch (field) {
case SUBJECT:
case RECOMMENDED_RETRIEVAL_MODE_TEXT:
case RETRIEVE_TEXT:
case STATUS_TEXT:
case STORE_STATUS_TEXT:
case RESPONSE_TEXT:
case FROM:
case PREVIOUSLY_SENT_BY:
case MM_FLAGS:
break;
default:
// This header value should not be Encoded-String-Value.
throw new RuntimeException("Invalid header field!");
}
mHeaderMap.put(field, value);
}
/**
* Set TO, CC or BCC header value.
*
* @param value the value
* @param field the field
* @return the EncodedStringValue value array of the pdu header
* with specified header field
* @throws NullPointerException if the value is null.
*/
protected void setEncodedStringValues(EncodedStringValue[] value, int field) {
/**
* Check whether this field can be set for specific
* header and check validity of the field.
*/
if (null == value) {
throw new NullPointerException();
}
switch (field) {
case BCC:
case CC:
case TO:
break;
default:
// This header value should not be Encoded-String-Value.
throw new RuntimeException("Invalid header field!");
}
ArrayList<EncodedStringValue> list = new ArrayList<EncodedStringValue>();
for (int i = 0; i < value.length; i++) {
list.add(value[i]);
}
mHeaderMap.put(field, list);
}
/**
* Append one EncodedStringValue to another.
*
* @param value the EncodedStringValue to append
* @param field the field
* @throws NullPointerException if the value is null.
*/
protected void appendEncodedStringValue(EncodedStringValue value,
int field) {
if (null == value) {
throw new NullPointerException();
}
switch (field) {
case BCC:
case CC:
case TO:
break;
default:
throw new RuntimeException("Invalid header field!");
}
ArrayList<EncodedStringValue> list =
(ArrayList<EncodedStringValue>) mHeaderMap.get(field);
if (null == list) {
list = new ArrayList<EncodedStringValue>();
}
list.add(value);
mHeaderMap.put(field, list);
}
/**
* Get LongInteger value by header field.
*
* @param field the field
* @return the LongInteger value of the pdu header
* with specified header field. if return -1, the
* field is not existed in pdu header.
*/
protected long getLongInteger(int field) {
Long longInteger = (Long) mHeaderMap.get(field);
if (null == longInteger) {
return -1;
}
return longInteger.longValue();
}
/**
* Set LongInteger value to pdu header by header field.
*
* @param value the value
* @param field the field
*/
protected void setLongInteger(long value, int field) {
/**
* Check whether this field can be set for specific
* header and check validity of the field.
*/
switch (field) {
case DATE:
case REPLY_CHARGING_SIZE:
case MESSAGE_SIZE:
case MESSAGE_COUNT:
case START:
case LIMIT:
case DELIVERY_TIME:
case EXPIRY:
case REPLY_CHARGING_DEADLINE:
case PREVIOUSLY_SENT_DATE:
break;
default:
// This header value should not be LongInteger.
throw new RuntimeException("Invalid header field!");
}
mHeaderMap.put(field, value);
}
}
File diff suppressed because it is too large Load Diff
-414
View File
@@ -1,414 +0,0 @@
/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 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 android.support.v7.mms.pdu;
import android.net.Uri;
import java.util.HashMap;
import java.util.Map;
/**
* The pdu part.
*/
public class PduPart {
/**
* Well-Known Parameters.
*/
public static final int P_Q = 0x80;
public static final int P_CHARSET = 0x81;
public static final int P_LEVEL = 0x82;
public static final int P_TYPE = 0x83;
public static final int P_DEP_NAME = 0x85;
public static final int P_DEP_FILENAME = 0x86;
public static final int P_DIFFERENCES = 0x87;
public static final int P_PADDING = 0x88;
// This value of "TYPE" s used with Content-Type: multipart/related
public static final int P_CT_MR_TYPE = 0x89;
public static final int P_DEP_START = 0x8A;
public static final int P_DEP_START_INFO = 0x8B;
public static final int P_DEP_COMMENT = 0x8C;
public static final int P_DEP_DOMAIN = 0x8D;
public static final int P_MAX_AGE = 0x8E;
public static final int P_DEP_PATH = 0x8F;
public static final int P_SECURE = 0x90;
public static final int P_SEC = 0x91;
public static final int P_MAC = 0x92;
public static final int P_CREATION_DATE = 0x93;
public static final int P_MODIFICATION_DATE = 0x94;
public static final int P_READ_DATE = 0x95;
public static final int P_SIZE = 0x96;
public static final int P_NAME = 0x97;
public static final int P_FILENAME = 0x98;
public static final int P_START = 0x99;
public static final int P_START_INFO = 0x9A;
public static final int P_COMMENT = 0x9B;
public static final int P_DOMAIN = 0x9C;
public static final int P_PATH = 0x9D;
/**
* Header field names.
*/
public static final int P_CONTENT_TYPE = 0x91;
public static final int P_CONTENT_LOCATION = 0x8E;
public static final int P_CONTENT_ID = 0xC0;
public static final int P_DEP_CONTENT_DISPOSITION = 0xAE;
public static final int P_CONTENT_DISPOSITION = 0xC5;
// The next header is unassigned header, use reserved header(0x48) value.
public static final int P_CONTENT_TRANSFER_ENCODING = 0xC8;
/**
* Content=Transfer-Encoding string.
*/
public static final String CONTENT_TRANSFER_ENCODING =
"Content-Transfer-Encoding";
/**
* Value of Content-Transfer-Encoding.
*/
public static final String P_BINARY = "binary";
public static final String P_7BIT = "7bit";
public static final String P_8BIT = "8bit";
public static final String P_BASE64 = "base64";
public static final String P_QUOTED_PRINTABLE = "quoted-printable";
/**
* Value of disposition can be set to PduPart when the value is octet in
* the PDU.
* "from-data" instead of Form-data<Octet 128>.
* "attachment" instead of Attachment<Octet 129>.
* "inline" instead of Inline<Octet 130>.
*/
static final byte[] DISPOSITION_FROM_DATA = "from-data".getBytes();
static final byte[] DISPOSITION_ATTACHMENT = "attachment".getBytes();
static final byte[] DISPOSITION_INLINE = "inline".getBytes();
/**
* Content-Disposition value.
*/
public static final int P_DISPOSITION_FROM_DATA = 0x80;
public static final int P_DISPOSITION_ATTACHMENT = 0x81;
public static final int P_DISPOSITION_INLINE = 0x82;
/**
* Header of part.
*/
private Map<Integer, Object> mPartHeader = null;
/**
* Data uri.
*/
private Uri mUri = null;
/**
* Part data.
*/
private byte[] mPartData = null;
private static final String TAG = "PduPart";
/**
* Empty Constructor.
*/
public PduPart() {
mPartHeader = new HashMap<Integer, Object>();
}
/**
* Set part data. The data are stored as byte array.
*
* @param data the data
*/
public void setData(byte[] data) {
if(data == null) {
return;
}
mPartData = new byte[data.length];
System.arraycopy(data, 0, mPartData, 0, data.length);
}
/**
* @return A copy of the part data or null if the data wasn't set or
* the data is stored as Uri.
* @see #getDataUri
*/
public byte[] getData() {
if(mPartData == null) {
return null;
}
byte[] byteArray = new byte[mPartData.length];
System.arraycopy(mPartData, 0, byteArray, 0, mPartData.length);
return byteArray;
}
/**
* @return The length of the data, if this object have data, else 0.
*/
public int getDataLength() {
if(mPartData != null){
return mPartData.length;
} else {
return 0;
}
}
/**
* Set data uri. The data are stored as Uri.
*
* @param uri the uri
*/
public void setDataUri(Uri uri) {
mUri = uri;
}
/**
* @return The Uri of the part data or null if the data wasn't set or
* the data is stored as byte array.
* @see #getData
*/
public Uri getDataUri() {
return mUri;
}
/**
* Set Content-id value
*
* @param contentId the content-id value
* @throws NullPointerException if the value is null.
*/
public void setContentId(byte[] contentId) {
if((contentId == null) || (contentId.length == 0)) {
throw new IllegalArgumentException(
"Content-Id may not be null or empty.");
}
if ((contentId.length > 1)
&& ((char) contentId[0] == '<')
&& ((char) contentId[contentId.length - 1] == '>')) {
mPartHeader.put(P_CONTENT_ID, contentId);
return;
}
// Insert beginning '<' and trailing '>' for Content-Id.
byte[] buffer = new byte[contentId.length + 2];
buffer[0] = (byte) (0xff & '<');
buffer[buffer.length - 1] = (byte) (0xff & '>');
System.arraycopy(contentId, 0, buffer, 1, contentId.length);
mPartHeader.put(P_CONTENT_ID, buffer);
}
/**
* Get Content-id value.
*
* @return the value
*/
public byte[] getContentId() {
return (byte[]) mPartHeader.get(P_CONTENT_ID);
}
/**
* Set Char-set value.
*
* @param charset the value
*/
public void setCharset(int charset) {
mPartHeader.put(P_CHARSET, charset);
}
/**
* Get Char-set value
*
* @return the charset value. Return 0 if charset was not set.
*/
public int getCharset() {
Integer charset = (Integer) mPartHeader.get(P_CHARSET);
if(charset == null) {
return 0;
} else {
return charset.intValue();
}
}
/**
* Set Content-Location value.
*
* @param contentLocation the value
* @throws NullPointerException if the value is null.
*/
public void setContentLocation(byte[] contentLocation) {
if(contentLocation == null) {
throw new NullPointerException("null content-location");
}
mPartHeader.put(P_CONTENT_LOCATION, contentLocation);
}
/**
* Get Content-Location value.
*
* @return the value
* return PduPart.disposition[0] instead of <Octet 128> (Form-data).
* return PduPart.disposition[1] instead of <Octet 129> (Attachment).
* return PduPart.disposition[2] instead of <Octet 130> (Inline).
*/
public byte[] getContentLocation() {
return (byte[]) mPartHeader.get(P_CONTENT_LOCATION);
}
/**
* Set Content-Disposition value.
* Use PduPart.disposition[0] instead of <Octet 128> (Form-data).
* Use PduPart.disposition[1] instead of <Octet 129> (Attachment).
* Use PduPart.disposition[2] instead of <Octet 130> (Inline).
*
* @param contentDisposition the value
* @throws NullPointerException if the value is null.
*/
public void setContentDisposition(byte[] contentDisposition) {
if(contentDisposition == null) {
throw new NullPointerException("null content-disposition");
}
mPartHeader.put(P_CONTENT_DISPOSITION, contentDisposition);
}
/**
* Get Content-Disposition value.
*
* @return the value
*/
public byte[] getContentDisposition() {
return (byte[]) mPartHeader.get(P_CONTENT_DISPOSITION);
}
/**
* Set Content-Type value.
*
* @param contentType the content type
* @throws NullPointerException if the value is null.
*/
public void setContentType(byte[] contentType) {
if(contentType == null) {
throw new NullPointerException("null content-type");
}
mPartHeader.put(P_CONTENT_TYPE, contentType);
}
/**
* Get Content-Type value of part.
*
* @return the value
*/
public byte[] getContentType() {
return (byte[]) mPartHeader.get(P_CONTENT_TYPE);
}
/**
* Set Content-Transfer-Encoding value
*
* @param contentTransferEncoding the Content-Transfer-Encoding value
* @throws NullPointerException if the value is null.
*/
public void setContentTransferEncoding(byte[] contentTransferEncoding) {
if(contentTransferEncoding == null) {
throw new NullPointerException("null content-transfer-encoding");
}
mPartHeader.put(P_CONTENT_TRANSFER_ENCODING, contentTransferEncoding);
}
/**
* Get Content-Transfer-Encoding value.
*
* @return the value
*/
public byte[] getContentTransferEncoding() {
return (byte[]) mPartHeader.get(P_CONTENT_TRANSFER_ENCODING);
}
/**
* Set Content-type parameter: name.
*
* @param name the name value
* @throws NullPointerException if the value is null.
*/
public void setName(byte[] name) {
if(null == name) {
throw new NullPointerException("null content-id");
}
mPartHeader.put(P_NAME, name);
}
/**
* Get content-type parameter: name.
*
* @return the name
*/
public byte[] getName() {
return (byte[]) mPartHeader.get(P_NAME);
}
/**
* Get Content-disposition parameter: filename
*
* @param fileName the filename value
* @throws NullPointerException if the value is null.
*/
public void setFilename(byte[] fileName) {
if(null == fileName) {
throw new NullPointerException("null content-id");
}
mPartHeader.put(P_FILENAME, fileName);
}
/**
* Set Content-disposition parameter: filename
*
* @return the filename
*/
public byte[] getFilename() {
return (byte[]) mPartHeader.get(P_FILENAME);
}
public String generateLocation() {
// Assumption: At least one of the content-location / name / filename
// or content-id should be set. This is guaranteed by the PduParser
// for incoming messages and by MM composer for outgoing messages.
byte[] location = (byte[]) mPartHeader.get(P_NAME);
if(null == location) {
location = (byte[]) mPartHeader.get(P_FILENAME);
if (null == location) {
location = (byte[]) mPartHeader.get(P_CONTENT_LOCATION);
}
}
if (null == location) {
byte[] contentId = (byte[]) mPartHeader.get(P_CONTENT_ID);
return "cid:" + new String(contentId);
} else {
return new String(location);
}
}
}
@@ -1,68 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
import java.io.ByteArrayOutputStream;
public class QuotedPrintable {
private static byte ESCAPE_CHAR = '=';
/**
* Decodes an array quoted-printable characters into an array of original bytes.
* Escaped characters are converted back to their original representation.
*
* <p>
* This function implements a subset of
* quoted-printable encoding specification (rule #1 and rule #2)
* as defined in RFC 1521.
* </p>
*
* @param bytes array of quoted-printable characters
* @return array of original bytes,
* null if quoted-printable decoding is unsuccessful.
*/
public static final byte[] decodeQuotedPrintable(byte[] bytes) {
if (bytes == null) {
return null;
}
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
for (int i = 0; i < bytes.length; i++) {
int b = bytes[i];
if (b == ESCAPE_CHAR) {
try {
if('\r' == (char)bytes[i + 1] &&
'\n' == (char)bytes[i + 2]) {
i += 2;
continue;
}
int u = Character.digit((char) bytes[++i], 16);
int l = Character.digit((char) bytes[++i], 16);
if (u == -1 || l == -1) {
return null;
}
buffer.write((char) ((u << 4) + l));
} catch (ArrayIndexOutOfBoundsException e) {
return null;
}
} else {
buffer.write(b);
}
}
return buffer.toByteArray();
}
}
@@ -1,151 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class ReadOrigInd extends GenericPdu {
/**
* Empty constructor.
* Since the Pdu corresponding to this class is constructed
* by the Proxy-Relay server, this class is only instantiated
* by the Pdu Parser.
*
* @throws InvalidHeaderValueException if error occurs.
*/
public ReadOrigInd() throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_READ_ORIG_IND);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
ReadOrigInd(PduHeaders headers) {
super(headers);
}
/**
* Get Date value.
*
* @return the value
*/
public long getDate() {
return mPduHeaders.getLongInteger(PduHeaders.DATE);
}
/**
* Set Date value.
*
* @param value the value
*/
public void setDate(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.DATE);
}
/**
* Get From value.
* From-value = Value-length
* (Address-present-token Encoded-string-value | Insert-address-token)
*
* @return the value
*/
public EncodedStringValue getFrom() {
return mPduHeaders.getEncodedStringValue(PduHeaders.FROM);
}
/**
* Set From value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setFrom(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM);
}
/**
* Get Message-ID value.
*
* @return the value
*/
public byte[] getMessageId() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID);
}
/**
* Set Message-ID value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID);
}
/**
* Get X-MMS-Read-status value.
*
* @return the value
*/
public int getReadStatus() {
return mPduHeaders.getOctet(PduHeaders.READ_STATUS);
}
/**
* Set X-MMS-Read-status value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setReadStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.READ_STATUS);
}
/**
* Get To value.
*
* @return the value
*/
public EncodedStringValue[] getTo() {
return mPduHeaders.getEncodedStringValues(PduHeaders.TO);
}
/**
* Set To value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTo(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.TO);
}
/*
* Optional, not supported header fields:
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*/
}
@@ -1,142 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class ReadRecInd extends GenericPdu {
/**
* Constructor, used when composing a M-ReadRec.ind pdu.
*
* @param from the from value
* @param messageId the message ID value
* @param mmsVersion current viersion of mms
* @param readStatus the read status value
* @param to the to value
* @throws InvalidHeaderValueException if parameters are invalid.
* NullPointerException if messageId or to is null.
*/
public ReadRecInd(EncodedStringValue from,
byte[] messageId,
int mmsVersion,
int readStatus,
EncodedStringValue[] to) throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_READ_REC_IND);
setFrom(from);
setMessageId(messageId);
setMmsVersion(mmsVersion);
setTo(to);
setReadStatus(readStatus);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
ReadRecInd(PduHeaders headers) {
super(headers);
}
/**
* Get Date value.
*
* @return the value
*/
public long getDate() {
return mPduHeaders.getLongInteger(PduHeaders.DATE);
}
/**
* Set Date value.
*
* @param value the value
*/
public void setDate(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.DATE);
}
/**
* Get Message-ID value.
*
* @return the value
*/
public byte[] getMessageId() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID);
}
/**
* Set Message-ID value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID);
}
/**
* Get To value.
*
* @return the value
*/
public EncodedStringValue[] getTo() {
return mPduHeaders.getEncodedStringValues(PduHeaders.TO);
}
/**
* Set To value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTo(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.TO);
}
/**
* Get X-MMS-Read-status value.
*
* @return the value
*/
public int getReadStatus() {
return mPduHeaders.getOctet(PduHeaders.READ_STATUS);
}
/**
* Set X-MMS-Read-status value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setReadStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.READ_STATUS);
}
/*
* Optional, not supported header fields:
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*/
}
@@ -1,298 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
/**
* M-Retrive.conf Pdu.
*/
public class RetrieveConf extends MultimediaMessagePdu {
/**
* Empty constructor.
* Since the Pdu corresponding to this class is constructed
* by the Proxy-Relay server, this class is only instantiated
* by the Pdu Parser.
*
* @throws InvalidHeaderValueException if error occurs.
*/
public RetrieveConf() throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
RetrieveConf(PduHeaders headers) {
super(headers);
}
/**
* Constructor with given headers and body
*
* @param headers Headers for this PDU.
* @param body Body of this PDu.
*/
RetrieveConf(PduHeaders headers, PduBody body) {
super(headers, body);
}
/**
* Get CC value.
*
* @return the value
*/
public EncodedStringValue[] getCc() {
return mPduHeaders.getEncodedStringValues(PduHeaders.CC);
}
/**
* Add a "CC" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void addCc(EncodedStringValue value) {
mPduHeaders.appendEncodedStringValue(value, PduHeaders.CC);
}
/**
* Get Content-type value.
*
* @return the value
*/
public byte[] getContentType() {
return mPduHeaders.getTextString(PduHeaders.CONTENT_TYPE);
}
/**
* Set Content-type value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setContentType(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.CONTENT_TYPE);
}
/**
* Get X-Mms-Delivery-Report value.
*
* @return the value
*/
public int getDeliveryReport() {
return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT);
}
/**
* Set X-Mms-Delivery-Report value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setDeliveryReport(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT);
}
/**
* Get From value.
* From-value = Value-length
* (Address-present-token Encoded-string-value | Insert-address-token)
*
* @return the value
*/
public EncodedStringValue getFrom() {
return mPduHeaders.getEncodedStringValue(PduHeaders.FROM);
}
/**
* Set From value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setFrom(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.FROM);
}
/**
* Get X-Mms-Message-Class value.
* Message-class-value = Class-identifier | Token-text
* Class-identifier = Personal | Advertisement | Informational | Auto
*
* @return the value
*/
public byte[] getMessageClass() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS);
}
/**
* Set X-Mms-Message-Class value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageClass(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS);
}
/**
* Get Message-ID value.
*
* @return the value
*/
public byte[] getMessageId() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID);
}
/**
* Set Message-ID value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID);
}
/**
* Get X-Mms-Read-Report value.
*
* @return the value
*/
public int getReadReport() {
return mPduHeaders.getOctet(PduHeaders.READ_REPORT);
}
/**
* Set X-Mms-Read-Report value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setReadReport(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.READ_REPORT);
}
/**
* Get X-Mms-Retrieve-Status value.
*
* @return the value
*/
public int getRetrieveStatus() {
return mPduHeaders.getOctet(PduHeaders.RETRIEVE_STATUS);
}
/**
* Set X-Mms-Retrieve-Status value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setRetrieveStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.RETRIEVE_STATUS);
}
/**
* Get X-Mms-Retrieve-Text value.
*
* @return the value
*/
public EncodedStringValue getRetrieveText() {
return mPduHeaders.getEncodedStringValue(PduHeaders.RETRIEVE_TEXT);
}
/**
* Set X-Mms-Retrieve-Text value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setRetrieveText(EncodedStringValue value) {
mPduHeaders.setEncodedStringValue(value, PduHeaders.RETRIEVE_TEXT);
}
/**
* Get X-Mms-Transaction-Id.
*
* @return the value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
/*
* Optional, not supported header fields:
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte getContentClass() {return 0x00;}
* public void setApplicId(byte value) {}
*
* public byte getDrmContent() {return 0x00;}
* public void setDrmContent(byte value) {}
*
* public byte getDistributionIndicator() {return 0x00;}
* public void setDistributionIndicator(byte value) {}
*
* public PreviouslySentByValue getPreviouslySentBy() {return null;}
* public void setPreviouslySentBy(PreviouslySentByValue value) {}
*
* public PreviouslySentDateValue getPreviouslySentDate() {}
* public void setPreviouslySentDate(PreviouslySentDateValue value) {}
*
* public MmFlagsValue getMmFlags() {return null;}
* public void setMmFlags(MmFlagsValue value) {}
*
* public MmStateValue getMmState() {return null;}
* public void getMmState(MmStateValue value) {}
*
* public byte[] getReplaceId() {return 0x00;}
* public void setReplaceId(byte[] value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*
* public byte getReplyCharging() {return 0x00;}
* public void setReplyCharging(byte value) {}
*
* public byte getReplyChargingDeadline() {return 0x00;}
* public void setReplyChargingDeadline(byte value) {}
*
* public byte[] getReplyChargingId() {return 0x00;}
* public void setReplyChargingId(byte[] value) {}
*
* public long getReplyChargingSize() {return 0;}
* public void setReplyChargingSize(long value) {}
*/
}
@@ -1,115 +0,0 @@
/*
* Copyright (C) 2007 Esmertec AG.
* Copyright (C) 2007 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 android.support.v7.mms.pdu;
public class SendConf extends GenericPdu {
/**
* Empty constructor.
* Since the Pdu corresponding to this class is constructed
* by the Proxy-Relay server, this class is only instantiated
* by the Pdu Parser.
*
* @throws InvalidHeaderValueException if error occurs.
*/
public SendConf() throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_SEND_CONF);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
SendConf(PduHeaders headers) {
super(headers);
}
/**
* Get Message-ID value.
*
* @return the value
*/
public byte[] getMessageId() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_ID);
}
/**
* Set Message-ID value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_ID);
}
/**
* Get X-Mms-Response-Status.
*
* @return the value
*/
public int getResponseStatus() {
return mPduHeaders.getOctet(PduHeaders.RESPONSE_STATUS);
}
/**
* Set X-Mms-Response-Status.
*
* @param value the values
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setResponseStatus(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.RESPONSE_STATUS);
}
/**
* Get X-Mms-Transaction-Id field value.
*
* @return the X-Mms-Report-Allowed value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id field value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
/*
* Optional, not supported header fields:
*
* public byte[] getContentLocation() {return null;}
* public void setContentLocation(byte[] value) {}
*
* public EncodedStringValue getResponseText() {return null;}
* public void setResponseText(EncodedStringValue value) {}
*
* public byte getStoreStatus() {return 0x00;}
* public void setStoreStatus(byte value) {}
*
* public byte[] getStoreStatusText() {return null;}
* public void setStoreStatusText(byte[] value) {}
*/
}
-343
View File
@@ -1,343 +0,0 @@
/*
* Copyright (C) 2007-2008 Esmertec AG.
* Copyright (C) 2007-2008 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 android.support.v7.mms.pdu;
import android.util.Log;
public class SendReq extends MultimediaMessagePdu {
private static final String TAG = "SendReq";
public SendReq() {
super();
try {
setMessageType(PduHeaders.MESSAGE_TYPE_SEND_REQ);
setMmsVersion(PduHeaders.CURRENT_MMS_VERSION);
// FIXME: Content-type must be decided according to whether
// SMIL part present.
setContentType("application/vnd.wap.multipart.related".getBytes());
setFrom(new EncodedStringValue(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR.getBytes()));
setTransactionId(generateTransactionId());
} catch (InvalidHeaderValueException e) {
// Impossible to reach here since all headers we set above are valid.
Log.e(TAG, "Unexpected InvalidHeaderValueException.", e);
throw new RuntimeException(e);
}
}
private byte[] generateTransactionId() {
String transactionId = "T" + Long.toHexString(System.currentTimeMillis());
return transactionId.getBytes();
}
/**
* Constructor, used when composing a M-Send.req pdu.
*
* @param contentType the content type value
* @param from the from value
* @param mmsVersion current viersion of mms
* @param transactionId the transaction-id value
* @throws InvalidHeaderValueException if parameters are invalid.
* NullPointerException if contentType, form or transactionId is null.
*/
public SendReq(byte[] contentType,
EncodedStringValue from,
int mmsVersion,
byte[] transactionId) throws InvalidHeaderValueException {
super();
setMessageType(PduHeaders.MESSAGE_TYPE_SEND_REQ);
setContentType(contentType);
setFrom(from);
setMmsVersion(mmsVersion);
setTransactionId(transactionId);
}
/**
* Constructor with given headers.
*
* @param headers Headers for this PDU.
*/
SendReq(PduHeaders headers) {
super(headers);
}
/**
* Constructor with given headers and body
*
* @param headers Headers for this PDU.
* @param body Body of this PDu.
*/
SendReq(PduHeaders headers, PduBody body) {
super(headers, body);
}
/**
* Get Bcc value.
*
* @return the value
*/
public EncodedStringValue[] getBcc() {
return mPduHeaders.getEncodedStringValues(PduHeaders.BCC);
}
/**
* Add a "BCC" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void addBcc(EncodedStringValue value) {
mPduHeaders.appendEncodedStringValue(value, PduHeaders.BCC);
}
/**
* Set "BCC" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setBcc(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.BCC);
}
/**
* Get CC value.
*
* @return the value
*/
public EncodedStringValue[] getCc() {
return mPduHeaders.getEncodedStringValues(PduHeaders.CC);
}
/**
* Add a "CC" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void addCc(EncodedStringValue value) {
mPduHeaders.appendEncodedStringValue(value, PduHeaders.CC);
}
/**
* Set "CC" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setCc(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.CC);
}
/**
* Get Content-type value.
*
* @return the value
*/
public byte[] getContentType() {
return mPduHeaders.getTextString(PduHeaders.CONTENT_TYPE);
}
/**
* Set Content-type value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setContentType(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.CONTENT_TYPE);
}
/**
* Get X-Mms-Delivery-Report value.
*
* @return the value
*/
public int getDeliveryReport() {
return mPduHeaders.getOctet(PduHeaders.DELIVERY_REPORT);
}
/**
* Set X-Mms-Delivery-Report value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setDeliveryReport(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.DELIVERY_REPORT);
}
/**
* Get X-Mms-Expiry value.
*
* Expiry-value = Value-length
* (Absolute-token Date-value | Relative-token Delta-seconds-value)
*
* @return the value
*/
public long getExpiry() {
return mPduHeaders.getLongInteger(PduHeaders.EXPIRY);
}
/**
* Set X-Mms-Expiry value.
*
* @param value the value
*/
public void setExpiry(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.EXPIRY);
}
/**
* Get X-Mms-MessageSize value.
*
* Expiry-value = size of message
*
* @return the value
*/
public long getMessageSize() {
return mPduHeaders.getLongInteger(PduHeaders.MESSAGE_SIZE);
}
/**
* Set X-Mms-MessageSize value.
*
* @param value the value
*/
public void setMessageSize(long value) {
mPduHeaders.setLongInteger(value, PduHeaders.MESSAGE_SIZE);
}
/**
* Get X-Mms-Message-Class value.
* Message-class-value = Class-identifier | Token-text
* Class-identifier = Personal | Advertisement | Informational | Auto
*
* @return the value
*/
public byte[] getMessageClass() {
return mPduHeaders.getTextString(PduHeaders.MESSAGE_CLASS);
}
/**
* Set X-Mms-Message-Class value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setMessageClass(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.MESSAGE_CLASS);
}
/**
* Get X-Mms-Read-Report value.
*
* @return the value
*/
public int getReadReport() {
return mPduHeaders.getOctet(PduHeaders.READ_REPORT);
}
/**
* Set X-Mms-Read-Report value.
*
* @param value the value
* @throws InvalidHeaderValueException if the value is invalid.
*/
public void setReadReport(int value) throws InvalidHeaderValueException {
mPduHeaders.setOctet(value, PduHeaders.READ_REPORT);
}
/**
* Set "To" value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTo(EncodedStringValue[] value) {
mPduHeaders.setEncodedStringValues(value, PduHeaders.TO);
}
/**
* Get X-Mms-Transaction-Id field value.
*
* @return the X-Mms-Report-Allowed value
*/
public byte[] getTransactionId() {
return mPduHeaders.getTextString(PduHeaders.TRANSACTION_ID);
}
/**
* Set X-Mms-Transaction-Id field value.
*
* @param value the value
* @throws NullPointerException if the value is null.
*/
public void setTransactionId(byte[] value) {
mPduHeaders.setTextString(value, PduHeaders.TRANSACTION_ID);
}
/*
* Optional, not supported header fields:
*
* public byte getAdaptationAllowed() {return 0};
* public void setAdaptationAllowed(btye value) {};
*
* public byte[] getApplicId() {return null;}
* public void setApplicId(byte[] value) {}
*
* public byte[] getAuxApplicId() {return null;}
* public void getAuxApplicId(byte[] value) {}
*
* public byte getContentClass() {return 0x00;}
* public void setApplicId(byte value) {}
*
* public long getDeliveryTime() {return 0};
* public void setDeliveryTime(long value) {};
*
* public byte getDrmContent() {return 0x00;}
* public void setDrmContent(byte value) {}
*
* public MmFlagsValue getMmFlags() {return null;}
* public void setMmFlags(MmFlagsValue value) {}
*
* public MmStateValue getMmState() {return null;}
* public void getMmState(MmStateValue value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*
* public byte getReplyCharging() {return 0x00;}
* public void setReplyCharging(byte value) {}
*
* public byte getReplyChargingDeadline() {return 0x00;}
* public void setReplyChargingDeadline(byte value) {}
*
* public byte[] getReplyChargingId() {return 0x00;}
* public void setReplyChargingId(byte[] value) {}
*
* public long getReplyChargingSize() {return 0;}
* public void setReplyChargingSize(long value) {}
*
* public byte[] getReplyApplicId() {return 0x00;}
* public void setReplyApplicId(byte[] value) {}
*
* public byte getStore() {return 0x00;}
* public void setStore(byte value) {}
*/
}
@@ -1,262 +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;
import android.app.Application;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.mms.CarrierConfigValuesLoader;
import android.support.v7.mms.MmsManager;
import android.telephony.CarrierConfigManager;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.receiver.SmsReceiver;
import com.android.messaging.sms.ApnDatabase;
import com.android.messaging.sms.BugleApnSettingsLoader;
import com.android.messaging.sms.BugleUserAgentInfoLoader;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.Trace;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
/**
* The application object
*/
public class BugleApplication extends Application implements UncaughtExceptionHandler {
private static final String TAG = LogUtil.BUGLE_TAG;
private UncaughtExceptionHandler sSystemUncaughtExceptionHandler;
private static boolean sRunningTests = false;
@VisibleForTesting
protected static void setTestsRunning() {
sRunningTests = true;
}
/**
* @return true if we're running unit tests.
*/
public static boolean isRunningTests() {
return sRunningTests;
}
@Override
public void onCreate() {
Trace.beginSection("app.onCreate");
super.onCreate();
// Note onCreate is called in both test and real application environments
if (!sRunningTests) {
// Only create the factory if not running tests
FactoryImpl.register(getApplicationContext(), this);
} else {
LogUtil.e(TAG, "BugleApplication.onCreate: FactoryImpl.register skipped for test run");
}
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
Trace.endSection();
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Update conversation drawables when changing writing systems
// (Right-To-Left / Left-To-Right)
ConversationDrawables.get().updateDrawables();
}
// Called by the "real" factory from FactoryImpl.register() (i.e. not run in tests)
public void initializeSync(final Factory factory) {
Trace.beginSection("app.initializeSync");
final Context context = factory.getApplicationContext();
final BugleGservices bugleGservices = factory.getBugleGservices();
final BuglePrefs buglePrefs = factory.getApplicationPrefs();
final DataModel dataModel = factory.getDataModel();
final CarrierConfigValuesLoader carrierConfigValuesLoader =
factory.getCarrierConfigValuesLoader();
maybeStartProfiling();
BugleApplication.updateAppConfig(context);
// Initialize MMS lib
initMmsLib(context, bugleGservices, carrierConfigValuesLoader);
// Initialize APN database
ApnDatabase.initializeAppContext(context);
// Fixup messages in flight if we crashed and send any pending
dataModel.onApplicationCreated();
// Register carrier config change receiver
if (OsUtil.isAtLeastM()) {
registerCarrierConfigChangeReceiver(context);
}
Trace.endSection();
}
private static void registerCarrierConfigChangeReceiver(final Context context) {
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LogUtil.i(TAG, "Carrier config changed. Reloading MMS config.");
MmsConfig.loadAsync();
}
}, new IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
}
private static void initMmsLib(final Context context, final BugleGservices bugleGservices,
final CarrierConfigValuesLoader carrierConfigValuesLoader) {
MmsManager.setApnSettingsLoader(new BugleApnSettingsLoader(context));
MmsManager.setCarrierConfigValuesLoader(carrierConfigValuesLoader);
MmsManager.setUserAgentInfoLoader(new BugleUserAgentInfoLoader(context));
MmsManager.setUseWakeLock(true);
// If Gservices is configured not to use mms api, force MmsManager to always use
// legacy mms sending logic
MmsManager.setForceLegacyMms(!bugleGservices.getBoolean(
BugleGservicesKeys.USE_MMS_API_IF_PRESENT,
BugleGservicesKeys.USE_MMS_API_IF_PRESENT_DEFAULT));
bugleGservices.registerForChanges(new Runnable() {
@Override
public void run() {
MmsManager.setForceLegacyMms(!bugleGservices.getBoolean(
BugleGservicesKeys.USE_MMS_API_IF_PRESENT,
BugleGservicesKeys.USE_MMS_API_IF_PRESENT_DEFAULT));
}
});
}
public static void updateAppConfig(final Context context) {
// Make sure we set the correct state for the SMS/MMS receivers
SmsReceiver.updateSmsReceiveHandler(context);
}
// Called from thread started in FactoryImpl.register() (i.e. not run in tests)
public void initializeAsync(final Factory factory) {
// Handle shared prefs upgrade & Load MMS Configuration
Trace.beginSection("app.initializeAsync");
maybeHandleSharedPrefsUpgrade(factory);
MmsConfig.load();
Trace.endSection();
}
@Override
public void onLowMemory() {
super.onLowMemory();
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "BugleApplication.onLowMemory");
}
Factory.get().reclaimMemory();
}
@Override
public void uncaughtException(final Thread thread, final Throwable ex) {
final boolean background = getMainLooper().getThread() != thread;
if (background) {
LogUtil.e(TAG, "Uncaught exception in background thread " + thread, ex);
final Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
sSystemUncaughtExceptionHandler.uncaughtException(thread, ex);
}
});
} else {
sSystemUncaughtExceptionHandler.uncaughtException(thread, ex);
}
}
private void maybeStartProfiling() {
// App startup profiling support. To use it:
// adb shell setprop log.tag.BugleProfile DEBUG
// # Start the app, wait for a 30s, download trace file:
// adb pull /data/data/com.android.messaging/cache/startup.trace /tmp
// # Open trace file (using adt/tools/traceview)
if (android.util.Log.isLoggable(LogUtil.PROFILE_TAG, android.util.Log.DEBUG)) {
// Start method tracing with a big enough buffer and let it run for 30s.
// Note we use a logging tag as we don't want to wait for gservices to start up.
final File file = DebugUtils.getDebugFile("startup.trace", true);
if (file != null) {
android.os.Debug.startMethodTracing(file.getAbsolutePath(), 160 * 1024 * 1024);
new Handler(Looper.getMainLooper()).postDelayed(
new Runnable() {
@Override
public void run() {
android.os.Debug.stopMethodTracing();
// Allow world to see trace file
DebugUtils.ensureReadable(file);
LogUtil.d(LogUtil.PROFILE_TAG, "Tracing complete - "
+ file.getAbsolutePath());
}
}, 30000);
}
}
}
private void maybeHandleSharedPrefsUpgrade(final Factory factory) {
final int existingVersion = factory.getApplicationPrefs().getInt(
BuglePrefsKeys.SHARED_PREFERENCES_VERSION,
BuglePrefsKeys.SHARED_PREFERENCES_VERSION_DEFAULT);
final int targetVersion = Integer.parseInt(getString(R.string.pref_version));
if (targetVersion > existingVersion) {
LogUtil.i(LogUtil.BUGLE_TAG, "Upgrading shared prefs from " + existingVersion +
" to " + targetVersion);
try {
// Perform upgrade on application-wide prefs.
factory.getApplicationPrefs().onUpgrade(existingVersion, targetVersion);
// Perform upgrade on each subscription's prefs.
PhoneUtils.forEachActiveSubscription(new PhoneUtils.SubscriptionRunnable() {
@Override
public void runForSubscription(final int subId) {
factory.getSubscriptionPrefs(subId)
.onUpgrade(existingVersion, targetVersion);
}
});
factory.getApplicationPrefs().putInt(BuglePrefsKeys.SHARED_PREFERENCES_VERSION,
targetVersion);
} catch (final Exception ex) {
// Upgrade failed. Don't crash the app because we can always fall back to the
// default settings.
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to upgrade shared prefs", ex);
}
} else if (targetVersion < existingVersion) {
// We don't care about downgrade since real user shouldn't encounter this, so log it
// and ignore any prefs migration.
LogUtil.e(LogUtil.BUGLE_TAG, "Shared prefs downgrade requested and ignored. " +
"oldVersion = " + existingVersion + ", newVersion = " + targetVersion);
}
}
}
-75
View File
@@ -1,75 +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;
import android.content.Context;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.MemoryCacheManager;
import com.android.messaging.datamodel.ParticipantRefresh.ContactContentObserver;
import com.android.messaging.datamodel.media.MediaCacheManager;
import com.android.messaging.datamodel.media.MediaResourceManager;
import com.android.messaging.sms.BugleCarrierConfigValuesLoader;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.PhoneUtils;
import com.google.common.annotations.VisibleForTesting;
public abstract class Factory {
// Making this volatile because on the unit tests, setInstance is called from a unit test
// thread, and then it's read on the UI thread.
private static volatile Factory sInstance;
@VisibleForTesting
protected static boolean sRegistered;
@VisibleForTesting
protected static boolean sInitialized;
public static Factory get() {
return sInstance;
}
protected static void setInstance(final Factory factory) {
// Not allowed to call this after real application initialization is complete
Assert.isTrue(!sRegistered);
Assert.isTrue(!sInitialized);
sInstance = factory;
}
public abstract void onRequiredPermissionsAcquired();
public abstract Context getApplicationContext();
public abstract DataModel getDataModel();
public abstract BugleGservices getBugleGservices();
public abstract BuglePrefs getApplicationPrefs();
public abstract BuglePrefs getSubscriptionPrefs(int subId);
public abstract BuglePrefs getWidgetPrefs();
public abstract UIIntents getUIIntents();
public abstract MemoryCacheManager getMemoryCacheManager();
public abstract MediaResourceManager getMediaResourceManager();
public abstract MediaCacheManager getMediaCacheManager();
public abstract ContactContentObserver getContactContentObserver();
public abstract PhoneUtils getPhoneUtils(int subId);
public abstract MediaUtil getMediaUtil();
public abstract BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader();
// Note this needs to run from any thread
public abstract void reclaimMemory();
public abstract void onActivityResume();
}
-245
View File
@@ -1,245 +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;
import android.content.Context;
import android.os.Process;
import android.telephony.SmsManager;
import android.util.SparseArray;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelImpl;
import com.android.messaging.datamodel.MemoryCacheManager;
import com.android.messaging.datamodel.ParticipantRefresh.ContactContentObserver;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.media.BugleMediaCacheManager;
import com.android.messaging.datamodel.media.MediaCacheManager;
import com.android.messaging.datamodel.media.MediaResourceManager;
import com.android.messaging.sms.BugleCarrierConfigValuesLoader;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.UIIntentsImpl;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleApplicationPrefs;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesImpl;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BugleSubscriptionPrefs;
import com.android.messaging.util.BugleWidgetPrefs;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.MediaUtil;
import com.android.messaging.util.MediaUtilImpl;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import java.util.concurrent.ConcurrentHashMap;
class FactoryImpl extends Factory {
private BugleApplication mApplication;
private DataModel mDataModel;
private BugleGservices mBugleGservices;
private BugleApplicationPrefs mBugleApplicationPrefs;
private BugleWidgetPrefs mBugleWidgetPrefs;
private Context mApplicationContext;
private UIIntents mUIIntents;
private MemoryCacheManager mMemoryCacheManager;
private MediaResourceManager mMediaResourceManager;
private MediaCacheManager mMediaCacheManager;
private ContactContentObserver mContactContentObserver;
private PhoneUtils mPhoneUtils;
private MediaUtil mMediaUtil;
private SparseArray<BugleSubscriptionPrefs> mSubscriptionPrefs;
private BugleCarrierConfigValuesLoader mCarrierConfigValuesLoader;
// Cached instance for Pre-L_MR1
private static final Object PHONEUTILS_INSTANCE_LOCK = new Object();
private static PhoneUtils sPhoneUtilsInstancePreLMR1 = null;
// Cached subId->instance for L_MR1 and beyond
private static final ConcurrentHashMap<Integer, PhoneUtils> sPhoneUtilsInstanceCacheLMR1 =
new ConcurrentHashMap<>();
private FactoryImpl() {
}
public static Factory register(final Context applicationContext,
final BugleApplication application) {
// This only gets called once (from BugleApplication.onCreate), but its not called in tests.
Assert.isTrue(!sRegistered);
Assert.isNull(Factory.get());
final FactoryImpl factory = new FactoryImpl();
Factory.setInstance(factory);
sRegistered = true;
// At this point Factory is published. Services can now get initialized and depend on
// Factory.get().
factory.mApplication = application;
factory.mApplicationContext = applicationContext;
factory.mMemoryCacheManager = new MemoryCacheManager();
factory.mMediaCacheManager = new BugleMediaCacheManager();
factory.mMediaResourceManager = new MediaResourceManager();
factory.mBugleGservices = new BugleGservicesImpl(applicationContext);
factory.mBugleApplicationPrefs = new BugleApplicationPrefs(applicationContext);
factory.mDataModel = new DataModelImpl(applicationContext);
factory.mBugleWidgetPrefs = new BugleWidgetPrefs(applicationContext);
factory.mUIIntents = new UIIntentsImpl();
factory.mContactContentObserver = new ContactContentObserver();
factory.mMediaUtil = new MediaUtilImpl();
factory.mSubscriptionPrefs = new SparseArray<BugleSubscriptionPrefs>();
factory.mCarrierConfigValuesLoader = new BugleCarrierConfigValuesLoader(applicationContext);
Assert.initializeGservices(factory.mBugleGservices);
LogUtil.initializeGservices(factory.mBugleGservices);
if (OsUtil.hasRequiredPermissions()) {
factory.onRequiredPermissionsAcquired();
}
return factory;
}
@Override
public void onRequiredPermissionsAcquired() {
if (sInitialized) {
return;
}
sInitialized = true;
mApplication.initializeSync(this);
final Thread asyncInitialization = new Thread() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
mApplication.initializeAsync(FactoryImpl.this);
}
};
asyncInitialization.start();
}
@Override
public Context getApplicationContext() {
return mApplicationContext;
}
@Override
public DataModel getDataModel() {
return mDataModel;
}
@Override
public BugleGservices getBugleGservices() {
return mBugleGservices;
}
@Override
public BuglePrefs getApplicationPrefs() {
return mBugleApplicationPrefs;
}
@Override
public BuglePrefs getWidgetPrefs() {
return mBugleWidgetPrefs;
}
@Override
public BuglePrefs getSubscriptionPrefs(int subId) {
subId = PhoneUtils.getDefault().getEffectiveSubId(subId);
BugleSubscriptionPrefs pref = mSubscriptionPrefs.get(subId);
if (pref == null) {
synchronized (this) {
if ((pref = mSubscriptionPrefs.get(subId)) == null) {
pref = new BugleSubscriptionPrefs(getApplicationContext(), subId);
mSubscriptionPrefs.put(subId, pref);
}
}
}
return pref;
}
@Override
public UIIntents getUIIntents() {
return mUIIntents;
}
@Override
public MemoryCacheManager getMemoryCacheManager() {
return mMemoryCacheManager;
}
@Override
public MediaResourceManager getMediaResourceManager() {
return mMediaResourceManager;
}
@Override
public MediaCacheManager getMediaCacheManager() {
return mMediaCacheManager;
}
@Override
public ContactContentObserver getContactContentObserver() {
return mContactContentObserver;
}
@Override
public PhoneUtils getPhoneUtils(int subId) {
if (OsUtil.isAtLeastL_MR1()) {
if (subId == ParticipantData.DEFAULT_SELF_SUB_ID) {
subId = SmsManager.getDefaultSmsSubscriptionId();
}
if (subId < 0) {
LogUtil.w(LogUtil.BUGLE_TAG, "PhoneUtils.getForLMR1(): invalid subId = " + subId);
subId = ParticipantData.DEFAULT_SELF_SUB_ID;
}
PhoneUtils instance = sPhoneUtilsInstanceCacheLMR1.get(subId);
if (instance == null) {
instance = new PhoneUtils.PhoneUtilsLMR1(subId);
sPhoneUtilsInstanceCacheLMR1.putIfAbsent(subId, instance);
}
return instance;
} else {
Assert.isTrue(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
if (sPhoneUtilsInstancePreLMR1 == null) {
synchronized (PHONEUTILS_INSTANCE_LOCK) {
if (sPhoneUtilsInstancePreLMR1 == null) {
sPhoneUtilsInstancePreLMR1 = new PhoneUtils.PhoneUtilsPreLMR1();
}
}
}
return sPhoneUtilsInstancePreLMR1;
}
}
@Override
public void reclaimMemory() {
mMemoryCacheManager.reclaimMemory();
}
@Override
public void onActivityResume() {
}
@Override
public MediaUtil getMediaUtil() {
return mMediaUtil;
}
@Override
public BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader() {
return mCarrierConfigValuesLoader;
}
}
@@ -1,23 +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.annotation;
/**
* An annotation for class members that are made visible for Android's ObjectAnimator to work
* properly through reflection.
*/
public @interface VisibleForAnimation {
}
@@ -1,364 +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.datamodel;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.SparseArray;
import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.io.InputStream;
/**
* Class for creating / loading / reusing bitmaps. This class allow the user to create a new bitmap,
* reuse an bitmap from the pool and to return a bitmap for future reuse. The pool of bitmaps
* allows for faster decode and more efficient memory usage.
* Note: consumers should not create BitmapPool directly, but instead get the pool they want from
* the BitmapPoolManager.
*/
public class BitmapPool implements MemoryCache {
public static final int MAX_SUPPORTED_IMAGE_DIMENSION = 0xFFFF;
protected static final boolean VERBOSE = false;
/**
* Number of reuse failures to skip before reporting.
*/
private static final int FAILED_REPORTING_FREQUENCY = 100;
/**
* Count of reuse failures which have occurred.
*/
private static volatile int sFailedBitmapReuseCount = 0;
/**
* Overall pool data structure which currently only supports rectangular bitmaps. The size of
* one of the sides is used to index into the SparseArray.
*/
private final SparseArray<SingleSizePool> mPool;
private final Object mPoolLock = new Object();
private final String mPoolName;
private final int mMaxSize;
/**
* Inner structure which holds a pool of bitmaps all the same size (i.e. all have the same
* width as each other and height as each other, but not necessarily the same).
*/
private class SingleSizePool {
int mNumItems;
final Bitmap[] mBitmaps;
SingleSizePool(final int maxPoolSize) {
mNumItems = 0;
mBitmaps = new Bitmap[maxPoolSize];
}
}
/**
* Creates a pool of reused bitmaps with helper decode methods which will attempt to use the
* reclaimed bitmaps. This will help speed up the creation of bitmaps by using already allocated
* bitmaps.
* @param maxSize The overall max size of the pool. When the pool exceeds this size, all calls
* to reclaimBitmap(Bitmap) will result in recycling the bitmap.
* @param name Name of the bitmap pool and only used for logging. Can not be null.
*/
BitmapPool(final int maxSize, @NonNull final String name) {
Assert.isTrue(maxSize > 0);
Assert.isTrue(!TextUtils.isEmpty(name));
mPoolName = name;
mMaxSize = maxSize;
mPool = new SparseArray<SingleSizePool>();
}
@Override
public void reclaim() {
synchronized (mPoolLock) {
for (int p = 0; p < mPool.size(); p++) {
final SingleSizePool singleSizePool = mPool.valueAt(p);
for (int i = 0; i < singleSizePool.mNumItems; i++) {
singleSizePool.mBitmaps[i].recycle();
singleSizePool.mBitmaps[i] = null;
}
singleSizePool.mNumItems = 0;
}
mPool.clear();
}
}
/**
* Creates a new BitmapFactory.Options.
*/
public static BitmapFactory.Options getBitmapOptionsForPool(final boolean scaled,
final int inputDensity, final int targetDensity) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = scaled;
options.inDensity = inputDensity;
options.inTargetDensity = targetDensity;
options.inSampleSize = 1;
options.inJustDecodeBounds = false;
options.inMutable = true;
return options;
}
/**
* @return The pool key for the provided image dimensions or 0 if either width or height is
* greater than the max supported image dimension.
*/
private int getPoolKey(final int width, final int height) {
if (width > MAX_SUPPORTED_IMAGE_DIMENSION || height > MAX_SUPPORTED_IMAGE_DIMENSION) {
return 0;
}
return (width << 16) | height;
}
/**
*
* @return A bitmap in the pool with the specified dimensions or null if no bitmap with the
* specified dimension is available.
*/
private Bitmap findPoolBitmap(final int width, final int height) {
final int poolKey = getPoolKey(width, height);
if (poolKey != 0) {
synchronized (mPoolLock) {
// Take a bitmap from the pool if one is available
final SingleSizePool singlePool = mPool.get(poolKey);
if (singlePool != null && singlePool.mNumItems > 0) {
singlePool.mNumItems--;
final Bitmap foundBitmap = singlePool.mBitmaps[singlePool.mNumItems];
singlePool.mBitmaps[singlePool.mNumItems] = null;
return foundBitmap;
}
}
}
return null;
}
/**
* Internal function to try and find a bitmap in the pool which matches the desired width and
* height and then set that in the bitmap options properly.
*
* TODO: Why do we take a width/height? Shouldn't this already be in the
* BitmapFactory.Options instance? Can we assert that they match?
* @param optionsTmp The BitmapFactory.Options to update with the bitmap for the system to try
* to reuse.
* @param width The width of the reusable bitmap.
* @param height The height of the reusable bitmap.
*/
private void assignPoolBitmap(final BitmapFactory.Options optionsTmp, final int width,
final int height) {
if (optionsTmp.inJustDecodeBounds) {
return;
}
optionsTmp.inBitmap = findPoolBitmap(width, height);
}
/**
* Load a resource into a bitmap. Uses a bitmap from the pool if possible to reduce memory
* turnover.
* @param resourceId Resource id to load.
* @param resources Application resources. Cannot be null.
* @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
* be null.
* @param width The width of the bitmap.
* @param height The height of the bitmap.
* @return The decoded Bitmap with the resource drawn in it.
*/
public Bitmap decodeSampledBitmapFromResource(final int resourceId,
@NonNull final Resources resources, @NonNull final BitmapFactory.Options optionsTmp,
final int width, final int height) {
Assert.notNull(resources);
Assert.notNull(optionsTmp);
Assert.isTrue(width > 0);
Assert.isTrue(height > 0);
assignPoolBitmap(optionsTmp, width, height);
Bitmap b = null;
try {
b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
} catch (final IllegalArgumentException e) {
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
if (optionsTmp.inBitmap != null) {
optionsTmp.inBitmap = null;
b = BitmapFactory.decodeResource(resources, resourceId, optionsTmp);
sFailedBitmapReuseCount++;
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
LogUtil.w(LogUtil.BUGLE_TAG,
"Pooled bitmap consistently not being reused count = " +
sFailedBitmapReuseCount);
}
}
} catch (final OutOfMemoryError e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding resource " + resourceId);
reclaim();
}
return b;
}
/**
* Load an input stream into a bitmap. Uses a bitmap from the pool if possible to reduce memory
* turnover.
* @param inputStream InputStream load. Cannot be null.
* @param optionsTmp Should be the same options returned from getBitmapOptionsForPool(). Cannot
* be null.
* @param width The width of the bitmap.
* @param height The height of the bitmap.
* @return The decoded Bitmap with the resource drawn in it.
*/
public Bitmap decodeSampledBitmapFromInputStream(@NonNull final InputStream inputStream,
@NonNull final BitmapFactory.Options optionsTmp,
final int width, final int height) {
Assert.notNull(inputStream);
Assert.isTrue(width > 0);
Assert.isTrue(height > 0);
assignPoolBitmap(optionsTmp, width, height);
Bitmap b = null;
try {
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
} catch (final IllegalArgumentException e) {
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
if (optionsTmp.inBitmap != null) {
optionsTmp.inBitmap = null;
b = BitmapFactory.decodeStream(inputStream, null, optionsTmp);
sFailedBitmapReuseCount++;
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
LogUtil.w(LogUtil.BUGLE_TAG,
"Pooled bitmap consistently not being reused count = " +
sFailedBitmapReuseCount);
}
}
} catch (final OutOfMemoryError e) {
LogUtil.w(LogUtil.BUGLE_TAG, "Oom decoding inputStream");
reclaim();
}
return b;
}
/**
* Turn encoded bytes into a bitmap. Uses a bitmap from the pool if possible to reduce memory
* turnover.
* @param bytes Encoded bytes to draw on the bitmap. Cannot be null.
* @param optionsTmp The bitmap will set here and the input should be generated from
* getBitmapOptionsForPool(). Cannot be null.
* @param width The width of the bitmap.
* @param height The height of the bitmap.
* @return A Bitmap with the encoded bytes drawn in it.
*/
public Bitmap decodeByteArray(@NonNull final byte[] bytes,
@NonNull final BitmapFactory.Options optionsTmp, final int width,
final int height) throws OutOfMemoryError {
Assert.notNull(bytes);
Assert.notNull(optionsTmp);
Assert.isTrue(width > 0);
Assert.isTrue(height > 0);
assignPoolBitmap(optionsTmp, width, height);
Bitmap b = null;
try {
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
} catch (final IllegalArgumentException e) {
if (VERBOSE) {
LogUtil.v(LogUtil.BUGLE_TAG, "BitmapPool(" + mPoolName +
") Unable to use pool bitmap");
}
// BitmapFactory couldn't decode the file, try again without an inputBufferBitmap.
// (i.e. without the bitmap from the pool)
if (optionsTmp.inBitmap != null) {
optionsTmp.inBitmap = null;
b = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, optionsTmp);
sFailedBitmapReuseCount++;
if (sFailedBitmapReuseCount % FAILED_REPORTING_FREQUENCY == 0) {
LogUtil.w(LogUtil.BUGLE_TAG,
"Pooled bitmap consistently not being reused count = " +
sFailedBitmapReuseCount);
}
}
}
return b;
}
/**
* Creates a bitmap with the given size, this will reuse a bitmap in the pool, if one is
* available, otherwise this will create a new one.
* @param width The desired width of the bitmap.
* @param height The desired height of the bitmap.
* @return A bitmap with the desired width and height, this maybe a reused bitmap from the pool.
*/
public Bitmap createOrReuseBitmap(final int width, final int height) {
Bitmap b = findPoolBitmap(width, height);
if (b == null) {
b = createBitmap(width, height);
}
return b;
}
/**
* This will create a new bitmap regardless of pool state.
* @param width The desired width of the bitmap.
* @param height The desired height of the bitmap.
* @return A bitmap with the desired width and height.
*/
private Bitmap createBitmap(final int width, final int height) {
return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
}
/**
* Called when a bitmap is finished being used so that it can be used for another bitmap in the
* future or recycled. Any bitmaps returned should not be used by the caller again.
* @param b The bitmap to return to the pool for future usage or recycled. This cannot be null.
*/
public void reclaimBitmap(@NonNull final Bitmap b) {
Assert.notNull(b);
final int poolKey = getPoolKey(b.getWidth(), b.getHeight());
if (poolKey == 0 || !b.isMutable()) {
// Unsupported image dimensions or a immutable bitmap.
b.recycle();
return;
}
synchronized (mPoolLock) {
SingleSizePool singleSizePool = mPool.get(poolKey);
if (singleSizePool == null) {
singleSizePool = new SingleSizePool(mMaxSize);
mPool.append(poolKey, singleSizePool);
}
if (singleSizePool.mNumItems < singleSizePool.mBitmaps.length) {
singleSizePool.mBitmaps[singleSizePool.mNumItems] = b;
singleSizePool.mNumItems++;
} else {
b.recycle();
}
}
}
/**
* @return whether the pool is full for a given width and height.
*/
public boolean isFull(final int width, final int height) {
final int poolKey = getPoolKey(width, height);
synchronized (mPoolLock) {
final SingleSizePool singleSizePool = mPool.get(poolKey);
if (singleSizePool != null &&
singleSizePool.mNumItems >= singleSizePool.mBitmaps.length) {
return true;
}
return false;
}
}
}
@@ -1,46 +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.datamodel;
import android.content.Context;
import android.content.CursorLoader;
import android.net.Uri;
/**
* Extension to basic cursor loader that has an attached binding id
*/
public class BoundCursorLoader extends CursorLoader {
private final String mBindingId;
/**
* Create cursor loader for associated binding id
*/
public BoundCursorLoader(final String bindingId, final Context context, final Uri uri,
final String[] projection, final String selection, final String[] selectionArgs,
final String sortOrder) {
super(context, uri, projection, selection, selectionArgs, sortOrder);
mBindingId = bindingId;
}
/**
* Binding id associated with this loader - consume can check to verify data still valid
* @return
*/
public String getBindingId() {
return mBindingId;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,64 +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.datamodel;
import android.net.Uri;
import android.text.TextUtils;
import com.android.ex.chips.RecipientEntry;
/**
* An extension of RecipientEntry for Bugle's use since Bugle uses phone numbers to identify
* participants / recipients instead of contact ids. This allows the user to send to multiple
* phone numbers of the same contact.
*/
public class BugleRecipientEntry extends RecipientEntry {
protected BugleRecipientEntry(final int entryType, final String displayName,
final String destination, final int destinationType, final String destinationLabel,
final long contactId, final Long directoryId, final long dataId,
final Uri photoThumbnailUri, final boolean isFirstLevel, final boolean isValid,
final String lookupKey) {
super(entryType, displayName, destination, destinationType, destinationLabel, contactId,
directoryId, dataId, photoThumbnailUri, isFirstLevel, isValid, lookupKey);
}
public static BugleRecipientEntry constructTopLevelEntry(final String displayName,
final int displayNameSource, final String destination, final int destinationType,
final String destinationLabel, final long contactId, final Long directoryId,
final long dataId, final String thumbnailUriAsString, final boolean isValid,
final String lookupKey) {
return new BugleRecipientEntry(ENTRY_TYPE_PERSON, displayName, destination, destinationType,
destinationLabel, contactId, directoryId, dataId, (thumbnailUriAsString != null
? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey);
}
public static BugleRecipientEntry constructSecondLevelEntry(final String displayName,
final int displayNameSource, final String destination, final int destinationType,
final String destinationLabel, final long contactId, final Long directoryId,
final long dataId, final String thumbnailUriAsString, final boolean isValid,
final String lookupKey) {
return new BugleRecipientEntry(ENTRY_TYPE_PERSON, displayName, destination, destinationType,
destinationLabel, contactId, directoryId, dataId, (thumbnailUriAsString != null
? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey);
}
@Override
public boolean isSamePerson(final RecipientEntry entry) {
return getDestination() != null && entry.getDestination() != null &&
TextUtils.equals(getDestination(), entry.getDestination());
}
}
@@ -1,120 +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.datamodel;
import android.provider.BaseColumns;
import com.android.ex.photo.provider.PhotoContract.PhotoViewColumns;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.util.ContentType;
/**
* View for the image parts for the conversation. It is used to provide the photoviewer with a
* a data source for all the photos in a conversation, so that the photoviewer can support paging
* through all the photos of the conversation. The columns of the view are a superset of
* {@link com.android.ex.photo.provider.PhotoContract.PhotoViewColumns}.
*/
public class ConversationImagePartsView {
private static final String VIEW_NAME = "conversation_image_parts_view";
private static final String CREATE_SQL = "CREATE VIEW " +
VIEW_NAME + " AS SELECT "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
+ " as " + Columns.CONVERSATION_ID + ", "
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_URI
+ " as " + Columns.URI + ", "
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.FULL_NAME
+ " as " + Columns.SENDER_FULL_NAME + ", "
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_URI
+ " as " + Columns.CONTENT_URI + ", "
// Use NULL as the thumbnail uri
+ " NULL as " + Columns.THUMBNAIL_URI + ", "
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE
+ " as " + Columns.CONTENT_TYPE + ", "
//
// Columns in addition to those specified by PhotoContract
//
+ DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns.DISPLAY_DESTINATION
+ " as " + Columns.DISPLAY_DESTINATION + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP
+ " as " + Columns.RECEIVED_TIMESTAMP + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.STATUS
+ " as " + Columns.STATUS + " "
+ " FROM " + DatabaseHelper.MESSAGES_TABLE + " LEFT JOIN " + DatabaseHelper.PARTS_TABLE
+ " ON (" + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID
+ "=" + DatabaseHelper.PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ") "
+ " LEFT JOIN " + DatabaseHelper.PARTICIPANTS_TABLE + " ON ("
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.SENDER_PARTICIPANT_ID
+ '=' + DatabaseHelper.PARTICIPANTS_TABLE + '.' + ParticipantColumns._ID + ")"
// "content_type like 'image/%'"
+ " WHERE " + DatabaseHelper.PARTS_TABLE + "." + PartColumns.CONTENT_TYPE
+ " like '" + ContentType.IMAGE_PREFIX + "%'"
+ " ORDER BY "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.RECEIVED_TIMESTAMP + " ASC, "
+ DatabaseHelper.PARTS_TABLE + '.' + PartColumns._ID + " ASC";
static class Columns implements BaseColumns {
static final String CONVERSATION_ID = MessageColumns.CONVERSATION_ID;
static final String URI = PhotoViewColumns.URI;
static final String SENDER_FULL_NAME = PhotoViewColumns.NAME;
static final String CONTENT_URI = PhotoViewColumns.CONTENT_URI;
static final String THUMBNAIL_URI = PhotoViewColumns.THUMBNAIL_URI;
static final String CONTENT_TYPE = PhotoViewColumns.CONTENT_TYPE;
// Columns in addition to those specified by PhotoContract
static final String DISPLAY_DESTINATION = ParticipantColumns.DISPLAY_DESTINATION;
static final String RECEIVED_TIMESTAMP = MessageColumns.RECEIVED_TIMESTAMP;
static final String STATUS = MessageColumns.STATUS;
}
public interface PhotoViewQuery {
public final String[] PROJECTION = {
PhotoViewColumns.URI,
PhotoViewColumns.NAME,
PhotoViewColumns.CONTENT_URI,
PhotoViewColumns.THUMBNAIL_URI,
PhotoViewColumns.CONTENT_TYPE,
// Columns in addition to those specified by PhotoContract
Columns.DISPLAY_DESTINATION,
Columns.RECEIVED_TIMESTAMP,
Columns.STATUS,
};
public final int INDEX_URI = 0;
public final int INDEX_SENDER_FULL_NAME = 1;
public final int INDEX_CONTENT_URI = 2;
public final int INDEX_THUMBNAIL_URI = 3;
public final int INDEX_CONTENT_TYPE = 4;
// Columns in addition to those specified by PhotoContract
public final int INDEX_DISPLAY_DESTINATION = 5;
public final int INDEX_RECEIVED_TIMESTAMP = 6;
public final int INDEX_STATUS = 7;
}
static final String getViewName() {
return VIEW_NAME;
}
static final String getCreateSql() {
return CREATE_SQL;
}
}
@@ -1,82 +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.datamodel;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import com.android.messaging.util.Assert;
import com.google.common.annotations.VisibleForTesting;
/**
* Holds parameters and data (such as content URI) for performing queries on the content provider.
* This class could then be used to perform a query using either a BoundCursorLoader or querying
* on the content resolver directly.
*
* This class is used for cases where the way to load a cursor is not fixed. For example,
* when using ContactUtil to query for phone numbers, the ContactPickerFragment wants to use
* a CursorLoader to asynchronously load the data and tie in nicely with its data binding
* paradigm, whereas ContactRecipientAdapter wants to synchronously perform the query on the
* worker thread.
*/
public class CursorQueryData {
protected final Uri mUri;
protected final String[] mProjection;
protected final String mSelection;
protected final String[] mSelectionArgs;
protected final String mSortOrder;
protected final Context mContext;
public CursorQueryData(final Context context, final Uri uri, final String[] projection,
final String selection, final String[] selectionArgs, final String sortOrder) {
mContext = context;
mUri = uri;
mProjection = projection;
mSelection = selection;
mSelectionArgs = selectionArgs;
mSortOrder = sortOrder;
}
public BoundCursorLoader createBoundCursorLoader(final String bindingId) {
return new BoundCursorLoader(bindingId, mContext, mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
}
public Cursor performSynchronousQuery() {
Assert.isNotMainThread();
if (mUri == null) {
// See {@link #getEmptyQueryData}
return null;
} else {
return mContext.getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
}
}
@VisibleForTesting
public Uri getUri() {
return mUri;
}
/**
* Representation of an invalid query. {@link #performSynchronousQuery} will return
* a null Cursor.
*/
public static CursorQueryData getEmptyQueryData() {
return new CursorQueryData(null, null, null, null, null, null);
}
}
@@ -1,158 +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.datamodel;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.action.Action;
import com.android.messaging.datamodel.action.ActionService;
import com.android.messaging.datamodel.action.BackgroundWorker;
import com.android.messaging.datamodel.data.BlockedParticipantsData;
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
import com.android.messaging.datamodel.data.ContactListItemData;
import com.android.messaging.datamodel.data.ContactPickerData;
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
import com.android.messaging.datamodel.data.ConversationData;
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
import com.android.messaging.datamodel.data.MediaPickerData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.ParticipantListItemData;
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
import com.android.messaging.datamodel.data.SettingsData;
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
import com.android.messaging.datamodel.data.SubscriptionListData;
import com.android.messaging.datamodel.data.VCardContactItemData;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.ConnectivityUtil;
public abstract class DataModel {
private String mFocusedConversation;
private boolean mConversationListScrolledToNewestConversation;
public static DataModel get() {
return Factory.get().getDataModel();
}
public static final void startActionService(final Action action) {
get().getActionService().startAction(action);
}
public static final void scheduleAction(final Action action,
final int code, final long delayMs) {
get().getActionService().scheduleAction(action, code, delayMs);
}
public abstract ConversationListData createConversationListData(final Context context,
final ConversationListDataListener listener, final boolean archivedMode);
public abstract ConversationData createConversationData(final Context context,
final ConversationDataListener listener, final String conversationId);
public abstract ContactListItemData createContactListItemData();
public abstract ContactPickerData createContactPickerData(final Context context,
final ContactPickerDataListener listener);
public abstract MediaPickerData createMediaPickerData(final Context context);
public abstract GalleryGridItemData createGalleryGridItemData();
public abstract LaunchConversationData createLaunchConversationData(
LaunchConversationDataListener listener);
public abstract PeopleOptionsItemData createPeopleOptionsItemData(final Context context);
public abstract PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
final Context context, final PeopleAndOptionsDataListener listener);
public abstract VCardContactItemData createVCardContactItemData(final Context context,
final MessagePartData data);
public abstract VCardContactItemData createVCardContactItemData(final Context context,
final Uri vCardUri);
public abstract ParticipantListItemData createParticipantListItemData(
final ParticipantData participant);
public abstract BlockedParticipantsData createBlockedParticipantsData(Context context,
BlockedParticipantsDataListener listener);
public abstract SubscriptionListData createSubscriptonListData(Context context);
public abstract SettingsData createSettingsData(Context context, SettingsDataListener listener);
public abstract DraftMessageData createDraftMessageData(String conversationId);
public abstract ActionService getActionService();
public abstract BackgroundWorker getBackgroundWorkerForActionService();
@DoesNotRunOnMainThread
public abstract DatabaseWrapper getDatabase();
// Allow DataModel to coordinate with activity lifetime events.
public abstract void onActivityResume();
abstract void onCreateTables(final SQLiteDatabase db);
public void setFocusedConversation(final String conversationId) {
mFocusedConversation = conversationId;
}
public boolean isFocusedConversation(final String conversationId) {
return !TextUtils.isEmpty(mFocusedConversation)
&& TextUtils.equals(mFocusedConversation, conversationId);
}
public void setConversationListScrolledToNewestConversation(
final boolean scrolledToNewestConversation) {
mConversationListScrolledToNewestConversation = scrolledToNewestConversation;
}
public boolean isConversationListScrolledToNewestConversation() {
return mConversationListScrolledToNewestConversation;
}
/**
* If a new message is received in the specified conversation, will the user be able to
* observe it in some UI within the app?
* @param conversationId conversation with the new incoming message
*/
public boolean isNewMessageObservable(final String conversationId) {
return isConversationListScrolledToNewestConversation()
|| isFocusedConversation(conversationId);
}
public abstract void onApplicationCreated();
public abstract ConnectivityUtil getConnectivityUtil();
public abstract SyncManager getSyncManager();
}
@@ -1,101 +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.datamodel;
public class DataModelException extends Exception {
private static final long serialVersionUID = 1L;
private static final int FIRST = 100;
// ERRORS GENERATED INTERNALLY BY DATA MODEL.
// ERRORS RELATED WITH SMS.
public static final int ERROR_SMS_TEMPORARY_FAILURE = 116;
public static final int ERROR_SMS_PERMANENT_FAILURE = 117;
public static final int ERROR_MMS_TEMPORARY_FAILURE = 118;
public static final int ERROR_MMS_PERMANENT_UNKNOWN_FAILURE = 119;
// Request expired.
public static final int ERROR_EXPIRED = 120;
// Request canceled by user.
public static final int ERROR_CANCELED = 121;
public static final int ERROR_MOBILE_DATA_DISABLED = 123;
public static final int ERROR_MMS_SERVICE_BLOCKED = 124;
public static final int ERROR_MMS_INVALID_ADDRESS = 125;
public static final int ERROR_MMS_NETWORK_PROBLEM = 126;
public static final int ERROR_MMS_MESSAGE_NOT_FOUND = 127;
public static final int ERROR_MMS_MESSAGE_FORMAT_CORRUPT = 128;
public static final int ERROR_MMS_CONTENT_NOT_ACCEPTED = 129;
public static final int ERROR_MMS_MESSAGE_NOT_SUPPORTED = 130;
public static final int ERROR_MMS_REPLY_CHARGING_ERROR = 131;
public static final int ERROR_MMS_ADDRESS_HIDING_NOT_SUPPORTED = 132;
public static final int ERROR_MMS_LACK_OF_PREPAID = 133;
public static final int ERROR_MMS_CAN_NOT_PERSIST = 134;
public static final int ERROR_MMS_NO_AVAILABLE_APN = 135;
public static final int ERROR_MMS_INVALID_MESSAGE_TO_SEND = 136;
public static final int ERROR_MMS_INVALID_MESSAGE_RECEIVED = 137;
public static final int ERROR_MMS_NO_CONFIGURATION = 138;
private static final int LAST = 138;
private final boolean mIsInjection;
private final int mErrorCode;
private final String mMessage;
private final long mBackoff;
public DataModelException(final int errorCode, final Exception innerException,
final long backoff, final boolean injection, final String message) {
// Since some of the exceptions passed in may not be serializable, only record message
// instead of setting inner exception for Exception class. Otherwise, we will get
// serialization issues when we pass ServerRequestException as intent extra later.
if (errorCode < FIRST || errorCode > LAST) {
throw new IllegalArgumentException("error code out of range: " + errorCode);
}
mIsInjection = injection;
mErrorCode = errorCode;
if (innerException != null) {
mMessage = innerException.getMessage() + " -- " +
(mIsInjection ? "[INJECTED] -- " : "") + message;
} else {
mMessage = (mIsInjection ? "[INJECTED] -- " : "") + message;
}
mBackoff = backoff;
}
public DataModelException(final int errorCode) {
this(errorCode, null, 0, false, null);
}
public DataModelException(final int errorCode, final Exception innerException) {
this(errorCode, innerException, 0, false, null);
}
public DataModelException(final int errorCode, final String message) {
this(errorCode, null, 0, false, message);
}
@Override
public String getMessage() {
return mMessage;
}
public int getErrorCode() {
return mErrorCode;
}
}
@@ -1,236 +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.datamodel;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.telephony.SubscriptionManager;
import com.android.messaging.datamodel.action.ActionService;
import com.android.messaging.datamodel.action.BackgroundWorker;
import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction;
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
import com.android.messaging.datamodel.data.BlockedParticipantsData;
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
import com.android.messaging.datamodel.data.ContactListItemData;
import com.android.messaging.datamodel.data.ContactPickerData;
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
import com.android.messaging.datamodel.data.ConversationData;
import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.DraftMessageData;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
import com.android.messaging.datamodel.data.MediaPickerData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.ParticipantListItemData;
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
import com.android.messaging.datamodel.data.SettingsData;
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
import com.android.messaging.datamodel.data.SubscriptionListData;
import com.android.messaging.datamodel.data.VCardContactItemData;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.ConnectivityUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
public class DataModelImpl extends DataModel {
private final Context mContext;
private final ActionService mActionService;
private final BackgroundWorker mDataModelWorker;
private final DatabaseHelper mDatabaseHelper;
private final ConnectivityUtil mConnectivityUtil;
private final SyncManager mSyncManager;
public DataModelImpl(final Context context) {
super();
mContext = context;
mActionService = new ActionService();
mDataModelWorker = new BackgroundWorker();
mDatabaseHelper = DatabaseHelper.getInstance(context);
mConnectivityUtil = new ConnectivityUtil(context);
mSyncManager = new SyncManager();
}
@Override
public ConversationListData createConversationListData(final Context context,
final ConversationListDataListener listener, final boolean archivedMode) {
return new ConversationListData(context, listener, archivedMode);
}
@Override
public ConversationData createConversationData(final Context context,
final ConversationDataListener listener, final String conversationId) {
return new ConversationData(context, listener, conversationId);
}
@Override
public ContactListItemData createContactListItemData() {
return new ContactListItemData();
}
@Override
public ContactPickerData createContactPickerData(final Context context,
final ContactPickerDataListener listener) {
return new ContactPickerData(context, listener);
}
@Override
public BlockedParticipantsData createBlockedParticipantsData(
final Context context, final BlockedParticipantsDataListener listener) {
return new BlockedParticipantsData(context, listener);
}
@Override
public MediaPickerData createMediaPickerData(final Context context) {
return new MediaPickerData(context);
}
@Override
public GalleryGridItemData createGalleryGridItemData() {
return new GalleryGridItemData();
}
@Override
public LaunchConversationData createLaunchConversationData(
final LaunchConversationDataListener listener) {
return new LaunchConversationData(listener);
}
@Override
public PeopleOptionsItemData createPeopleOptionsItemData(final Context context) {
return new PeopleOptionsItemData(context);
}
@Override
public PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
final Context context, final PeopleAndOptionsDataListener listener) {
return new PeopleAndOptionsData(conversationId, context, listener);
}
@Override
public VCardContactItemData createVCardContactItemData(final Context context,
final MessagePartData data) {
return new VCardContactItemData(context, data);
}
@Override
public VCardContactItemData createVCardContactItemData(final Context context,
final Uri vCardUri) {
return new VCardContactItemData(context, vCardUri);
}
@Override
public ParticipantListItemData createParticipantListItemData(
final ParticipantData participant) {
return new ParticipantListItemData(participant);
}
@Override
public SubscriptionListData createSubscriptonListData(Context context) {
return new SubscriptionListData(context);
}
@Override
public SettingsData createSettingsData(Context context, SettingsDataListener listener) {
return new SettingsData(context, listener);
}
@Override
public DraftMessageData createDraftMessageData(String conversationId) {
return new DraftMessageData(conversationId);
}
@Override
public ActionService getActionService() {
// We need to allow access to this on the UI thread since it's used to start actions.
return mActionService;
}
@Override
public BackgroundWorker getBackgroundWorkerForActionService() {
return mDataModelWorker;
}
@Override
@DoesNotRunOnMainThread
public DatabaseWrapper getDatabase() {
// We prevent the main UI thread from accessing the database since we have to allow
// public access to this class to enable sub-packages to access data.
Assert.isNotMainThread();
return mDatabaseHelper.getDatabase();
}
@Override
public ConnectivityUtil getConnectivityUtil() {
return mConnectivityUtil;
}
@Override
public SyncManager getSyncManager() {
return mSyncManager;
}
@Override
void onCreateTables(final SQLiteDatabase db) {
LogUtil.w(LogUtil.BUGLE_TAG, "Rebuilt databases: reseting related state");
// Clear other things that implicitly reference the DB
SyncManager.resetLastSyncTimestamps();
}
@Override
public void onActivityResume() {
// Perform an incremental sync and register for changes if necessary
mSyncManager.updateSyncObserver(mContext);
// Trigger a participant refresh if needed, we should only need to refresh if there is
// contact change while the activity was paused.
ParticipantRefresh.refreshParticipantsIfNeeded();
}
@Override
public void onApplicationCreated() {
FixupMessageStatusOnStartupAction.fixupMessageStatus();
ProcessPendingMessagesAction.processFirstPendingMessage();
SyncManager.immediateSync();
if (OsUtil.isAtLeastL_MR1()) {
// Start listening for subscription change events for refreshing self participants.
PhoneUtils.getDefault().toLMr1().registerOnSubscriptionsChangedListener(
new SubscriptionManager.OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
// TODO: This dynamically changes the mms config that app is
// currently using. It may cause inconsistency in some cases. We need
// to check the usage of mms config and handle the dynamic change
// gracefully
MmsConfig.loadAsync();
ParticipantRefresh.refreshSelfParticipants();
}
});
}
}
}
@@ -1,813 +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.datamodel;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
import com.android.messaging.BugleApplication;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
/**
* TODO: Open Issues:
* - Should we be storing the draft messages in the regular messages table or should we have a
* separate table for drafts to keep the normal messages query as simple as possible?
*/
/**
* Allows access to the SQL database. This is package private.
*/
public class DatabaseHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "bugle_db";
private static final int getDatabaseVersion(final Context context) {
return Integer.parseInt(context.getResources().getString(R.string.database_version));
}
/** Table containing names of all other tables and views */
private static final String MASTER_TABLE = "sqlite_master";
/** Column containing the name of the tables and views */
private static final String[] MASTER_COLUMNS = new String[] { "name", };
// Table names
public static final String CONVERSATIONS_TABLE = "conversations";
public static final String MESSAGES_TABLE = "messages";
public static final String PARTS_TABLE = "parts";
public static final String PARTICIPANTS_TABLE = "participants";
public static final String CONVERSATION_PARTICIPANTS_TABLE = "conversation_participants";
// Views
static final String DRAFT_PARTS_VIEW = "draft_parts_view";
// Conversations table schema
public static class ConversationColumns implements BaseColumns {
/* SMS/MMS Thread ID from the system provider */
public static final String SMS_THREAD_ID = "sms_thread_id";
/* Display name for the conversation */
public static final String NAME = "name";
/* Latest Message ID for the read status to display in conversation list */
public static final String LATEST_MESSAGE_ID = "latest_message_id";
/* Latest text snippet for display in conversation list */
public static final String SNIPPET_TEXT = "snippet_text";
/* Latest text subject for display in conversation list, empty string if none exists */
public static final String SUBJECT_TEXT = "subject_text";
/* Preview Uri */
public static final String PREVIEW_URI = "preview_uri";
/* The preview uri's content type */
public static final String PREVIEW_CONTENT_TYPE = "preview_content_type";
/* If we should display the current draft snippet/preview pair or snippet/preview pair */
public static final String SHOW_DRAFT = "show_draft";
/* Latest draft text subject for display in conversation list, empty string if none exists*/
public static final String DRAFT_SUBJECT_TEXT = "draft_subject_text";
/* Latest draft text snippet for display, empty string if none exists */
public static final String DRAFT_SNIPPET_TEXT = "draft_snippet_text";
/* Draft Preview Uri, empty string if none exists */
public static final String DRAFT_PREVIEW_URI = "draft_preview_uri";
/* The preview uri's content type */
public static final String DRAFT_PREVIEW_CONTENT_TYPE = "draft_preview_content_type";
/* If this conversation is archived */
public static final String ARCHIVE_STATUS = "archive_status";
/* Timestamp for sorting purposes */
public static final String SORT_TIMESTAMP = "sort_timestamp";
/* Last read message timestamp */
public static final String LAST_READ_TIMESTAMP = "last_read_timestamp";
/* Avatar for the conversation. Could be for group of individual */
public static final String ICON = "icon";
/* Participant contact ID if this conversation has a single participant. -1 otherwise */
public static final String PARTICIPANT_CONTACT_ID = "participant_contact_id";
/* Participant lookup key if this conversation has a single participant. null otherwise */
public static final String PARTICIPANT_LOOKUP_KEY = "participant_lookup_key";
/*
* Participant's normalized destination if this conversation has a single participant.
* null otherwise.
*/
public static final String OTHER_PARTICIPANT_NORMALIZED_DESTINATION =
"participant_normalized_destination";
/* Default self participant for the conversation */
public static final String CURRENT_SELF_ID = "current_self_id";
/* Participant count not including self (so will be 1 for 1:1 or bigger for group) */
public static final String PARTICIPANT_COUNT = "participant_count";
/* Should notifications be enabled for this conversation? */
public static final String NOTIFICATION_ENABLED = "notification_enabled";
/* Notification sound used for the conversation */
public static final String NOTIFICATION_SOUND_URI = "notification_sound_uri";
/* Should vibrations be enabled for the conversation's notification? */
public static final String NOTIFICATION_VIBRATION = "notification_vibration";
/* Conversation recipients include email address */
public static final String INCLUDE_EMAIL_ADDRESS = "include_email_addr";
// Record the last received sms's service center info if it indicates that the reply path
// is present (TP-Reply-Path), so that we could use it for the subsequent message to send.
// Refer to TS 23.040 D.6 and SmsMessageSender.java in Android Messaging app.
public static final String SMS_SERVICE_CENTER = "sms_service_center";
}
// Conversation table SQL
private static final String CREATE_CONVERSATIONS_TABLE_SQL =
"CREATE TABLE " + CONVERSATIONS_TABLE + "("
+ ConversationColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
// TODO : Int? Required not default?
+ ConversationColumns.SMS_THREAD_ID + " INT DEFAULT(0), "
+ ConversationColumns.NAME + " TEXT, "
+ ConversationColumns.LATEST_MESSAGE_ID + " INT, "
+ ConversationColumns.SNIPPET_TEXT + " TEXT, "
+ ConversationColumns.SUBJECT_TEXT + " TEXT, "
+ ConversationColumns.PREVIEW_URI + " TEXT, "
+ ConversationColumns.PREVIEW_CONTENT_TYPE + " TEXT, "
+ ConversationColumns.SHOW_DRAFT + " INT DEFAULT(0), "
+ ConversationColumns.DRAFT_SNIPPET_TEXT + " TEXT, "
+ ConversationColumns.DRAFT_SUBJECT_TEXT + " TEXT, "
+ ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, "
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, "
+ ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), "
+ ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), "
+ ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), "
+ ConversationColumns.ICON + " TEXT, "
+ ConversationColumns.PARTICIPANT_CONTACT_ID + " INT DEFAULT ( "
+ ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), "
+ ConversationColumns.PARTICIPANT_LOOKUP_KEY + " TEXT, "
+ ConversationColumns.OTHER_PARTICIPANT_NORMALIZED_DESTINATION + " TEXT, "
+ ConversationColumns.CURRENT_SELF_ID + " TEXT, "
+ ConversationColumns.PARTICIPANT_COUNT + " INT DEFAULT(0), "
+ ConversationColumns.NOTIFICATION_ENABLED + " INT DEFAULT(1), "
+ ConversationColumns.NOTIFICATION_SOUND_URI + " TEXT, "
+ ConversationColumns.NOTIFICATION_VIBRATION + " INT DEFAULT(1), "
+ ConversationColumns.INCLUDE_EMAIL_ADDRESS + " INT DEFAULT(0), "
+ ConversationColumns.SMS_SERVICE_CENTER + " TEXT "
+ ");";
private static final String CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL =
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SMS_THREAD_ID
+ " ON " + CONVERSATIONS_TABLE
+ "(" + ConversationColumns.SMS_THREAD_ID + ")";
private static final String CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL =
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.ARCHIVE_STATUS
+ " ON " + CONVERSATIONS_TABLE
+ "(" + ConversationColumns.ARCHIVE_STATUS + ")";
private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL =
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP
+ " ON " + CONVERSATIONS_TABLE
+ "(" + ConversationColumns.SORT_TIMESTAMP + ")";
// Messages table schema
public static class MessageColumns implements BaseColumns {
/* conversation id that this message belongs to */
public static final String CONVERSATION_ID = "conversation_id";
/* participant which send this message */
public static final String SENDER_PARTICIPANT_ID = "sender_id";
/* This is bugle's internal status for the message */
public static final String STATUS = "message_status";
/* Type of message: SMS, MMS or MMS notification */
public static final String PROTOCOL = "message_protocol";
/* This is the time that the sender sent the message */
public static final String SENT_TIMESTAMP = "sent_timestamp";
/* Time that we received the message on this device */
public static final String RECEIVED_TIMESTAMP = "received_timestamp";
/* When the message has been seen by a user in a notification */
public static final String SEEN = "seen";
/* When the message has been read by a user */
public static final String READ = "read";
/* participant representing the sim which processed this message */
public static final String SELF_PARTICIPANT_ID = "self_id";
/*
* Time when a retry is initiated. This is used to compute the retry window
* when we retry sending/downloading a message.
*/
public static final String RETRY_START_TIMESTAMP = "retry_start_timestamp";
// Columns which map to the SMS provider
/* Message ID from the platform provider */
public static final String SMS_MESSAGE_URI = "sms_message_uri";
/* The message priority for MMS message */
public static final String SMS_PRIORITY = "sms_priority";
/* The message size for MMS message */
public static final String SMS_MESSAGE_SIZE = "sms_message_size";
/* The subject for MMS message */
public static final String MMS_SUBJECT = "mms_subject";
/* Transaction id for MMS notificaiton */
public static final String MMS_TRANSACTION_ID = "mms_transaction_id";
/* Content location for MMS notificaiton */
public static final String MMS_CONTENT_LOCATION = "mms_content_location";
/* The expiry time (ms) for MMS message */
public static final String MMS_EXPIRY = "mms_expiry";
/* The detailed status (RESPONSE_STATUS or RETRIEVE_STATUS) for MMS message */
public static final String RAW_TELEPHONY_STATUS = "raw_status";
}
// Messages table SQL
private static final String CREATE_MESSAGES_TABLE_SQL =
"CREATE TABLE " + MESSAGES_TABLE + " ("
+ MessageColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ MessageColumns.CONVERSATION_ID + " INT, "
+ MessageColumns.SENDER_PARTICIPANT_ID + " INT, "
+ MessageColumns.SENT_TIMESTAMP + " INT DEFAULT(0), "
+ MessageColumns.RECEIVED_TIMESTAMP + " INT DEFAULT(0), "
+ MessageColumns.PROTOCOL + " INT DEFAULT(0), "
+ MessageColumns.STATUS + " INT DEFAULT(0), "
+ MessageColumns.SEEN + " INT DEFAULT(0), "
+ MessageColumns.READ + " INT DEFAULT(0), "
+ MessageColumns.SMS_MESSAGE_URI + " TEXT, "
+ MessageColumns.SMS_PRIORITY + " INT DEFAULT(0), "
+ MessageColumns.SMS_MESSAGE_SIZE + " INT DEFAULT(0), "
+ MessageColumns.MMS_SUBJECT + " TEXT, "
+ MessageColumns.MMS_TRANSACTION_ID + " TEXT, "
+ MessageColumns.MMS_CONTENT_LOCATION + " TEXT, "
+ MessageColumns.MMS_EXPIRY + " INT DEFAULT(0), "
+ MessageColumns.RAW_TELEPHONY_STATUS + " INT DEFAULT(0), "
+ MessageColumns.SELF_PARTICIPANT_ID + " INT, "
+ MessageColumns.RETRY_START_TIMESTAMP + " INT DEFAULT(0), "
+ "FOREIGN KEY (" + MessageColumns.CONVERSATION_ID + ") REFERENCES "
+ CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE "
+ "FOREIGN KEY (" + MessageColumns.SENDER_PARTICIPANT_ID + ") REFERENCES "
+ PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL "
+ "FOREIGN KEY (" + MessageColumns.SELF_PARTICIPANT_ID + ") REFERENCES "
+ PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + ") ON DELETE SET NULL "
+ ");";
// Primary sort index for messages table : by conversation id, status, received timestamp.
private static final String MESSAGES_TABLE_SORT_INDEX_SQL =
"CREATE INDEX index_" + MESSAGES_TABLE + "_sort ON " + MESSAGES_TABLE + "("
+ MessageColumns.CONVERSATION_ID + ", "
+ MessageColumns.STATUS + ", "
+ MessageColumns.RECEIVED_TIMESTAMP + ")";
private static final String MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL =
"CREATE INDEX index_" + MESSAGES_TABLE + "_status_seen ON " + MESSAGES_TABLE + "("
+ MessageColumns.STATUS + ", "
+ MessageColumns.SEEN + ")";
// Parts table schema
// A part may contain text or a media url, but not both.
public static class PartColumns implements BaseColumns {
/* message id that this part belongs to */
public static final String MESSAGE_ID = "message_id";
/* conversation id that this part belongs to */
public static final String CONVERSATION_ID = "conversation_id";
/* text for this part */
public static final String TEXT = "text";
/* content uri for this part */
public static final String CONTENT_URI = "uri";
/* content type for this part */
public static final String CONTENT_TYPE = "content_type";
/* cached width for this part (for layout while loading) */
public static final String WIDTH = "width";
/* cached height for this part (for layout while loading) */
public static final String HEIGHT = "height";
/* de-normalized copy of timestamp from the messages table. This is populated
* via an insert trigger on the parts table.
*/
public static final String TIMESTAMP = "timestamp";
}
// Message part table SQL
private static final String CREATE_PARTS_TABLE_SQL =
"CREATE TABLE " + PARTS_TABLE + "("
+ PartColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ PartColumns.MESSAGE_ID + " INT,"
+ PartColumns.TEXT + " TEXT,"
+ PartColumns.CONTENT_URI + " TEXT,"
+ PartColumns.CONTENT_TYPE + " TEXT,"
+ PartColumns.WIDTH + " INT DEFAULT("
+ MessagingContentProvider.UNSPECIFIED_SIZE + "),"
+ PartColumns.HEIGHT + " INT DEFAULT("
+ MessagingContentProvider.UNSPECIFIED_SIZE + "),"
+ PartColumns.TIMESTAMP + " INT, "
+ PartColumns.CONVERSATION_ID + " INT NOT NULL,"
+ "FOREIGN KEY (" + PartColumns.MESSAGE_ID + ") REFERENCES "
+ MESSAGES_TABLE + "(" + MessageColumns._ID + ") ON DELETE CASCADE "
+ "FOREIGN KEY (" + PartColumns.CONVERSATION_ID + ") REFERENCES "
+ CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ") ON DELETE CASCADE "
+ ");";
public static final String CREATE_PARTS_TRIGGER_SQL =
"CREATE TRIGGER " + PARTS_TABLE + "_TRIGGER" + " AFTER INSERT ON " + PARTS_TABLE
+ " FOR EACH ROW "
+ " BEGIN UPDATE " + PARTS_TABLE
+ " SET " + PartColumns.TIMESTAMP + "="
+ " (SELECT received_timestamp FROM " + MESSAGES_TABLE + " WHERE " + MESSAGES_TABLE
+ "." + MessageColumns._ID + "=" + "NEW." + PartColumns.MESSAGE_ID + ")"
+ " WHERE " + PARTS_TABLE + "." + PartColumns._ID + "=" + "NEW." + PartColumns._ID
+ "; END";
public static final String CREATE_MESSAGES_TRIGGER_SQL =
"CREATE TRIGGER " + MESSAGES_TABLE + "_TRIGGER" + " AFTER UPDATE OF "
+ MessageColumns.RECEIVED_TIMESTAMP + " ON " + MESSAGES_TABLE
+ " FOR EACH ROW BEGIN UPDATE " + PARTS_TABLE + " SET " + PartColumns.TIMESTAMP
+ " = NEW." + MessageColumns.RECEIVED_TIMESTAMP + " WHERE " + PARTS_TABLE + "."
+ PartColumns.MESSAGE_ID + " = NEW." + MessageColumns._ID
+ "; END;";
// Primary sort index for parts table : by message_id
private static final String PARTS_TABLE_MESSAGE_INDEX_SQL =
"CREATE INDEX index_" + PARTS_TABLE + "_message_id ON " + PARTS_TABLE + "("
+ PartColumns.MESSAGE_ID + ")";
// Participants table schema
public static class ParticipantColumns implements BaseColumns {
/* The subscription id for the sim associated with this self participant.
* Introduced in L. For earlier versions will always be default_sub_id (-1).
* For multi sim devices (or cases where the sim was changed) single device
* may have several different sub_id values */
public static final String SUB_ID = "sub_id";
/* The slot of the active SIM (inserted in the device) for this self-participant. If the
* self-participant doesn't correspond to any active SIM, this will be
* {@link android.telephony.SubscriptionManager#INVALID_SLOT_ID}.
* The column is ignored for all non-self participants.
*/
public static final String SIM_SLOT_ID = "sim_slot_id";
/* The phone number stored in a standard E164 format if possible. This is unique for a
* given participant. We can't handle multiple participants with the same phone number
* since we don't know which of them a message comes from. This can also be an email
* address, in which case this is the same as the displayed address */
public static final String NORMALIZED_DESTINATION = "normalized_destination";
/* The phone number as originally supplied and used for dialing. Not necessarily in E164
* format or unique */
public static final String SEND_DESTINATION = "send_destination";
/* The user-friendly formatting of the phone number according to the region setting of
* the device when the row was added. */
public static final String DISPLAY_DESTINATION = "display_destination";
/* A string with this participant's full name or a pretty printed phone number */
public static final String FULL_NAME = "full_name";
/* A string with just this participant's first name */
public static final String FIRST_NAME = "first_name";
/* A local URI to an asset for the icon for this participant */
public static final String PROFILE_PHOTO_URI = "profile_photo_uri";
/* Contact id for matching local contact for this participant */
public static final String CONTACT_ID = "contact_id";
/* String that contains hints on how to find contact information in a contact lookup */
public static final String LOOKUP_KEY = "lookup_key";
/* If this participant is blocked */
public static final String BLOCKED = "blocked";
/* The color of the subscription (FOR SELF PARTICIPANTS ONLY) */
public static final String SUBSCRIPTION_COLOR = "subscription_color";
/* The name of the subscription (FOR SELF PARTICIPANTS ONLY) */
public static final String SUBSCRIPTION_NAME = "subscription_name";
/* The exact destination stored in Contacts for this participant */
public static final String CONTACT_DESTINATION = "contact_destination";
}
// Participants table SQL
private static final String CREATE_PARTICIPANTS_TABLE_SQL =
"CREATE TABLE " + PARTICIPANTS_TABLE + "("
+ ParticipantColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ ParticipantColumns.SUB_ID + " INT DEFAULT("
+ ParticipantData.OTHER_THAN_SELF_SUB_ID + "),"
+ ParticipantColumns.SIM_SLOT_ID + " INT DEFAULT("
+ ParticipantData.INVALID_SLOT_ID + "),"
+ ParticipantColumns.NORMALIZED_DESTINATION + " TEXT,"
+ ParticipantColumns.SEND_DESTINATION + " TEXT,"
+ ParticipantColumns.DISPLAY_DESTINATION + " TEXT,"
+ ParticipantColumns.FULL_NAME + " TEXT,"
+ ParticipantColumns.FIRST_NAME + " TEXT,"
+ ParticipantColumns.PROFILE_PHOTO_URI + " TEXT, "
+ ParticipantColumns.CONTACT_ID + " INT DEFAULT( "
+ ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED + "), "
+ ParticipantColumns.LOOKUP_KEY + " STRING, "
+ ParticipantColumns.BLOCKED + " INT DEFAULT(0), "
+ ParticipantColumns.SUBSCRIPTION_NAME + " TEXT, "
+ ParticipantColumns.SUBSCRIPTION_COLOR + " INT DEFAULT(0), "
+ ParticipantColumns.CONTACT_DESTINATION + " TEXT, "
+ "UNIQUE (" + ParticipantColumns.NORMALIZED_DESTINATION + ", "
+ ParticipantColumns.SUB_ID + ") ON CONFLICT FAIL" + ");";
private static final String CREATE_SELF_PARTICIPANT_SQL =
"INSERT INTO " + PARTICIPANTS_TABLE
+ " ( " + ParticipantColumns.SUB_ID + " ) VALUES ( %s )";
static String getCreateSelfParticipantSql(int subId) {
return String.format(CREATE_SELF_PARTICIPANT_SQL, subId);
}
// Conversation Participants table schema - contains a list of participants excluding the user
// in a given conversation.
public static class ConversationParticipantsColumns implements BaseColumns {
/* participant id of someone in this conversation */
public static final String PARTICIPANT_ID = "participant_id";
/* conversation id that this participant belongs to */
public static final String CONVERSATION_ID = "conversation_id";
}
// Conversation Participants table SQL
private static final String CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL =
"CREATE TABLE " + CONVERSATION_PARTICIPANTS_TABLE + "("
+ ConversationParticipantsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ ConversationParticipantsColumns.CONVERSATION_ID + " INT,"
+ ConversationParticipantsColumns.PARTICIPANT_ID + " INT,"
+ "UNIQUE (" + ConversationParticipantsColumns.CONVERSATION_ID + ","
+ ConversationParticipantsColumns.PARTICIPANT_ID + ") ON CONFLICT FAIL, "
+ "FOREIGN KEY (" + ConversationParticipantsColumns.CONVERSATION_ID + ") "
+ "REFERENCES " + CONVERSATIONS_TABLE + "(" + ConversationColumns._ID + ")"
+ " ON DELETE CASCADE "
+ "FOREIGN KEY (" + ConversationParticipantsColumns.PARTICIPANT_ID + ")"
+ " REFERENCES " + PARTICIPANTS_TABLE + "(" + ParticipantColumns._ID + "));";
// Primary access pattern for conversation participants is to look them up for a specific
// conversation.
private static final String CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL =
"CREATE INDEX index_" + CONVERSATION_PARTICIPANTS_TABLE + "_"
+ ConversationParticipantsColumns.CONVERSATION_ID
+ " ON " + CONVERSATION_PARTICIPANTS_TABLE
+ "(" + ConversationParticipantsColumns.CONVERSATION_ID + ")";
// View for getting parts which are for draft messages.
static final String DRAFT_PARTS_VIEW_SQL = "CREATE VIEW " +
DRAFT_PARTS_VIEW + " AS SELECT "
+ PARTS_TABLE + '.' + PartColumns._ID
+ " as " + PartColumns._ID + ", "
+ PARTS_TABLE + '.' + PartColumns.MESSAGE_ID
+ " as " + PartColumns.MESSAGE_ID + ", "
+ PARTS_TABLE + '.' + PartColumns.TEXT
+ " as " + PartColumns.TEXT + ", "
+ PARTS_TABLE + '.' + PartColumns.CONTENT_URI
+ " as " + PartColumns.CONTENT_URI + ", "
+ PARTS_TABLE + '.' + PartColumns.CONTENT_TYPE
+ " as " + PartColumns.CONTENT_TYPE + ", "
+ PARTS_TABLE + '.' + PartColumns.WIDTH
+ " as " + PartColumns.WIDTH + ", "
+ PARTS_TABLE + '.' + PartColumns.HEIGHT
+ " as " + PartColumns.HEIGHT + ", "
+ MESSAGES_TABLE + '.' + MessageColumns.CONVERSATION_ID
+ " as " + MessageColumns.CONVERSATION_ID + " "
+ " FROM " + MESSAGES_TABLE + " LEFT JOIN " + PARTS_TABLE + " ON ("
+ MESSAGES_TABLE + "." + MessageColumns._ID
+ "=" + PARTS_TABLE + "." + PartColumns.MESSAGE_ID + ")"
// Exclude draft messages from main view
+ " WHERE " + MESSAGES_TABLE + "." + MessageColumns.STATUS
+ " = " + MessageData.BUGLE_STATUS_OUTGOING_DRAFT;
// List of all our SQL tables
private static final String[] CREATE_TABLE_SQLS = new String[] {
CREATE_CONVERSATIONS_TABLE_SQL,
CREATE_MESSAGES_TABLE_SQL,
CREATE_PARTS_TABLE_SQL,
CREATE_PARTICIPANTS_TABLE_SQL,
CREATE_CONVERSATION_PARTICIPANTS_TABLE_SQL,
};
// List of all our indices
private static final String[] CREATE_INDEX_SQLS = new String[] {
CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL,
CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL,
CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL,
MESSAGES_TABLE_SORT_INDEX_SQL,
MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL,
PARTS_TABLE_MESSAGE_INDEX_SQL,
CONVERSATION_PARTICIPANTS_TABLE_CONVERSATION_ID_INDEX_SQL,
};
// List of all our SQL triggers
private static final String[] CREATE_TRIGGER_SQLS = new String[] {
CREATE_PARTS_TRIGGER_SQL,
CREATE_MESSAGES_TRIGGER_SQL,
};
// List of all our views
private static final String[] CREATE_VIEW_SQLS = new String[] {
ConversationListItemData.getConversationListViewSql(),
ConversationImagePartsView.getCreateSql(),
DRAFT_PARTS_VIEW_SQL,
};
private static final Object sLock = new Object();
private final Context mApplicationContext;
private static DatabaseHelper sHelperInstance; // Protected by sLock.
private final Object mDatabaseWrapperLock = new Object();
private DatabaseWrapper mDatabaseWrapper; // Protected by mDatabaseWrapperLock.
private final DatabaseUpgradeHelper mUpgradeHelper = new DatabaseUpgradeHelper();
/**
* Get a (singleton) instance of {@link DatabaseHelper}, creating one if there isn't one yet.
* This is the only public method for getting a new instance of the class.
* @param context Should be the application context (or something that will live for the
* lifetime of the application).
* @return The current (or a new) DatabaseHelper instance.
*/
public static DatabaseHelper getInstance(final Context context) {
synchronized (sLock) {
if (sHelperInstance == null) {
sHelperInstance = new DatabaseHelper(context);
}
return sHelperInstance;
}
}
/**
* Private constructor, used from {@link #getInstance()}.
* @param context Should be the application context (or something that will live for the
* lifetime of the application).
*/
private DatabaseHelper(final Context context) {
super(context, DATABASE_NAME, null, getDatabaseVersion(context), null);
mApplicationContext = context;
}
/**
* Test method that always instantiates a new DatabaseHelper instance. This should
* be used ONLY by the tests and never by the real application.
* @param context Test context.
* @return Brand new DatabaseHelper instance.
*/
@VisibleForTesting
static DatabaseHelper getNewInstanceForTest(final Context context) {
Assert.isEngBuild();
Assert.isTrue(BugleApplication.isRunningTests());
return new DatabaseHelper(context);
}
/**
* Get the (singleton) instance of @{link DatabaseWrapper}.
* <p>The database is always opened as a writeable database.
* @return The current (or a new) DatabaseWrapper instance.
*/
@DoesNotRunOnMainThread
DatabaseWrapper getDatabase() {
// We prevent the main UI thread from accessing the database here since we have to allow
// public access to this class to enable sub-packages to access data.
Assert.isNotMainThread();
synchronized (mDatabaseWrapperLock) {
if (mDatabaseWrapper == null) {
mDatabaseWrapper = new DatabaseWrapper(mApplicationContext, getWritableDatabase());
}
return mDatabaseWrapper;
}
}
@Override
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
mUpgradeHelper.onDowngrade(db, oldVersion, newVersion);
}
/**
* Drops and recreates all tables.
*/
public static void rebuildTables(final SQLiteDatabase db) {
// Drop tables first, then views, and indices.
dropAllTables(db);
dropAllViews(db);
dropAllIndexes(db);
dropAllTriggers(db);
// Recreate the whole database.
createDatabase(db);
}
/**
* Drop and rebuild a given view.
*/
static void rebuildView(final SQLiteDatabase db, final String viewName,
final String createViewSql) {
dropView(db, viewName, true /* throwOnFailure */);
db.execSQL(createViewSql);
}
private static void dropView(final SQLiteDatabase db, final String viewName,
final boolean throwOnFailure) {
final String dropPrefix = "DROP VIEW IF EXISTS ";
try {
db.execSQL(dropPrefix + viewName);
} catch (final SQLException ex) {
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop view " + viewName + " "
+ ex);
}
if (throwOnFailure) {
throw ex;
}
}
}
/**
* Drops all user-defined tables from the given database.
*/
private static void dropAllTables(final SQLiteDatabase db) {
final Cursor tableCursor =
db.query(MASTER_TABLE, MASTER_COLUMNS, "type='table'", null, null, null, null);
if (tableCursor != null) {
try {
final String dropPrefix = "DROP TABLE IF EXISTS ";
while (tableCursor.moveToNext()) {
final String tableName = tableCursor.getString(0);
// Skip special tables
if (tableName.startsWith("android_") || tableName.startsWith("sqlite_")) {
continue;
}
try {
db.execSQL(dropPrefix + tableName);
} catch (final SQLException ex) {
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop table " + tableName + " "
+ ex);
}
}
}
} finally {
tableCursor.close();
}
}
}
/**
* Drops all user-defined triggers from the given database.
*/
private static void dropAllTriggers(final SQLiteDatabase db) {
final Cursor triggerCursor =
db.query(MASTER_TABLE, MASTER_COLUMNS, "type='trigger'", null, null, null, null);
if (triggerCursor != null) {
try {
final String dropPrefix = "DROP TRIGGER IF EXISTS ";
while (triggerCursor.moveToNext()) {
final String triggerName = triggerCursor.getString(0);
// Skip special tables
if (triggerName.startsWith("android_") || triggerName.startsWith("sqlite_")) {
continue;
}
try {
db.execSQL(dropPrefix + triggerName);
} catch (final SQLException ex) {
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop trigger " + triggerName +
" " + ex);
}
}
}
} finally {
triggerCursor.close();
}
}
}
/**
* Drops all user-defined views from the given database.
*/
private static void dropAllViews(final SQLiteDatabase db) {
final Cursor viewCursor =
db.query(MASTER_TABLE, MASTER_COLUMNS, "type='view'", null, null, null, null);
if (viewCursor != null) {
try {
while (viewCursor.moveToNext()) {
final String viewName = viewCursor.getString(0);
dropView(db, viewName, false /* throwOnFailure */);
}
} finally {
viewCursor.close();
}
}
}
/**
* Drops all user-defined views from the given database.
*/
private static void dropAllIndexes(final SQLiteDatabase db) {
final Cursor indexCursor =
db.query(MASTER_TABLE, MASTER_COLUMNS, "type='index'", null, null, null, null);
if (indexCursor != null) {
try {
final String dropPrefix = "DROP INDEX IF EXISTS ";
while (indexCursor.moveToNext()) {
final String indexName = indexCursor.getString(0);
try {
db.execSQL(dropPrefix + indexName);
} catch (final SQLException ex) {
if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.DEBUG)) {
LogUtil.d(LogUtil.BUGLE_TAG, "unable to drop index " + indexName + " "
+ ex);
}
}
}
} finally {
indexCursor.close();
}
}
}
private static void createDatabase(final SQLiteDatabase db) {
for (final String sql : CREATE_TABLE_SQLS) {
db.execSQL(sql);
}
for (final String sql : CREATE_INDEX_SQLS) {
db.execSQL(sql);
}
for (final String sql : CREATE_VIEW_SQLS) {
db.execSQL(sql);
}
for (final String sql : CREATE_TRIGGER_SQLS) {
db.execSQL(sql);
}
// Enable foreign key constraints
db.execSQL("PRAGMA foreign_keys=ON;");
// Add the default self participant. The default self will be assigned a proper slot id
// during participant refresh.
db.execSQL(getCreateSelfParticipantSql(ParticipantData.DEFAULT_SELF_SUB_ID));
DataModel.get().onCreateTables(db);
}
@Override
public void onCreate(SQLiteDatabase db) {
createDatabase(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
mUpgradeHelper.doOnUpgrade(db, oldVersion, newVersion);
}
}
@@ -1,42 +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.datamodel;
import android.database.sqlite.SQLiteDatabase;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
public class DatabaseUpgradeHelper {
private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
public void doOnUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
Assert.isTrue(newVersion >= oldVersion);
if (oldVersion == newVersion) {
return;
}
LogUtil.i(TAG, "Database upgrade started from version " + oldVersion + " to " + newVersion);
// Add future upgrade code here
}
public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
DatabaseHelper.rebuildTables(db);
LogUtil.e(TAG, "Database downgrade requested for version " +
oldVersion + " version " + newVersion + ", forcing db rebuild!");
}
}
@@ -1,482 +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.datamodel;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteFullException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import android.util.SparseArray;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.UiUtils;
import java.util.Locale;
import java.util.Stack;
import java.util.regex.Pattern;
public class DatabaseWrapper {
private static final String TAG = LogUtil.BUGLE_DATABASE_TAG;
private final SQLiteDatabase mDatabase;
private final Context mContext;
private final boolean mLog;
/**
* Set mExplainQueryPlanRegexp (via {@link BugleGservicesKeys#EXPLAIN_QUERY_PLAN_REGEXP}
* to regex matching queries to see query plans. For example, ".*" to show all query plans.
*/
// See
private final String mExplainQueryPlanRegexp;
private static final int sTimingThreshold = 50; // in milliseconds
public static final int INDEX_INSERT_MESSAGE_PART = 0;
public static final int INDEX_INSERT_MESSAGE = 1;
public static final int INDEX_QUERY_CONVERSATIONS_LATEST_MESSAGE = 2;
public static final int INDEX_QUERY_MESSAGES_LATEST_MESSAGE = 3;
private final SparseArray<SQLiteStatement> mCompiledStatements;
static class TransactionData {
long time;
boolean transactionSuccessful;
}
// track transaction on a per thread basis
private static ThreadLocal<Stack<TransactionData>> sTransactionDepth =
new ThreadLocal<Stack<TransactionData>>() {
@Override
public Stack<TransactionData> initialValue() {
return new Stack<TransactionData>();
}
};
private static String[] sFormatStrings = new String[] {
"took %d ms to %s",
" took %d ms to %s",
" took %d ms to %s",
};
DatabaseWrapper(final Context context, final SQLiteDatabase db) {
mLog = LogUtil.isLoggable(LogUtil.BUGLE_DATABASE_PERF_TAG, LogUtil.VERBOSE);
mExplainQueryPlanRegexp = Factory.get().getBugleGservices().getString(
BugleGservicesKeys.EXPLAIN_QUERY_PLAN_REGEXP, null);
mDatabase = db;
mContext = context;
mCompiledStatements = new SparseArray<SQLiteStatement>();
}
public SQLiteStatement getStatementInTransaction(final int index, final String statement) {
// Use transaction to serialize access to statements
Assert.isTrue(mDatabase.inTransaction());
SQLiteStatement compiled = mCompiledStatements.get(index);
if (compiled == null) {
compiled = mDatabase.compileStatement(statement);
Assert.isTrue(compiled.toString().contains(statement.trim()));
mCompiledStatements.put(index, compiled);
}
return compiled;
}
private void maybePlayDebugNoise() {
DebugUtils.maybePlayDebugNoise(mContext, DebugUtils.DEBUG_SOUND_DB_OP);
}
private static void printTiming(final long t1, final String msg) {
final int transactionDepth = sTransactionDepth.get().size();
final long t2 = System.currentTimeMillis();
final long delta = t2 - t1;
if (delta > sTimingThreshold) {
LogUtil.v(LogUtil.BUGLE_DATABASE_PERF_TAG, String.format(Locale.US,
sFormatStrings[Math.min(sFormatStrings.length - 1, transactionDepth)],
delta,
msg));
}
}
public Context getContext() {
return mContext;
}
public void beginTransaction() {
final long t1 = System.currentTimeMillis();
// push the current time onto the transaction stack
final TransactionData f = new TransactionData();
f.time = t1;
sTransactionDepth.get().push(f);
mDatabase.beginTransaction();
}
public void setTransactionSuccessful() {
final TransactionData f = sTransactionDepth.get().peek();
f.transactionSuccessful = true;
mDatabase.setTransactionSuccessful();
}
public void endTransaction() {
long t1 = 0;
long transactionStartTime = 0;
final TransactionData f = sTransactionDepth.get().pop();
if (f.transactionSuccessful == false) {
LogUtil.w(TAG, "endTransaction without setting successful");
for (final StackTraceElement st : (new Exception()).getStackTrace()) {
LogUtil.w(TAG, " " + st.toString());
}
}
if (mLog) {
transactionStartTime = f.time;
t1 = System.currentTimeMillis();
}
try {
mDatabase.endTransaction();
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to endTransaction", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US,
">>> endTransaction (total for this transaction: %d)",
(System.currentTimeMillis() - transactionStartTime)));
}
}
public void yieldTransaction() {
long yieldStartTime = 0;
if (mLog) {
yieldStartTime = System.currentTimeMillis();
}
final boolean wasYielded = mDatabase.yieldIfContendedSafely();
if (wasYielded && mLog) {
printTiming(yieldStartTime, "yieldTransaction");
}
}
public void insertWithOnConflict(final String searchTable, final String nullColumnHack,
final ContentValues initialValues, final int conflictAlgorithm) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
try {
mDatabase.insertWithOnConflict(searchTable, nullColumnHack, initialValues,
conflictAlgorithm);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to insertWithOnConflict", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US,
"insertWithOnConflict with ", searchTable));
}
}
private void explainQueryPlan(final SQLiteQueryBuilder qb, final SQLiteDatabase db,
final String[] projection, final String selection,
@SuppressWarnings("unused")
final String[] queryArgs,
final String groupBy,
@SuppressWarnings("unused")
final String having,
final String sortOrder, final String limit) {
final String queryString = qb.buildQuery(
projection,
selection,
groupBy,
null/*having*/,
sortOrder,
limit);
explainQueryPlan(db, queryString, queryArgs);
}
private void explainQueryPlan(final SQLiteDatabase db, final String sql,
final String[] queryArgs) {
if (!Pattern.matches(mExplainQueryPlanRegexp, sql)) {
return;
}
final Cursor planCursor = db.rawQuery("explain query plan " + sql, queryArgs);
try {
if (planCursor != null && planCursor.moveToFirst()) {
final int detailColumn = planCursor.getColumnIndex("detail");
final StringBuilder sb = new StringBuilder();
do {
sb.append(planCursor.getString(detailColumn));
sb.append("\n");
} while (planCursor.moveToNext());
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
}
LogUtil.v(TAG, "for query " + sql + "\nplan is: "
+ sb.toString());
}
} catch (final Exception e) {
LogUtil.w(TAG, "Query plan failed ", e);
} finally {
if (planCursor != null) {
planCursor.close();
}
}
}
public Cursor query(final String searchTable, final String[] projection,
final String selection, final String[] selectionArgs, final String groupBy,
final String having, final String orderBy, final String limit) {
if (mExplainQueryPlanRegexp != null) {
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(searchTable);
explainQueryPlan(qb, mDatabase, projection, selection, selectionArgs,
groupBy, having, orderBy, limit);
}
maybePlayDebugNoise();
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
final Cursor cursor = mDatabase.query(searchTable, projection, selection, selectionArgs,
groupBy, having, orderBy, limit);
if (mLog) {
printTiming(
t1,
String.format(Locale.US, "query %s with %s ==> %d",
searchTable, selection, cursor.getCount()));
}
return cursor;
}
public Cursor query(final String searchTable, final String[] columns,
final String selection, final String[] selectionArgs, final String groupBy,
final String having, final String orderBy) {
return query(
searchTable, columns, selection, selectionArgs,
groupBy, having, orderBy, null);
}
public Cursor query(final SQLiteQueryBuilder qb,
final String[] projection, final String selection, final String[] queryArgs,
final String groupBy, final String having, final String sortOrder, final String limit) {
if (mExplainQueryPlanRegexp != null) {
explainQueryPlan(qb, mDatabase, projection, selection, queryArgs,
groupBy, having, sortOrder, limit);
}
maybePlayDebugNoise();
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
final Cursor cursor = qb.query(mDatabase, projection, selection, queryArgs, groupBy,
having, sortOrder, limit);
if (mLog) {
printTiming(
t1,
String.format(Locale.US, "query %s with %s ==> %d",
qb.getTables(), selection, cursor.getCount()));
}
return cursor;
}
public long queryNumEntries(final String table, final String selection,
final String[] selectionArgs) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
final long retval =
DatabaseUtils.queryNumEntries(mDatabase, table, selection, selectionArgs);
if (mLog){
printTiming(
t1,
String.format(Locale.US, "queryNumEntries %s with %s ==> %d", table,
selection, retval));
}
return retval;
}
public Cursor rawQuery(final String sql, final String[] args) {
if (mExplainQueryPlanRegexp != null) {
explainQueryPlan(mDatabase, sql, args);
}
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
final Cursor cursor = mDatabase.rawQuery(sql, args);
if (mLog) {
printTiming(
t1,
String.format(Locale.US, "rawQuery %s ==> %d", sql, cursor.getCount()));
}
return cursor;
}
public int update(final String table, final ContentValues values,
final String selection, final String[] selectionArgs) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
int count = 0;
try {
count = mDatabase.update(table, values, selection, selectionArgs);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to update", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "update %s with %s ==> %d",
table, selection, count));
}
return count;
}
public int delete(final String table, final String whereClause, final String[] whereArgs) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
int count = 0;
try {
count = mDatabase.delete(table, whereClause, whereArgs);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to delete", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1,
String.format(Locale.US, "delete from %s with %s ==> %d", table,
whereClause, count));
}
return count;
}
public long insert(final String table, final String nullColumnHack,
final ContentValues values) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
long rowId = -1;
try {
rowId = mDatabase.insert(table, nullColumnHack, values);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to insert", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "insert to %s", table));
}
return rowId;
}
public long replace(final String table, final String nullColumnHack,
final ContentValues values) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
long rowId = -1;
try {
rowId = mDatabase.replace(table, nullColumnHack, values);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to replace", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "replace to %s", table));
}
return rowId;
}
public void setLocale(final Locale locale) {
mDatabase.setLocale(locale);
}
public void execSQL(final String sql, final String[] bindArgs) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
try {
mDatabase.execSQL(sql, bindArgs);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to execSQL", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "execSQL %s", sql));
}
}
public void execSQL(final String sql) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
try {
mDatabase.execSQL(sql);
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to execSQL", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "execSQL %s", sql));
}
}
public int execSQLUpdateDelete(final String sql) {
long t1 = 0;
if (mLog) {
t1 = System.currentTimeMillis();
}
maybePlayDebugNoise();
final SQLiteStatement statement = mDatabase.compileStatement(sql);
int rowsUpdated = 0;
try {
rowsUpdated = statement.executeUpdateDelete();
} catch (SQLiteFullException ex) {
LogUtil.e(TAG, "Database full, unable to execSQLUpdateDelete", ex);
UiUtils.showToastAtBottom(R.string.db_full);
}
if (mLog) {
printTiming(t1, String.format(Locale.US, "execSQLUpdateDelete %s", sql));
}
return rowsUpdated;
}
public SQLiteDatabase getDatabase() {
return mDatabase;
}
}
@@ -1,151 +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.datamodel;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Random;
/**
* A very simple content provider that can serve files.
*/
public abstract class FileProvider extends ContentProvider {
// Object to generate random id for temp images.
private static final Random RANDOM_ID = new Random();
abstract File getFile(final String path, final String extension);
private static final String FILE_EXTENSION_PARAM_KEY = "ext";
/**
* Check if filename conforms to requirement for our provider
* @param fileId filename (optionally starting with path character
* @return true if filename consists only of digits
*/
protected static boolean isValidFileId(final String fileId) {
// Ignore initial "/"
for (int index = (fileId.startsWith("/") ? 1 : 0); index < fileId.length(); index++) {
final Character c = fileId.charAt(index);
if (!Character.isDigit(c)) {
return false;
}
}
return true;
}
/**
* Create a temp file (to allow writing to that one particular file)
* @param file the file to create
* @return true if file successfully created
*/
protected static boolean ensureFileExists(final File file) {
try {
final File parentDir = file.getParentFile();
if (parentDir.exists() || parentDir.mkdirs()) {
return file.createNewFile();
}
} catch (final IOException e) {
// fail on exceptions creating the file
}
return false;
}
/**
* Build uri for a new temporary file (creating file)
* @param authority authority with which to populate uri
* @param extension optional file extension
* @return unique uri that can be used to write temporary files
*/
protected static Uri buildFileUri(final String authority, final String extension) {
final long fileId = Math.abs(RANDOM_ID.nextLong());
final Uri.Builder builder = (new Uri.Builder()).authority(authority).scheme(
ContentResolver.SCHEME_CONTENT);
builder.appendPath(String.valueOf(fileId));
if (!TextUtils.isEmpty(extension)) {
builder.appendQueryParameter(FILE_EXTENSION_PARAM_KEY, extension);
}
return builder.build();
}
@Override
public boolean onCreate() {
return true;
}
@Override
public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
final String fileId = uri.getPath();
if (isValidFileId(fileId)) {
final File file = getFile(fileId, getExtensionFromUri(uri));
return file.delete() ? 1 : 0;
}
return 0;
}
@Override
public ParcelFileDescriptor openFile(final Uri uri, final String fileMode)
throws FileNotFoundException {
final String fileId = uri.getPath();
if (isValidFileId(fileId)) {
final File file = getFile(fileId, getExtensionFromUri(uri));
final int mode =
(TextUtils.equals(fileMode, "r") ? ParcelFileDescriptor.MODE_READ_ONLY :
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
return ParcelFileDescriptor.open(file, mode);
}
return null;
}
protected static String getExtensionFromUri(final Uri uri) {
return uri.getQueryParameter(FILE_EXTENSION_PARAM_KEY);
}
@Override
public Cursor query(final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
// Don't support queries.
return null;
}
@Override
public Uri insert(final Uri uri, final ContentValues values) {
// Don't support inserts.
return null;
}
@Override
public int update(final Uri uri, final ContentValues values, final String selection,
final String[] selectionArgs) {
// Don't support updates.
return 0;
}
@Override
public String getType(final Uri uri) {
// No need for mime types.
return null;
}
}
@@ -1,179 +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.datamodel;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v4.util.SimpleArrayMap;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
/**
* A cursor builder that takes the frequent contacts cursor and aggregate it with the all contacts
* cursor to fill in contact details such as phone numbers and strip away invalid contacts.
*
* Because the frequent contact list depends on the loading of two cursors, it needs to temporarily
* store the cursor that it receives with setFrequents() and setAllContacts() calls. Because it
* doesn't know which one will be finished first, it always checks whether both cursors are ready
* to pull data from and construct the aggregate cursor when it's ready to do so. Note that
* this cursor builder doesn't assume ownership of the cursors passed in - it merely references
* them and always does a isClosed() check before consuming them. The ownership still belongs to
* the loader framework and the cursor may be closed when the UI is torn down.
*/
public class FrequentContactsCursorBuilder {
private Cursor mAllContactsCursor;
private Cursor mFrequentContactsCursor;
/**
* Sets the frequent contacts cursor as soon as it is loaded, or null if it's reset.
* @return this builder instance for chained operations
*/
public FrequentContactsCursorBuilder setFrequents(final Cursor frequentContactsCursor) {
mFrequentContactsCursor = frequentContactsCursor;
return this;
}
/**
* Sets the all contacts cursor as soon as it is loaded, or null if it's reset.
* @return this builder instance for chained operations
*/
public FrequentContactsCursorBuilder setAllContacts(final Cursor allContactsCursor) {
mAllContactsCursor = allContactsCursor;
return this;
}
/**
* Reset this builder. Must be called when the consumer resets its data.
*/
public void resetBuilder() {
mAllContactsCursor = null;
mFrequentContactsCursor = null;
}
/**
* Attempt to build the cursor records from the frequent and all contacts cursor if they
* are both ready to be consumed.
* @return the frequent contact cursor if built successfully, or null if it can't be built yet.
*/
public Cursor build() {
if (mFrequentContactsCursor != null && mAllContactsCursor != null) {
Assert.isTrue(!mFrequentContactsCursor.isClosed());
Assert.isTrue(!mAllContactsCursor.isClosed());
// Frequent contacts cursor has one record per contact, plus it doesn't contain info
// such as phone number and type. In order for the records to be usable by Bugle, we
// would like to populate it with information from the all contacts cursor.
final MatrixCursor retCursor = new MatrixCursor(ContactUtil.PhoneQuery.PROJECTION);
// First, go through the frequents cursor and take note of all lookup keys and their
// corresponding rank in the frequents list.
final SimpleArrayMap<String, Integer> lookupKeyToRankMap =
new SimpleArrayMap<String, Integer>();
int oldPosition = mFrequentContactsCursor.getPosition();
int rank = 0;
mFrequentContactsCursor.moveToPosition(-1);
while (mFrequentContactsCursor.moveToNext()) {
final String lookupKey = mFrequentContactsCursor.getString(
ContactUtil.INDEX_LOOKUP_KEY_FREQUENT);
lookupKeyToRankMap.put(lookupKey, rank++);
}
mFrequentContactsCursor.moveToPosition(oldPosition);
// Second, go through the all contacts cursor once and retrieve all information
// (multiple phone numbers etc.) and store that in an array list. Since the all
// contacts list only contains phone contacts, this step will ensure that we filter
// out any invalid/email contacts in the frequents list.
final ArrayList<Object[]> rows =
new ArrayList<Object[]>(mFrequentContactsCursor.getCount());
oldPosition = mAllContactsCursor.getPosition();
mAllContactsCursor.moveToPosition(-1);
while (mAllContactsCursor.moveToNext()) {
final String lookupKey = mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
if (lookupKeyToRankMap.containsKey(lookupKey)) {
final Object[] row = new Object[ContactUtil.PhoneQuery.PROJECTION.length];
row[ContactUtil.INDEX_DATA_ID] =
mAllContactsCursor.getLong(ContactUtil.INDEX_DATA_ID);
row[ContactUtil.INDEX_CONTACT_ID] =
mAllContactsCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
row[ContactUtil.INDEX_LOOKUP_KEY] =
mAllContactsCursor.getString(ContactUtil.INDEX_LOOKUP_KEY);
row[ContactUtil.INDEX_DISPLAY_NAME] =
mAllContactsCursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
row[ContactUtil.INDEX_PHOTO_URI] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHOTO_URI);
row[ContactUtil.INDEX_PHONE_EMAIL] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL);
row[ContactUtil.INDEX_PHONE_EMAIL_TYPE] =
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
rows.add(row);
}
}
mAllContactsCursor.moveToPosition(oldPosition);
// Now we have a list of rows containing frequent contacts in alphabetical order.
// Therefore, sort all the rows according to their actual ranks in the frequents list.
Collections.sort(rows, new Comparator<Object[]>() {
@Override
public int compare(final Object[] lhs, final Object[] rhs) {
final String lookupKeyLhs = (String) lhs[ContactUtil.INDEX_LOOKUP_KEY];
final String lookupKeyRhs = (String) rhs[ContactUtil.INDEX_LOOKUP_KEY];
Assert.isTrue(lookupKeyToRankMap.containsKey(lookupKeyLhs) &&
lookupKeyToRankMap.containsKey(lookupKeyRhs));
final int rankLhs = lookupKeyToRankMap.get(lookupKeyLhs);
final int rankRhs = lookupKeyToRankMap.get(lookupKeyRhs);
if (rankLhs < rankRhs) {
return -1;
} else if (rankLhs > rankRhs) {
return 1;
} else {
// Same rank, so it's two contact records for the same contact.
// Perform secondary sorting on the phone type. Always place
// mobile before everything else.
final int phoneTypeLhs = (int) lhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
final int phoneTypeRhs = (int) rhs[ContactUtil.INDEX_PHONE_EMAIL_TYPE];
if (phoneTypeLhs == Phone.TYPE_MOBILE &&
phoneTypeRhs == Phone.TYPE_MOBILE) {
return 0;
} else if (phoneTypeLhs == Phone.TYPE_MOBILE) {
return -1;
} else if (phoneTypeRhs == Phone.TYPE_MOBILE) {
return 1;
} else {
// Use the default sort order, i.e. sort by phoneType value.
return phoneTypeLhs < phoneTypeRhs ? -1 :
(phoneTypeLhs == phoneTypeRhs ? 0 : 1);
}
}
}
});
// Finally, add all the rows to this cursor.
for (final Object[] row : rows) {
retCursor.addRow(row);
}
return retCursor;
}
return null;
}
}
@@ -1,114 +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.datamodel;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import com.android.messaging.util.FallbackStrategies;
import com.android.messaging.util.FallbackStrategies.Strategy;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
/**
* Helper for querying frequent (and/or starred) contacts.
*/
public class FrequentContactsCursorQueryData extends CursorQueryData {
private static final String TAG = LogUtil.BUGLE_TAG;
private static class FrequentContactsCursorLoader extends BoundCursorLoader {
private final Uri mOriginalUri;
FrequentContactsCursorLoader(String bindingId, Context context, Uri uri,
String[] projection, String selection, String[] selectionArgs, String sortOrder) {
super(bindingId, context, uri, projection, selection, selectionArgs, sortOrder);
mOriginalUri = uri;
}
@Override
public Cursor loadInBackground() {
return FallbackStrategies
.startWith(new PrimaryStrequentContactsQueryStrategy())
.thenTry(new FrequentOnlyContactsQueryStrategy())
.thenTry(new PhoneOnlyStrequentContactsQueryStrategy())
.execute(null);
}
private abstract class StrequentContactsQueryStrategy implements Strategy<Void, Cursor> {
@Override
public Cursor execute(Void params) throws Exception {
final Uri uri = getUri();
if (uri != null) {
setUri(uri);
}
return FrequentContactsCursorLoader.super.loadInBackground();
}
protected abstract Uri getUri();
}
private class PrimaryStrequentContactsQueryStrategy extends StrequentContactsQueryStrategy {
@Override
protected Uri getUri() {
// Use the original URI requested.
return mOriginalUri;
}
}
private class FrequentOnlyContactsQueryStrategy extends StrequentContactsQueryStrategy {
@Override
protected Uri getUri() {
// Some phones have a buggy implementation of the Contacts provider which crashes
// when we query for strequent (starred+frequent) contacts (b/17991485).
// If this happens, switch to just querying for frequent contacts.
return Contacts.CONTENT_FREQUENT_URI;
}
}
private class PhoneOnlyStrequentContactsQueryStrategy extends
StrequentContactsQueryStrategy {
@Override
protected Uri getUri() {
// Some 3rd party ROMs have content provider
// implementation where invalid SQL queries are returned for regular strequent
// queries. Using strequent_phone_only query as a fallback to display only phone
// contacts. This is the last-ditch effort; if this fails, we will display an
// empty frequent list (b/18354836).
final String strequentQueryParam = OsUtil.isAtLeastL() ?
ContactsContract.STREQUENT_PHONE_ONLY : "strequent_phone_only";
// TODO: Handle enterprise contacts post M once contacts provider supports it
return Contacts.CONTENT_STREQUENT_URI.buildUpon()
.appendQueryParameter(strequentQueryParam, "true").build();
}
}
}
public FrequentContactsCursorQueryData(Context context, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
// TODO: Handle enterprise contacts post M once contacts provider supports it
super(context, Contacts.CONTENT_STREQUENT_URI, projection, selection, selectionArgs,
sortOrder);
}
@Override
public BoundCursorLoader createBoundCursorLoader(String bindingId) {
return new FrequentContactsCursorLoader(bindingId, mContext, mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
}
}
@@ -1,49 +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.datamodel;
import android.content.Context;
import android.net.Uri;
import android.provider.MediaStore.Files;
import android.provider.MediaStore.Files.FileColumns;
import android.provider.MediaStore.Images.Media;
import com.android.messaging.datamodel.data.GalleryGridItemData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.google.common.base.Joiner;
/**
* A BoundCursorLoader that reads local media on the device.
*/
public class GalleryBoundCursorLoader extends BoundCursorLoader {
public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external";
private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL);
private static final String SORT_ORDER = Media.DATE_MODIFIED + " DESC";
private static final String IMAGE_SELECTION = createSelection(
MessagePartData.ACCEPTABLE_IMAGE_TYPES,
new Integer[] { FileColumns.MEDIA_TYPE_IMAGE });
public GalleryBoundCursorLoader(final String bindingId, final Context context) {
super(bindingId, context, STORAGE_URI, GalleryGridItemData.IMAGE_PROJECTION,
IMAGE_SELECTION, null, SORT_ORDER);
}
private static String createSelection(final String[] mimeTypes, Integer[] mediaTypes) {
return Media.MIME_TYPE + " IN ('" + Joiner.on("','").join(mimeTypes) + "') AND "
+ FileColumns.MEDIA_TYPE + " IN (" + Joiner.on(',').join(mediaTypes) + ")";
}
}
@@ -1,132 +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.datamodel;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.util.List;
/**
* A very simple content provider that can serve media files from our cache directory.
*/
public class MediaScratchFileProvider extends FileProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap =
new SimpleArrayMap<Uri, String>();
@VisibleForTesting
public static final String AUTHORITY =
"com.android.messaging.datamodel.MediaScratchFileProvider";
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
public static boolean isMediaScratchSpaceUri(final Uri uri) {
if (uri == null) {
return false;
}
final List<String> segments = uri.getPathSegments();
return (TextUtils.equals(uri.getScheme(), ContentResolver.SCHEME_CONTENT) &&
TextUtils.equals(uri.getAuthority(), AUTHORITY) &&
segments.size() == 1 && FileProvider.isValidFileId(segments.get(0)));
}
/**
* Returns a uri that can be used to access a raw mms file.
*
* @return the URI for an raw mms file
*/
public static Uri buildMediaScratchSpaceUri(final String extension) {
final Uri uri = FileProvider.buildFileUri(AUTHORITY, extension);
final File file = getFileWithExtension(uri.getPath(), extension);
if (!ensureFileExists(file)) {
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
}
return uri;
}
public static File getFileFromUri(final Uri uri) {
Assert.equals(AUTHORITY, uri.getAuthority());
return getFileWithExtension(uri.getPath(), getExtensionFromUri(uri));
}
public static Uri.Builder getUriBuilder() {
return (new Uri.Builder()).authority(AUTHORITY).scheme(ContentResolver.SCHEME_CONTENT);
}
@Override
File getFile(final String path, final String extension) {
return getFileWithExtension(path, extension);
}
private static File getFileWithExtension(final String path, final String extension) {
final Context context = Factory.get().getApplicationContext();
return new File(getDirectory(context),
TextUtils.isEmpty(extension) ? path : path + "." + extension);
}
private static File getDirectory(final Context context) {
return new File(context.getCacheDir(), MEDIA_SCRATCH_SPACE_DIR);
}
@Override
public Cursor query(final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
if (projection != null && projection.length > 0 &&
TextUtils.equals(projection[0], OpenableColumns.DISPLAY_NAME) &&
isMediaScratchSpaceUri(uri)) {
// Retrieve the display name associated with a temp file. This is used by the Contacts
// ImportVCardActivity to retrieve the name of the contact(s) being imported.
String displayName;
synchronized (sUriToDisplayNameMap) {
displayName = sUriToDisplayNameMap.get(uri);
}
if (!TextUtils.isEmpty(displayName)) {
MatrixCursor cursor =
new MatrixCursor(new String[] { OpenableColumns.DISPLAY_NAME });
RowBuilder row = cursor.newRow();
row.add(displayName);
return cursor;
}
}
return null;
}
public static void addUriToDisplayNameEntry(final Uri scratchFileUri,
final String displayName) {
if (TextUtils.isEmpty(displayName)) {
return;
}
synchronized (sUriToDisplayNameMap) {
sUriToDisplayNameMap.put(scratchFileUri, displayName);
}
}
}
@@ -1,75 +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.datamodel;
import com.android.messaging.Factory;
import java.util.HashSet;
/**
* Utility abstraction which allows MemoryCaches in an application to register and then when there
* is memory pressure provide a callback to reclaim the memory in the caches.
*/
public class MemoryCacheManager {
private final HashSet<MemoryCache> mMemoryCaches = new HashSet<MemoryCache>();
private final Object mMemoryCacheLock = new Object();
public static MemoryCacheManager get() {
return Factory.get().getMemoryCacheManager();
}
/**
* Extend this interface to provide a reclaim method on a memory cache.
*/
public interface MemoryCache {
void reclaim();
}
/**
* Register the memory cache with the application.
*/
public void registerMemoryCache(final MemoryCache cache) {
synchronized (mMemoryCacheLock) {
mMemoryCaches.add(cache);
}
}
/**
* Unregister the memory cache with the application.
*/
public void unregisterMemoryCache(final MemoryCache cache) {
synchronized (mMemoryCacheLock) {
mMemoryCaches.remove(cache);
}
}
/**
* Reclaim memory in all the memory caches in the application.
*/
@SuppressWarnings("unchecked")
public void reclaimMemory() {
// We're creating a cache copy in the lock to ensure we're not working on a concurrently
// modified set, then reclaim outside of the lock to minimize the time within the lock.
final HashSet<MemoryCache> shallowCopy;
synchronized (mMemoryCacheLock) {
shallowCopy = (HashSet<MemoryCache>) mMemoryCaches.clone();
}
for (final MemoryCache cache : shallowCopy) {
cache.reclaim();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,92 +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.datamodel;
import android.telephony.SmsMessage;
import com.android.messaging.sms.MmsConfig;
public class MessageTextStats {
private boolean mMessageLengthRequiresMms;
private int mMessageCount;
private int mCodePointsRemainingInCurrentMessage;
public MessageTextStats() {
mCodePointsRemainingInCurrentMessage = Integer.MAX_VALUE;
}
public int getNumMessagesToBeSent() {
return mMessageCount;
}
public int getCodePointsRemainingInCurrentMessage() {
return mCodePointsRemainingInCurrentMessage;
}
public boolean getMessageLengthRequiresMms() {
return mMessageLengthRequiresMms;
}
public void updateMessageTextStats(final int selfSubId, final String messageText) {
final int[] params = SmsMessage.calculateLength(messageText, false);
/* SmsMessage.calculateLength returns an int[4] with:
* int[0] being the number of SMS's required,
* int[1] the number of code points used,
* int[2] is the number of code points remaining until the next message.
* int[3] is the encoding type that should be used for the message.
*/
mMessageCount = params[0];
mCodePointsRemainingInCurrentMessage = params[2];
final MmsConfig mmsConfig = MmsConfig.get(selfSubId);
if (!mmsConfig.getMultipartSmsEnabled() &&
!mmsConfig.getSendMultipartSmsAsSeparateMessages()) {
// The provider doesn't support multi-part sms's and we should use MMS to
// send multi-part sms, so as soon as the user types
// an sms longer than one segment, we have to turn the message into an mms.
mMessageLengthRequiresMms = mMessageCount > 1;
} else {
final int threshold = mmsConfig.getSmsToMmsTextThreshold();
mMessageLengthRequiresMms = threshold > 0 && mMessageCount > threshold;
}
// Some carriers require any SMS message longer than 80 to be sent as MMS
// see b/12122333
int smsToMmsLengthThreshold = mmsConfig.getSmsToMmsTextLengthThreshold();
if (smsToMmsLengthThreshold > 0) {
final int usedInCurrentMessage = params[1];
/*
* A little hacky way to find out if we should count characters in double bytes.
* SmsMessage.calculateLength counts message code units based on the characters
* in input. If all of them are ascii, the max length is
* SmsMessage.MAX_USER_DATA_SEPTETS (160). If any of them are double-byte, like
* Korean or Chinese, the max length is SmsMessage.MAX_USER_DATA_BYTES (140) bytes
* (70 code units).
* Here we check if the total code units we can use is smaller than 140. If so,
* we know we should count threshold in double-byte, so divide the threshold by 2.
* In this way, we will count Korean text correctly with regard to the length threshold.
*/
if (usedInCurrentMessage + mCodePointsRemainingInCurrentMessage
< SmsMessage.MAX_USER_DATA_BYTES) {
smsToMmsLengthThreshold /= 2;
}
if (usedInCurrentMessage > smsToMmsLengthThreshold) {
mMessageLengthRequiresMms = true;
}
}
}
}
@@ -1,476 +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.datamodel;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.text.TextUtils;
import com.android.messaging.BugleApplication;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
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.widget.BugleWidgetProvider;
import com.android.messaging.widget.WidgetConversationProvider;
import com.google.common.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
/**
* A centralized provider for Uris exposed by Bugle.
* */
public class MessagingContentProvider extends ContentProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
@VisibleForTesting
public static final String AUTHORITY =
"com.android.messaging.datamodel.MessagingContentProvider";
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
// Conversations query
private static final String CONVERSATIONS_QUERY = "conversations";
public static final Uri CONVERSATIONS_URI = Uri.parse(CONTENT_AUTHORITY + CONVERSATIONS_QUERY);
static final Uri PARTS_URI = Uri.parse(CONTENT_AUTHORITY + DatabaseHelper.PARTS_TABLE);
// Messages query
private static final String MESSAGES_QUERY = "messages";
static final Uri MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY + MESSAGES_QUERY);
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
MESSAGES_QUERY + "/conversation");
// Conversation participants query
private static final String PARTICIPANTS_QUERY = "participants";
static class ConversationParticipantsQueryColumns extends ParticipantColumns {
static final String CONVERSATION_ID = ConversationParticipantsColumns.CONVERSATION_ID;
}
static final Uri CONVERSATION_PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY +
PARTICIPANTS_QUERY + "/conversation");
public static final Uri PARTICIPANTS_URI = Uri.parse(CONTENT_AUTHORITY + PARTICIPANTS_QUERY);
// Conversation images query
private static final String CONVERSATION_IMAGES_QUERY = "conversation_images";
public static final Uri CONVERSATION_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
CONVERSATION_IMAGES_QUERY);
private static final String DRAFT_IMAGES_QUERY = "draft_images";
public static final Uri DRAFT_IMAGES_URI = Uri.parse(CONTENT_AUTHORITY +
DRAFT_IMAGES_QUERY);
/**
* Notifies that <i>all</i> data exposed by the provider needs to be refreshed.
* <p>
* <b>IMPORTANT!</b> You probably shouldn't be calling this. Prefer to notify more specific
* uri's instead. Currently only sync uses this, because sync can potentially update many
* different tables at once.
*/
public static void notifyEverythingChanged() {
final Uri uri = Uri.parse(CONTENT_AUTHORITY);
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(uri, null);
// Notify any conversations widgets the conversation list has changed.
BugleWidgetProvider.notifyConversationListChanged(context);
// Notify all conversation widgets to update.
WidgetConversationProvider.notifyMessagesChanged(context, null /*conversationId*/);
}
/**
* Build a participant uri from the conversation id.
*/
public static Uri buildConversationParticipantsUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_PARTICIPANTS_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyParticipantsChanged(final String conversationId) {
final Uri uri = buildConversationParticipantsUri(conversationId);
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(uri, null);
}
public static void notifyAllMessagesChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(CONVERSATION_MESSAGES_URI, null);
}
public static void notifyAllParticipantsChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(CONVERSATION_PARTICIPANTS_URI, null);
}
// Default value for unknown dimension of image
public static final int UNSPECIFIED_SIZE = -1;
// Internal
private static final int CONVERSATIONS_QUERY_CODE = 10;
private static final int CONVERSATION_QUERY_CODE = 20;
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
private static final int PARTICIPANTS_QUERY_CODE = 70;
// TODO: Move to a better structured URI namespace.
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY, CONVERSATIONS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
CONVERSATION_MESSAGES_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
CONVERSATION_PARTICIPANTS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, CONVERSATION_IMAGES_QUERY + "/*",
CONVERSATION_IMAGES_QUERY_CODE);
sURIMatcher.addURI(AUTHORITY, DRAFT_IMAGES_QUERY + "/*",
DRAFT_IMAGES_QUERY_CODE);
}
/**
* Build a messages uri from the conversation id.
*/
public static Uri buildConversationMessagesUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_MESSAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyMessagesChanged(final String conversationId) {
final Uri uri = buildConversationMessagesUri(conversationId);
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(uri, null);
notifyConversationListChanged();
// Notify the widget the messages changed
WidgetConversationProvider.notifyMessagesChanged(context, conversationId);
}
/**
* Build a conversation metadata uri from a conversation id.
*/
public static Uri buildConversationMetadataUri(final String conversationId) {
final Uri.Builder builder = CONVERSATIONS_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
public static void notifyConversationMetadataChanged(final String conversationId) {
final Uri uri = buildConversationMetadataUri(conversationId);
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(uri, null);
notifyConversationListChanged();
}
public static void notifyPartsChanged() {
final ContentResolver cr = Factory.get().getApplicationContext().getContentResolver();
cr.notifyChange(PARTS_URI, null);
}
public static void notifyConversationListChanged() {
final Context context = Factory.get().getApplicationContext();
final ContentResolver cr = context.getContentResolver();
cr.notifyChange(CONVERSATIONS_URI, null);
// Notify the widget the conversation list changed
BugleWidgetProvider.notifyConversationListChanged(context);
}
/**
* Build a conversation images uri from a conversation id.
*/
public static Uri buildConversationImagesUri(final String conversationId) {
final Uri.Builder builder = CONVERSATION_IMAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
/**
* Build a draft images uri from a conversation id.
*/
public static Uri buildDraftImagesUri(final String conversationId) {
final Uri.Builder builder = DRAFT_IMAGES_URI.buildUpon();
builder.appendPath(conversationId);
return builder.build();
}
private DatabaseHelper mDatabaseHelper;
private DatabaseWrapper mDatabaseWrapper;
public MessagingContentProvider() {
super();
}
@VisibleForTesting
public void setDatabaseForTest(final DatabaseWrapper db) {
Assert.isTrue(BugleApplication.isRunningTests());
mDatabaseWrapper = db;
}
private DatabaseWrapper getDatabaseWrapper() {
if (mDatabaseWrapper == null) {
mDatabaseWrapper = mDatabaseHelper.getDatabase();
}
return mDatabaseWrapper;
}
@Override
public Cursor query(final Uri uri, final String[] projection, String selection,
final String[] selectionArgs, String sortOrder) {
// Processes other than self are allowed to temporarily access the media
// scratch space; we grant uri read access on a case-by-case basis. Dialer app and
// contacts app would doQuery() on the vCard uri before trying to open the inputStream.
// There's nothing that we need to return for this uri so just No-Op.
//if (isMediaScratchSpaceUri(uri)) {
// return null;
//}
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String[] queryArgs = selectionArgs;
final int match = sURIMatcher.match(uri);
String groupBy = null;
String limit = null;
switch (match) {
case CONVERSATIONS_QUERY_CODE:
queryBuilder.setTables(ConversationListItemData.getConversationListView());
// Hide empty conversations (ones with 0 sort_timestamp)
queryBuilder.appendWhere(ConversationColumns.SORT_TIMESTAMP + " > 0 ");
break;
case CONVERSATION_QUERY_CODE:
queryBuilder.setTables(ConversationListItemData.getConversationListView());
if (uri.getPathSegments().size() == 2) {
queryBuilder.appendWhere(ConversationColumns._ID + "=?");
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case CONVERSATION_PARTICIPANTS_QUERY_CODE:
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
if (uri.getPathSegments().size() == 3 &&
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
queryBuilder.appendWhere(ParticipantColumns._ID + " IN ( " + "SELECT "
+ ConversationParticipantsColumns.PARTICIPANT_ID + " AS "
+ ParticipantColumns._ID
+ " FROM " + DatabaseHelper.CONVERSATION_PARTICIPANTS_TABLE
+ " WHERE " + ConversationParticipantsColumns.CONVERSATION_ID
+ " =? UNION SELECT " + ParticipantColumns._ID + " FROM "
+ DatabaseHelper.PARTICIPANTS_TABLE + " WHERE "
+ ParticipantColumns.SUB_ID + " != "
+ ParticipantData.OTHER_THAN_SELF_SUB_ID + " )");
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(2));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case PARTICIPANTS_QUERY_CODE:
queryBuilder.setTables(DatabaseHelper.PARTICIPANTS_TABLE);
if (uri.getPathSegments().size() != 1) {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case CONVERSATION_MESSAGES_QUERY_CODE:
if (uri.getPathSegments().size() == 3 &&
TextUtils.equals(uri.getPathSegments().get(1), "conversation")) {
// Get the conversation id from the uri
final String conversationId = uri.getPathSegments().get(2);
// We need to handle this query differently, instead of falling through to the
// generic query call at the bottom. For performance reasons, the conversation
// messages query is executed as a raw query. It is invalid to specify
// selection/sorting for this query.
if (selection == null && selectionArgs == null && sortOrder == null) {
return queryConversationMessages(conversationId, uri);
} else {
throw new IllegalArgumentException(
"Cannot set selection or sort order with this query");
}
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
case CONVERSATION_IMAGES_QUERY_CODE:
queryBuilder.setTables(ConversationImagePartsView.getViewName());
if (uri.getPathSegments().size() == 2) {
// Exclude draft.
queryBuilder.appendWhere(
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
ConversationImagePartsView.Columns.STATUS + "<>" +
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
case DRAFT_IMAGES_QUERY_CODE:
queryBuilder.setTables(ConversationImagePartsView.getViewName());
if (uri.getPathSegments().size() == 2) {
// Draft only.
queryBuilder.appendWhere(
ConversationImagePartsView.Columns.CONVERSATION_ID + " =? AND " +
ConversationImagePartsView.Columns.STATUS + "=" +
MessageData.BUGLE_STATUS_OUTGOING_DRAFT);
// Get the conversation id from the uri
queryArgs = prependArgs(queryArgs, uri.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Malformed URI " + uri);
}
break;
default: {
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
final Cursor cursor = getDatabaseWrapper().query(queryBuilder, projection, selection,
queryArgs, groupBy, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
private Cursor queryConversationMessages(final String conversationId, final Uri notifyUri) {
final String[] queryArgs = { conversationId };
final Cursor cursor = getDatabaseWrapper().rawQuery(
ConversationMessageData.getConversationMessagesQuerySql(), queryArgs);
cursor.setNotificationUri(getContext().getContentResolver(), notifyUri);
return cursor;
}
@Override
public String getType(final Uri uri) {
final StringBuilder sb = new
StringBuilder("vnd.android.cursor.dir/vnd.android.messaging.");
switch (sURIMatcher.match(uri)) {
case CONVERSATIONS_QUERY_CODE: {
sb.append(CONVERSATIONS_QUERY);
break;
}
default: {
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
return sb.toString();
}
protected DatabaseHelper getDatabase() {
return DatabaseHelper.getInstance(getContext());
}
@Override
public ParcelFileDescriptor openFile(final Uri uri, final String fileMode)
throws FileNotFoundException {
throw new IllegalArgumentException("openFile not supported: " + uri);
}
@Override
public Uri insert(final Uri uri, final ContentValues values) {
throw new IllegalStateException("Insert not supported " + uri);
}
@Override
public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
throw new IllegalArgumentException("Delete not supported: " + uri);
}
@Override
public int update(final Uri uri, final ContentValues values, final String selection,
final String[] selectionArgs) {
throw new IllegalArgumentException("Update not supported: " + uri);
}
/**
* Prepends new arguments to the existing argument list.
*
* @param oldArgList The current list of arguments. May be {@code null}
* @param args The new arguments to prepend
* @return A new argument list with the given arguments prepended
*/
private String[] prependArgs(final String[] oldArgList, final String... args) {
if (args == null || args.length == 0) {
return oldArgList;
}
final int oldArgCount = (oldArgList == null ? 0 : oldArgList.length);
final int newArgCount = args.length;
final String[] newArgs = new String[oldArgCount + newArgCount];
System.arraycopy(args, 0, newArgs, 0, newArgCount);
if (oldArgCount > 0) {
System.arraycopy(oldArgList, 0, newArgs, newArgCount, oldArgCount);
}
return newArgs;
}
/**
* {@inheritDoc}
*/
@Override
public void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
// First dump out the default SMS app package name
String defaultSmsApp = PhoneUtils.getDefault().getDefaultSmsApp();
if (TextUtils.isEmpty(defaultSmsApp)) {
if (OsUtil.isAtLeastKLP()) {
defaultSmsApp = "None";
} else {
defaultSmsApp = "None (pre-Kitkat)";
}
}
writer.println("Default SMS app: " + defaultSmsApp);
// Now dump logs
LogUtil.dump(writer);
}
@Override
public boolean onCreate() {
// This is going to wind up calling into createDatabase() below.
mDatabaseHelper = (DatabaseHelper) getDatabase();
// We cannot initialize mDatabaseWrapper yet as the Factory may not be initialized
return true;
}
}
@@ -1,69 +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.datamodel;
import android.content.Context;
import android.net.Uri;
import com.android.messaging.Factory;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
/**
* A very simple content provider that can serve mms files from our cache directory.
*/
public class MmsFileProvider extends FileProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
@VisibleForTesting
static final String AUTHORITY = "com.android.messaging.datamodel.MmsFileProvider";
private static final String RAW_MMS_DIR = "rawmms";
/**
* Returns a uri that can be used to access a raw mms file.
*
* @return the URI for an raw mms file
*/
public static Uri buildRawMmsUri() {
final Uri uri = FileProvider.buildFileUri(AUTHORITY, null);
final File file = getFile(uri.getPath());
if (!ensureFileExists(file)) {
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
}
return uri;
}
@Override
File getFile(final String path, final String extension) {
return getFile(path);
}
public static File getFile(final Uri uri) {
return getFile(uri.getPath());
}
private static File getFile(final String path) {
final Context context = Factory.get().getApplicationContext();
return new File(getDirectory(context), path + ".dat");
}
private static File getDirectory(final Context context) {
return new File(context.getCacheDir(), RAW_MMS_DIR);
}
}
@@ -1,148 +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.datamodel;
import android.app.IntentService;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.RemoteInput;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.messaging.datamodel.action.InsertNewMessageAction;
import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.conversationlist.ConversationListActivity;
import com.android.messaging.util.LogUtil;
/**
* Respond to a special intent and send an SMS message without the user's intervention, unless
* the intent extra "showUI" is true.
*/
public class NoConfirmationSmsSendService extends IntentService {
private static final String TAG = LogUtil.BUGLE_TAG;
private static final String EXTRA_SUBSCRIPTION = "subscription";
public static final String EXTRA_SELF_ID = "self_id";
public NoConfirmationSmsSendService() {
// Class name will be the thread name.
super(NoConfirmationSmsSendService.class.getName());
// Intent should be redelivered if the process gets killed before completing the job.
setIntentRedelivery(true);
}
@Override
protected void onHandleIntent(final Intent intent) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "NoConfirmationSmsSendService onHandleIntent");
}
final String action = intent.getAction();
if (!TelephonyManager.ACTION_RESPOND_VIA_MESSAGE.equals(action)) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "NoConfirmationSmsSendService onHandleIntent wrong action: " +
action);
}
return;
}
final Bundle extras = intent.getExtras();
if (extras == null) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Called to send SMS but no extras");
}
return;
}
// Get all possible extras from intent
final String conversationId =
intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
final String selfId = intent.getStringExtra(EXTRA_SELF_ID);
final boolean requiresMms = intent.getBooleanExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS,
false);
final String message = getText(intent, Intent.EXTRA_TEXT);
final String subject = getText(intent, Intent.EXTRA_SUBJECT);
final int subId = extras.getInt(EXTRA_SUBSCRIPTION, ParticipantData.DEFAULT_SELF_SUB_ID);
final Uri intentUri = intent.getData();
final String recipients = intentUri != null ? MmsUtils.getSmsRecipients(intentUri) : null;
if (TextUtils.isEmpty(recipients) && TextUtils.isEmpty(conversationId)) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Both conversationId and recipient(s) cannot be empty");
}
return;
}
if (extras.getBoolean("showUI", false)) {
startActivity(new Intent(this, ConversationListActivity.class));
} else {
if (TextUtils.isEmpty(message)) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Message cannot be empty");
}
return;
}
// TODO: it's possible that a long message would require sending it via mms,
// but we're not testing for that here and we're sending the message as an sms.
if (TextUtils.isEmpty(conversationId)) {
InsertNewMessageAction.insertNewMessage(subId, recipients, message, subject);
} else {
MessageData messageData = null;
if (requiresMms) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Auto-sending MMS message in conversation: " +
conversationId);
}
messageData = MessageData.createDraftMmsMessage(conversationId, selfId, message,
subject);
} else {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Auto-sending SMS message in conversation: " +
conversationId);
}
messageData = MessageData.createDraftSmsMessage(conversationId, selfId,
message);
}
InsertNewMessageAction.insertNewMessage(messageData);
}
UpdateMessageNotificationAction.updateMessageNotification();
}
}
private String getText(final Intent intent, final String textType) {
final String message = intent.getStringExtra(textType);
if (message == null) {
final Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
final CharSequence extra = remoteInput.getCharSequence(textType);
if (extra != null) {
return extra.toString();
}
}
}
return message;
}
}
@@ -1,149 +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.datamodel;
import android.app.PendingIntent;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.ConversationIdSet;
import java.util.ArrayList;
import java.util.HashSet;
/**
* Base class for representing notifications. The main reason for this class is that in order to
* show pictures or avatars they might need to be loaded in the background. This class and
* subclasses can do the main work to get the notification ready and then wait until any images
* that are needed are ready before posting.
*
* The creation of a notification is split into two parts. The NotificationState ctor should
* setup the basic information including the mContentIntent. A Notification Builder is created in
* RealTimeChatNotifications and passed to the build() method of each notification where the
* Notification is fully specified.
*
* TODO: There is still some duplication and inconsistency in the utility functions and
* placement of different building blocks across notification types (e.g. summary text for accounts)
*/
public abstract class NotificationState {
private static final int CONTENT_INTENT_REQUEST_CODE_OFFSET = 0;
private static final int CLEAR_INTENT_REQUEST_CODE_OFFSET = 1;
private static final int NUM_REQUEST_CODES_NEEDED = 2;
public interface FailedMessageQuery {
static final String FAILED_MESSAGES_WHERE_CLAUSE =
"((" + MessageColumns.STATUS + " = " +
MessageData.BUGLE_STATUS_OUTGOING_FAILED + " OR " +
MessageColumns.STATUS + " = " +
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED + ") AND " +
DatabaseHelper.MessageColumns.SEEN + " = 0)";
static final String FAILED_ORDER_BY = DatabaseHelper.MessageColumns.CONVERSATION_ID + ", " +
DatabaseHelper.MessageColumns.SENT_TIMESTAMP + " asc";
}
public final ConversationIdSet mConversationIds;
public final HashSet<String> mPeople;
public NotificationCompat.Style mNotificationStyle;
public NotificationCompat.Builder mNotificationBuilder;
public boolean mCanceled;
public int mType;
public int mBaseRequestCode;
public ArrayList<Uri> mParticipantAvatarsUris = null;
public ArrayList<Uri> mParticipantContactUris = null;
NotificationState(final ConversationIdSet conversationIds) {
mConversationIds = conversationIds;
mPeople = new HashSet<String>();
}
/**
* The intent to be triggered when the notification is dismissed.
*/
public abstract PendingIntent getClearIntent();
protected Uri getAttachmentUri() {
return null;
}
// Returns the mime type of the attachment (See ContentType class for definitions)
protected String getAttachmentType() {
return null;
}
/**
* Build the notification using the given builder.
* @param builder
* @return The style of the notification.
*/
protected abstract NotificationCompat.Style build(NotificationCompat.Builder builder);
protected void setAvatarUrlsForConversation(final String conversationId) {
}
protected void setPeopleForConversation(final String conversationId) {
}
/**
* Reserves request codes for this notification type. By default 2 codes are reserved, one for
* the main intent and another for the cancel intent. Override this function to reserve more.
*/
public int getNumRequestCodesNeeded() {
return NUM_REQUEST_CODES_NEEDED;
}
public int getContentIntentRequestCode() {
return mBaseRequestCode + CONTENT_INTENT_REQUEST_CODE_OFFSET;
}
public int getClearIntentRequestCode() {
return mBaseRequestCode + CLEAR_INTENT_REQUEST_CODE_OFFSET;
}
/**
* Gets the appropriate icon needed for notifications.
*/
public abstract int getIcon();
/**
* @return the type of notification that should be used from {@link RealTimeChatNotifications}
* so that the proper ringtone and vibrate settings can be used.
*/
public int getLatestMessageNotificationType() {
return BugleNotifications.LOCAL_SMS_NOTIFICATION;
}
/**
* @return the notification priority level for this notification.
*/
public abstract int getPriority();
/** @return custom ringtone URI or null if not set */
public String getRingtoneUri() {
return null;
}
public boolean getNotificationVibrate() {
return false;
}
public long getLatestReceivedTimestamp() {
return Long.MIN_VALUE;
}
}
@@ -1,738 +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.datamodel;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.graphics.Color;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v4.util.ArrayMap;
import android.telephony.SubscriptionInfo;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
import com.android.messaging.datamodel.DatabaseHelper.ConversationParticipantsColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
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.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Utility class for refreshing participant information based on matching contact. This updates
* 1. name, photo_uri, matching contact_id of participants.
* 2. generated_name of conversations.
*
* There are two kinds of participant refreshes,
* 1. Full refresh, this is triggered at application start or activity resumes after contact
* change is detected.
* 2. Partial refresh, this is triggered when a participant is added to a conversation. This
* normally happens during SMS sync.
*/
@VisibleForTesting
public class ParticipantRefresh {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
/**
* Refresh all participants including ones that were resolved before.
*/
public static final int REFRESH_MODE_FULL = 0;
/**
* Refresh all unresolved participants.
*/
public static final int REFRESH_MODE_INCREMENTAL = 1;
/**
* Force refresh all self participants.
*/
public static final int REFRESH_MODE_SELF_ONLY = 2;
public static class ConversationParticipantsQuery {
public static final String[] PROJECTION = new String[] {
ConversationParticipantsColumns._ID,
ConversationParticipantsColumns.CONVERSATION_ID,
ConversationParticipantsColumns.PARTICIPANT_ID
};
public static final int INDEX_ID = 0;
public static final int INDEX_CONVERSATION_ID = 1;
public static final int INDEX_PARTICIPANT_ID = 2;
}
// Track whether observer is initialized or not.
private static volatile boolean sObserverInitialized = false;
private static final Object sLock = new Object();
private static final AtomicBoolean sFullRefreshScheduled = new AtomicBoolean(false);
private static final Runnable sFullRefreshRunnable = new Runnable() {
@Override
public void run() {
final boolean oldScheduled = sFullRefreshScheduled.getAndSet(false);
Assert.isTrue(oldScheduled);
refreshParticipants(REFRESH_MODE_FULL);
}
};
private static final Runnable sSelfOnlyRefreshRunnable = new Runnable() {
@Override
public void run() {
refreshParticipants(REFRESH_MODE_SELF_ONLY);
}
};
/**
* A customized content resolver to track contact changes.
*/
public static class ContactContentObserver extends ContentObserver {
private volatile boolean mContactChanged = false;
public ContactContentObserver() {
super(null);
}
@Override
public void onChange(final boolean selfChange) {
super.onChange(selfChange);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Contacts changed");
}
mContactChanged = true;
}
public boolean getContactChanged() {
return mContactChanged;
}
public void resetContactChanged() {
mContactChanged = false;
}
public void initialize() {
// TODO: Handle enterprise contacts post M once contacts provider supports it
Factory.get().getApplicationContext().getContentResolver().registerContentObserver(
Phone.CONTENT_URI, true, this);
mContactChanged = true; // Force a full refresh on initialization.
}
}
/**
* Refresh participants only if needed, i.e., application start or contact changed.
*/
public static void refreshParticipantsIfNeeded() {
if (ParticipantRefresh.getNeedFullRefresh() &&
sFullRefreshScheduled.compareAndSet(false, true)) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Started full participant refresh");
}
SafeAsyncTask.executeOnThreadPool(sFullRefreshRunnable);
} else if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Skipped full participant refresh");
}
}
/**
* Refresh self participants on subscription or settings change.
*/
public static void refreshSelfParticipants() {
SafeAsyncTask.executeOnThreadPool(sSelfOnlyRefreshRunnable);
}
private static boolean getNeedFullRefresh() {
final ContactContentObserver observer = Factory.get().getContactContentObserver();
if (observer == null) {
// If there is no observer (for unittest cases), we don't need to refresh participants.
return false;
}
if (!sObserverInitialized) {
synchronized (sLock) {
if (!sObserverInitialized) {
observer.initialize();
sObserverInitialized = true;
}
}
}
return observer.getContactChanged();
}
private static void resetNeedFullRefresh() {
final ContactContentObserver observer = Factory.get().getContactContentObserver();
if (observer != null) {
observer.resetContactChanged();
}
}
/**
* This class is totally static. Make constructor to be private so that an instance
* of this class would not be created by by mistake.
*/
private ParticipantRefresh() {
}
/**
* Refresh participants in Bugle.
*
* @param refreshMode the refresh mode desired. See {@link #REFRESH_MODE_FULL},
* {@link #REFRESH_MODE_INCREMENTAL}, and {@link #REFRESH_MODE_SELF_ONLY}
*/
@VisibleForTesting
static void refreshParticipants(final int refreshMode) {
Assert.inRange(refreshMode, REFRESH_MODE_FULL, REFRESH_MODE_SELF_ONLY);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
switch (refreshMode) {
case REFRESH_MODE_FULL:
LogUtil.v(TAG, "Start full participant refresh");
break;
case REFRESH_MODE_INCREMENTAL:
LogUtil.v(TAG, "Start partial participant refresh");
break;
case REFRESH_MODE_SELF_ONLY:
LogUtil.v(TAG, "Start self participant refresh");
break;
}
}
if (!ContactUtil.hasReadContactsPermission() || !OsUtil.hasPhonePermission()) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Skipping participant referesh because of permissions");
}
return;
}
if (refreshMode == REFRESH_MODE_FULL) {
// resetNeedFullRefresh right away so that we will skip duplicated full refresh
// requests.
resetNeedFullRefresh();
}
if (refreshMode == REFRESH_MODE_FULL || refreshMode == REFRESH_MODE_SELF_ONLY) {
refreshSelfParticipantList();
}
final ArrayList<String> changedParticipants = new ArrayList<String>();
String selection = null;
String[] selectionArgs = null;
if (refreshMode == REFRESH_MODE_INCREMENTAL) {
// In case of incremental refresh, filter out participants that are already resolved.
selection = ParticipantColumns.CONTACT_ID + "=?";
selectionArgs = new String[] {
String.valueOf(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED) };
} else if (refreshMode == REFRESH_MODE_SELF_ONLY) {
// In case of self-only refresh, filter out non-self participants.
selection = SELF_PARTICIPANTS_CLAUSE;
selectionArgs = null;
}
final DatabaseWrapper db = DataModel.get().getDatabase();
Cursor cursor = null;
boolean selfUpdated = false;
try {
cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
ParticipantsQuery.PROJECTION, selection, selectionArgs, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
try {
final ParticipantData participantData =
ParticipantData.getFromCursor(cursor);
if (refreshParticipant(db, participantData)) {
if (participantData.isSelf()) {
selfUpdated = true;
}
updateParticipant(db, participantData);
final String id = participantData.getId();
changedParticipants.add(id);
}
} catch (final Exception exception) {
// Failure to update one participant shouldn't cancel the entire refresh.
// Log the failure so we know what's going on and resume the loop.
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "ParticipantRefresh: Failed to " +
"update participant", exception);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "Number of participants refreshed:" + changedParticipants.size());
}
// Refresh conversations for participants that are changed.
if (changedParticipants.size() > 0) {
BugleDatabaseOperations.refreshConversationsForParticipants(changedParticipants);
}
if (selfUpdated) {
// Boom
MessagingContentProvider.notifyAllParticipantsChanged();
MessagingContentProvider.notifyAllMessagesChanged();
}
}
private static final String SELF_PARTICIPANTS_CLAUSE = ParticipantColumns.SUB_ID
+ " NOT IN ( "
+ ParticipantData.OTHER_THAN_SELF_SUB_ID
+ " )";
private static final Set<Integer> getExistingSubIds() {
final DatabaseWrapper db = DataModel.get().getDatabase();
final HashSet<Integer> existingSubIds = new HashSet<Integer>();
Cursor cursor = null;
try {
cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
ParticipantsQuery.PROJECTION,
SELF_PARTICIPANTS_CLAUSE, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
final int subId = cursor.getInt(ParticipantsQuery.INDEX_SUB_ID);
existingSubIds.add(subId);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return existingSubIds;
}
private static final String UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL =
"UPDATE " + DatabaseHelper.PARTICIPANTS_TABLE + " SET "
+ ParticipantColumns.SIM_SLOT_ID + " = %d, "
+ ParticipantColumns.SUBSCRIPTION_COLOR + " = %d, "
+ ParticipantColumns.SUBSCRIPTION_NAME + " = %s "
+ " WHERE %s";
static String getUpdateSelfParticipantSubscriptionInfoSql(final int slotId,
final int subscriptionColor, final String subscriptionName, final String where) {
return String.format((Locale) null /* construct SQL string without localization */,
UPDATE_SELF_PARTICIPANT_SUBSCRIPTION_SQL,
slotId, subscriptionColor, subscriptionName, where);
}
/**
* Ensure that there is a self participant corresponding to every active SIM. Also, ensure
* that any other older SIM self participants are marked as inactive.
*/
private static void refreshSelfParticipantList() {
if (!OsUtil.isAtLeastL_MR1()) {
return;
}
final DatabaseWrapper db = DataModel.get().getDatabase();
final List<SubscriptionInfo> subInfoRecords =
PhoneUtils.getDefault().toLMr1().getActiveSubscriptionInfoList();
final ArrayMap<Integer, SubscriptionInfo> activeSubscriptionIdToRecordMap =
new ArrayMap<Integer, SubscriptionInfo>();
db.beginTransaction();
final Set<Integer> existingSubIds = getExistingSubIds();
try {
if (subInfoRecords != null) {
for (final SubscriptionInfo subInfoRecord : subInfoRecords) {
final int subId = subInfoRecord.getSubscriptionId();
// If its a new subscription, add it to the database.
if (!existingSubIds.contains(subId)) {
db.execSQL(DatabaseHelper.getCreateSelfParticipantSql(subId));
// Add it to the local set to guard against duplicated entries returned
// by subscription manager.
existingSubIds.add(subId);
}
activeSubscriptionIdToRecordMap.put(subId, subInfoRecord);
if (subId == PhoneUtils.getDefault().getDefaultSmsSubscriptionId()) {
// This is the system default subscription, so update the default self.
activeSubscriptionIdToRecordMap.put(ParticipantData.DEFAULT_SELF_SUB_ID,
subInfoRecord);
}
}
}
// For subscriptions already in the database, refresh ParticipantColumns.SIM_SLOT_ID.
for (final Integer subId : activeSubscriptionIdToRecordMap.keySet()) {
final SubscriptionInfo record = activeSubscriptionIdToRecordMap.get(subId);
final String displayName =
DatabaseUtils.sqlEscapeString(record.getDisplayName().toString());
db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(record.getSimSlotIndex(),
record.getIconTint(), displayName,
ParticipantColumns.SUB_ID + " = " + subId));
}
db.execSQL(getUpdateSelfParticipantSubscriptionInfoSql(
ParticipantData.INVALID_SLOT_ID, Color.TRANSPARENT, "''",
ParticipantColumns.SUB_ID + " NOT IN (" +
Joiner.on(", ").join(activeSubscriptionIdToRecordMap.keySet()) + ")"));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// Fix up conversation self ids by reverting to default self for conversations whose self
// ids are no longer active.
refreshConversationSelfIds();
}
/**
* Refresh one participant.
* @return true if the ParticipantData was changed
*/
public static boolean refreshParticipant(final DatabaseWrapper db,
final ParticipantData participantData) {
boolean updated = false;
if (participantData.isSelf()) {
final int selfChange = refreshFromSelfProfile(db, participantData);
if (selfChange == SELF_PROFILE_EXISTS) {
// If a self-profile exists, it takes precedence over Contacts data. So we are done.
return true;
}
updated = (selfChange == SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED);
// Fall-through and try to update based on Contacts data
}
updated |= refreshFromContacts(db, participantData);
return updated;
}
private static final int SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED = 1;
private static final int SELF_PROFILE_EXISTS = 2;
private static int refreshFromSelfProfile(final DatabaseWrapper db,
final ParticipantData participantData) {
int changed = 0;
// Refresh the phone number based on information from telephony
if (participantData.updatePhoneNumberForSelfIfChanged()) {
changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
}
if (OsUtil.isAtLeastL_MR1()) {
// Refresh the subscription info based on information from SubscriptionManager.
final SubscriptionInfo subscriptionInfo =
PhoneUtils.get(participantData.getSubId()).toLMr1().getActiveSubscriptionInfo();
if (participantData.updateSubscriptionInfoForSelfIfChanged(subscriptionInfo)) {
changed = SELF_PHONE_NUMBER_OR_SUBSCRIPTION_CHANGED;
}
}
// For self participant, try getting name/avatar from self profile in CP2 first.
// TODO: in case of multi-sim, profile would not be able to be used for
// different numbers. Need to figure out that.
Cursor selfCursor = null;
try {
selfCursor = ContactUtil.getSelf(db.getContext()).performSynchronousQuery();
if (selfCursor != null && selfCursor.getCount() > 0) {
selfCursor.moveToNext();
final long selfContactId = selfCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
participantData.setContactId(selfContactId);
participantData.setFullName(selfCursor.getString(
ContactUtil.INDEX_DISPLAY_NAME));
participantData.setFirstName(
ContactUtil.lookupFirstName(db.getContext(), selfContactId));
participantData.setProfilePhotoUri(selfCursor.getString(
ContactUtil.INDEX_PHOTO_URI));
participantData.setLookupKey(selfCursor.getString(
ContactUtil.INDEX_SELF_QUERY_LOOKUP_KEY));
return SELF_PROFILE_EXISTS;
}
} catch (final Exception exception) {
// It's possible for contact query to fail and we don't want that to crash our app.
// However, we need to at least log the exception so we know something was wrong.
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
"participant. exception=" + exception);
} finally {
if (selfCursor != null) {
selfCursor.close();
}
}
return changed;
}
private static boolean refreshFromContacts(final DatabaseWrapper db,
final ParticipantData participantData) {
final String normalizedDestination = participantData.getNormalizedDestination();
final long currentContactId = participantData.getContactId();
final String currentDisplayName = participantData.getFullName();
final String currentFirstName = participantData.getFirstName();
final String currentPhotoUri = participantData.getProfilePhotoUri();
final String currentContactDestination = participantData.getContactDestination();
Cursor matchingContactCursor = null;
long matchingContactId = -1;
String matchingDisplayName = null;
String matchingFirstName = null;
String matchingPhotoUri = null;
String matchingLookupKey = null;
String matchingDestination = null;
boolean updated = false;
if (TextUtils.isEmpty(normalizedDestination)) {
// The normalized destination can be "" for the self id if we can't get it from the
// SIM. Some contact providers throw an IllegalArgumentException if you lookup "",
// so we early out.
return false;
}
try {
matchingContactCursor = ContactUtil.lookupDestination(db.getContext(),
normalizedDestination).performSynchronousQuery();
if (matchingContactCursor == null || matchingContactCursor.getCount() == 0) {
// If there is no match, mark the participant as contact not found.
if (currentContactId != ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND) {
participantData.setContactId(ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND);
participantData.setFullName(null);
participantData.setFirstName(null);
participantData.setProfilePhotoUri(null);
participantData.setLookupKey(null);
updated = true;
}
return updated;
}
while (matchingContactCursor.moveToNext()) {
final long contactId = matchingContactCursor.getLong(ContactUtil.INDEX_CONTACT_ID);
// Pick either the first contact or the contact with same id as previous matched
// contact id.
if (matchingContactId == -1 || currentContactId == contactId) {
matchingContactId = contactId;
matchingDisplayName = matchingContactCursor.getString(
ContactUtil.INDEX_DISPLAY_NAME);
matchingFirstName = ContactUtil.lookupFirstName(db.getContext(), contactId);
matchingPhotoUri = matchingContactCursor.getString(
ContactUtil.INDEX_PHOTO_URI);
matchingLookupKey = matchingContactCursor.getString(
ContactUtil.INDEX_LOOKUP_KEY);
matchingDestination = matchingContactCursor.getString(
ContactUtil.INDEX_PHONE_EMAIL);
}
// There is no need to try other contacts if the current contactId was not filled...
if (currentContactId < 0
// or we found the matching contact id
|| currentContactId == contactId) {
break;
}
}
} catch (final Exception exception) {
// It's possible for contact query to fail and we don't want that to crash our app.
// However, we need to at least log the exception so we know something was wrong.
LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "Participant refresh: failed to refresh " +
"participant. exception=" + exception);
return false;
} finally {
if (matchingContactCursor != null) {
matchingContactCursor.close();
}
}
// Update participant only if something changed.
final boolean isContactIdChanged = (matchingContactId != currentContactId);
final boolean isDisplayNameChanged =
!TextUtils.equals(matchingDisplayName, currentDisplayName);
final boolean isFirstNameChanged = !TextUtils.equals(matchingFirstName, currentFirstName);
final boolean isPhotoUrlChanged = !TextUtils.equals(matchingPhotoUri, currentPhotoUri);
final boolean isDestinationChanged = !TextUtils.equals(matchingDestination,
currentContactDestination);
if (isContactIdChanged || isDisplayNameChanged || isFirstNameChanged || isPhotoUrlChanged
|| isDestinationChanged) {
participantData.setContactId(matchingContactId);
participantData.setFullName(matchingDisplayName);
participantData.setFirstName(matchingFirstName);
participantData.setProfilePhotoUri(matchingPhotoUri);
participantData.setLookupKey(matchingLookupKey);
participantData.setContactDestination(matchingDestination);
if (isDestinationChanged) {
// Update the send destination to the new one entered by user in Contacts.
participantData.setSendDestination(matchingDestination);
}
updated = true;
}
return updated;
}
/**
* Update participant with matching contact's contactId, displayName and photoUri.
*/
private static void updateParticipant(final DatabaseWrapper db,
final ParticipantData participantData) {
final ContentValues values = new ContentValues();
if (participantData.isSelf()) {
// Self participants can refresh their normalized phone numbers
values.put(ParticipantColumns.NORMALIZED_DESTINATION,
participantData.getNormalizedDestination());
values.put(ParticipantColumns.DISPLAY_DESTINATION,
participantData.getDisplayDestination());
}
values.put(ParticipantColumns.CONTACT_ID, participantData.getContactId());
values.put(ParticipantColumns.LOOKUP_KEY, participantData.getLookupKey());
values.put(ParticipantColumns.FULL_NAME, participantData.getFullName());
values.put(ParticipantColumns.FIRST_NAME, participantData.getFirstName());
values.put(ParticipantColumns.PROFILE_PHOTO_URI, participantData.getProfilePhotoUri());
values.put(ParticipantColumns.CONTACT_DESTINATION, participantData.getContactDestination());
values.put(ParticipantColumns.SEND_DESTINATION, participantData.getSendDestination());
db.beginTransaction();
try {
db.update(DatabaseHelper.PARTICIPANTS_TABLE, values, ParticipantColumns._ID + "=?",
new String[] { participantData.getId() });
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Get a list of inactive self ids in the participants table.
*/
private static List<String> getInactiveSelfParticipantIds() {
final DatabaseWrapper db = DataModel.get().getDatabase();
final List<String> inactiveSelf = new ArrayList<String>();
final String selection = ParticipantColumns.SIM_SLOT_ID + "=? AND " +
SELF_PARTICIPANTS_CLAUSE;
Cursor cursor = null;
try {
cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
new String[] { ParticipantColumns._ID },
selection, new String[] { String.valueOf(ParticipantData.INVALID_SLOT_ID) },
null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
final String participantId = cursor.getString(0);
inactiveSelf.add(participantId);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return inactiveSelf;
}
/**
* Gets a list of conversations with the given self ids.
*/
private static List<String> getConversationsWithSelfParticipantIds(final List<String> selfIds) {
final DatabaseWrapper db = DataModel.get().getDatabase();
final List<String> conversationIds = new ArrayList<String>();
Cursor cursor = null;
try {
final StringBuilder selectionList = new StringBuilder();
for (int i = 0; i < selfIds.size(); i++) {
selectionList.append('?');
if (i < selfIds.size() - 1) {
selectionList.append(',');
}
}
final String selection =
ConversationColumns.CURRENT_SELF_ID + " IN (" + selectionList + ")";
cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
new String[] { ConversationColumns._ID },
selection, selfIds.toArray(new String[0]),
null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
final String conversationId = cursor.getString(0);
conversationIds.add(conversationId);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return conversationIds;
}
/**
* Refresh one conversation's self id.
*/
private static void updateConversationSelfId(final String conversationId,
final String selfId) {
final DatabaseWrapper db = DataModel.get().getDatabase();
db.beginTransaction();
try {
BugleDatabaseOperations.updateConversationSelfIdInTransaction(db, conversationId,
selfId);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
MessagingContentProvider.notifyMessagesChanged(conversationId);
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
UIIntents.get().broadcastConversationSelfIdChange(db.getContext(), conversationId, selfId);
}
/**
* After refreshing the self participant list, find all conversations with inactive self ids,
* and switch them back to system default.
*/
private static void refreshConversationSelfIds() {
final List<String> inactiveSelfs = getInactiveSelfParticipantIds();
if (inactiveSelfs.size() == 0) {
return;
}
final List<String> conversationsToRefresh =
getConversationsWithSelfParticipantIds(inactiveSelfs);
if (conversationsToRefresh.size() == 0) {
return;
}
final DatabaseWrapper db = DataModel.get().getDatabase();
final ParticipantData defaultSelf =
BugleDatabaseOperations.getOrCreateSelf(db, ParticipantData.DEFAULT_SELF_SUB_ID);
if (defaultSelf != null) {
for (final String conversationId : conversationsToRefresh) {
updateConversationSelfId(conversationId, defaultSelf.getId());
}
}
}
}
@@ -1,478 +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.datamodel;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.provider.Telephony;
import android.support.v4.util.LongSparseArray;
import com.android.messaging.datamodel.action.SyncMessagesAction;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
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.BuglePrefsKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
/**
* This class manages message sync with the Telephony SmsProvider/MmsProvider.
*/
public class SyncManager {
private static final String TAG = LogUtil.BUGLE_TAG;
/**
* Record of any user customization to conversation settings
*/
public static class ConversationCustomization {
private final boolean mArchived;
private final boolean mMuted;
private final boolean mNoVibrate;
private final String mNotificationSoundUri;
public ConversationCustomization(final boolean archived, final boolean muted,
final boolean noVibrate, final String notificationSoundUri) {
mArchived = archived;
mMuted = muted;
mNoVibrate = noVibrate;
mNotificationSoundUri = notificationSoundUri;
}
public boolean isArchived() {
return mArchived;
}
public boolean isMuted() {
return mMuted;
}
public boolean noVibrate() {
return mNoVibrate;
}
public String getNotificationSoundUri() {
return mNotificationSoundUri;
}
}
SyncManager() {
}
/**
* Timestamp of in progress sync - used to keep track of whether sync is running
*/
private long mSyncInProgressTimestamp = -1;
/**
* Timestamp of current sync batch upper bound - used to determine if message makes batch dirty
*/
private long mCurrentUpperBoundTimestamp = -1;
/**
* Timestamp of messages inserted since sync batch started - used to determine if batch dirty
*/
private long mMaxRecentChangeTimestamp = -1L;
private final ThreadInfoCache mThreadInfoCache = new ThreadInfoCache();
/**
* User customization to conversations. If this is set, we need to recover them after
* a full sync.
*/
private LongSparseArray<ConversationCustomization> mCustomization = null;
/**
* Start an incremental sync (backed off a few seconds)
*/
public static void sync() {
SyncMessagesAction.sync();
}
/**
* Start an incremental sync (with no backoff)
*/
public static void immediateSync() {
SyncMessagesAction.immediateSync();
}
/**
* Start a full sync (for debugging)
*/
public static void forceSync() {
SyncMessagesAction.fullSync();
}
/**
* Called from data model thread when starting a sync batch
* @param upperBoundTimestamp upper bound timestamp for sync batch
*/
public synchronized void startSyncBatch(final long upperBoundTimestamp) {
Assert.isTrue(mCurrentUpperBoundTimestamp < 0);
mCurrentUpperBoundTimestamp = upperBoundTimestamp;
mMaxRecentChangeTimestamp = -1L;
}
/**
* Called from data model thread at end of batch to determine if any messages added in window
* @param lowerBoundTimestamp lower bound timestamp for sync batch
* @return true if message added within window from lower to upper bound timestamp of batch
*/
public synchronized boolean isBatchDirty(final long lowerBoundTimestamp) {
Assert.isTrue(mCurrentUpperBoundTimestamp >= 0);
final long max = mMaxRecentChangeTimestamp;
final boolean dirty = (max >= 0 && max >= lowerBoundTimestamp);
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Sync batch of messages from " + lowerBoundTimestamp
+ " to " + mCurrentUpperBoundTimestamp + " is "
+ (dirty ? "DIRTY" : "clean") + "; max change timestamp = "
+ mMaxRecentChangeTimestamp);
}
mCurrentUpperBoundTimestamp = -1L;
mMaxRecentChangeTimestamp = -1L;
return dirty;
}
/**
* Called from data model or background worker thread to indicate start of message add process
* (add must complete on that thread before action transitions to new thread/stage)
* @param timestamp timestamp of message being added
*/
public synchronized void onNewMessageInserted(final long timestamp) {
if (mCurrentUpperBoundTimestamp >= 0 && timestamp <= mCurrentUpperBoundTimestamp) {
// Message insert in current sync window
mMaxRecentChangeTimestamp = Math.max(mCurrentUpperBoundTimestamp, timestamp);
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " before upper bound of "
+ "current sync batch " + mCurrentUpperBoundTimestamp);
}
} else if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: New message @ " + timestamp + " after upper bound of "
+ "current sync batch " + mCurrentUpperBoundTimestamp);
}
}
/**
* Synchronously checks whether sync is allowed and starts sync if allowed
* @param full - true indicates a full (not incremental) sync operation
* @param startTimestamp - starttimestamp for this sync (if allowed)
* @return - true if sync should start
*/
public synchronized boolean shouldSync(final boolean full, final long startTimestamp) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SyncManager: Checking shouldSync " + (full ? "full " : "")
+ "at " + startTimestamp);
}
if (full) {
final long delayUntilFullSync = delayUntilFullSync(startTimestamp);
if (delayUntilFullSync > 0) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Full sync requested for " + startTimestamp
+ " delayed for " + delayUntilFullSync + " ms");
}
return false;
}
}
if (isSyncing()) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Not allowed to " + (full ? "full " : "")
+ "sync yet; still running sync started at " + mSyncInProgressTimestamp);
}
return false;
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Starting " + (full ? "full " : "") + "sync at "
+ startTimestamp);
}
mSyncInProgressTimestamp = startTimestamp;
return true;
}
/**
* Return delay (in ms) until allowed to run a full sync (0 meaning can run immediately)
* @param startTimestamp Timestamp used to start the sync
* @return 0 if allowed to run now, else delay in ms
*/
public long delayUntilFullSync(final long startTimestamp) {
final BugleGservices bugleGservices = BugleGservices.get();
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
final long lastFullSyncTime = prefs.getLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME, -1L);
final long smsFullSyncBackoffTimeMillis = bugleGservices.getLong(
BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS,
BugleGservicesKeys.SMS_FULL_SYNC_BACKOFF_TIME_MILLIS_DEFAULT);
final long noFullSyncBefore = (lastFullSyncTime < 0 ? startTimestamp :
lastFullSyncTime + smsFullSyncBackoffTimeMillis);
final long delayUntilFullSync = noFullSyncBefore - startTimestamp;
if (delayUntilFullSync > 0) {
return delayUntilFullSync;
}
return 0;
}
/**
* Check if sync currently in progress (public for asserts/logging).
*/
public synchronized boolean isSyncing() {
return (mSyncInProgressTimestamp >= 0);
}
/**
* Check if sync batch should be in progress - compares upperBound with in memory value
* @param upperBoundTimestamp - upperbound timestamp for sync batch
* @return - true if timestamps match (otherwise batch is orphan from older process)
*/
public synchronized boolean isSyncing(final long upperBoundTimestamp) {
Assert.isTrue(upperBoundTimestamp >= 0);
return (upperBoundTimestamp == mCurrentUpperBoundTimestamp);
}
/**
* Check if sync has completed for the first time.
*/
public boolean getHasFirstSyncCompleted() {
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
return prefs.getLong(BuglePrefsKeys.LAST_SYNC_TIME,
BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT) !=
BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT;
}
/**
* Called once sync is complete
*/
public synchronized void complete() {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Sync started at " + mSyncInProgressTimestamp
+ " marked as complete");
}
mSyncInProgressTimestamp = -1L;
// Conversation customization only used once
mCustomization = null;
}
private final ContentObserver mMmsSmsObserver = new TelephonyMessagesObserver();
private boolean mSyncOnChanges = false;
private boolean mNotifyOnChanges = false;
/**
* Register content observer when necessary and kick off a catch up sync
*/
public void updateSyncObserver(final Context context) {
registerObserver(context);
// Trigger an sms sync in case we missed and messages before registering this observer or
// becoming the SMS provider.
immediateSync();
}
private void registerObserver(final Context context) {
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
// Not default SMS app - need to actively monitor telephony but not notify
mNotifyOnChanges = false;
mSyncOnChanges = true;
} else if (OsUtil.isSecondaryUser()){
// Secondary users default SMS app - need to actively monitor telephony and notify
mNotifyOnChanges = true;
mSyncOnChanges = true;
} else {
// Primary users default SMS app - don't monitor telephony (most changes from this app)
mNotifyOnChanges = false;
mSyncOnChanges = false;
}
if (mNotifyOnChanges || mSyncOnChanges) {
context.getContentResolver().registerContentObserver(Telephony.MmsSms.CONTENT_URI,
true, mMmsSmsObserver);
} else {
context.getContentResolver().unregisterContentObserver(mMmsSmsObserver);
}
}
public synchronized void setCustomization(
final LongSparseArray<ConversationCustomization> customization) {
this.mCustomization = customization;
}
public synchronized ConversationCustomization getCustomizationForThread(final long threadId) {
if (mCustomization != null) {
return mCustomization.get(threadId);
}
return null;
}
public static void resetLastSyncTimestamps() {
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
prefs.putLong(BuglePrefsKeys.LAST_FULL_SYNC_TIME,
BuglePrefsKeys.LAST_FULL_SYNC_TIME_DEFAULT);
prefs.putLong(BuglePrefsKeys.LAST_SYNC_TIME, BuglePrefsKeys.LAST_SYNC_TIME_DEFAULT);
}
private class TelephonyMessagesObserver extends ContentObserver {
public TelephonyMessagesObserver() {
// Just run on default thread
super(null);
}
// Implement the onChange(boolean) method to delegate the change notification to
// the onChange(boolean, Uri) method to ensure correct operation on older versions
// of the framework that did not have the onChange(boolean, Uri) method.
@Override
public void onChange(final boolean selfChange) {
onChange(selfChange, null);
}
// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
@Override
public void onChange(final boolean selfChange, final Uri uri) {
// Handle change.
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "SyncManager: Sms/Mms DB changed @" + System.currentTimeMillis()
+ " for " + (uri == null ? "<unk>" : uri.toString()) + " "
+ mSyncOnChanges + "/" + mNotifyOnChanges);
}
if (mSyncOnChanges) {
// If sync is already running this will do nothing - but at end of each sync
// action there is a check for recent messages that should catch new changes.
SyncManager.immediateSync();
}
if (mNotifyOnChanges) {
// TODO: Secondary users are not going to get notifications
}
}
}
public ThreadInfoCache getThreadInfoCache() {
return mThreadInfoCache;
}
public static class ThreadInfoCache {
// Cache of thread->conversationId map
private final LongSparseArray<String> mThreadToConversationId =
new LongSparseArray<String>();
// Cache of thread->recipients map
private final LongSparseArray<List<String>> mThreadToRecipients =
new LongSparseArray<List<String>>();
// Remember the conversation ids that need to be archived
private final HashSet<String> mArchivedConversations = new HashSet<>();
public synchronized void clear() {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "SyncManager: Cleared ThreadInfoCache");
}
mThreadToConversationId.clear();
mThreadToRecipients.clear();
mArchivedConversations.clear();
}
public synchronized boolean isArchived(final String conversationId) {
return mArchivedConversations.contains(conversationId);
}
/**
* Get or create a conversation based on the message's thread id
*
* @param threadId The message's thread
* @param refSubId The subId used for normalizing phone numbers in the thread
* @param customization The user setting customization to the conversation if any
* @return The existing conversation id or new conversation id
*/
public synchronized String getOrCreateConversation(final DatabaseWrapper db,
final long threadId, int refSubId, final ConversationCustomization customization) {
// This function has several components which need to be atomic.
Assert.isTrue(db.getDatabase().inTransaction());
// If we already have this conversation ID in our local map, just return it
String conversationId = mThreadToConversationId.get(threadId);
if (conversationId != null) {
return conversationId;
}
final List<String> recipients = getThreadRecipients(threadId);
final ArrayList<ParticipantData> participants =
BugleDatabaseOperations.getConversationParticipantsFromRecipients(recipients,
refSubId);
if (customization != null) {
// There is user customization we need to recover
conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
customization.isArchived(), participants, customization.isMuted(),
customization.noVibrate(), customization.getNotificationSoundUri());
if (customization.isArchived()) {
mArchivedConversations.add(conversationId);
}
} else {
conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
false/*archived*/, participants, false/*noNotification*/,
false/*noVibrate*/, null/*soundUri*/);
}
if (conversationId != null) {
mThreadToConversationId.put(threadId, conversationId);
return conversationId;
}
return null;
}
/**
* Load the recipients of a thread from telephony provider. If we fail, use
* a predefined unknown recipient. This should not return null.
*
* @param threadId
*/
public synchronized List<String> getThreadRecipients(final long threadId) {
List<String> recipients = mThreadToRecipients.get(threadId);
if (recipients == null) {
recipients = MmsUtils.getRecipientsByThread(threadId);
if (recipients != null && recipients.size() > 0) {
mThreadToRecipients.put(threadId, recipients);
}
}
if (recipients == null || recipients.isEmpty()) {
LogUtil.w(TAG, "SyncManager : using unknown sender since thread " + threadId +
" couldn't find any recipients.");
// We want to try our best to load the messages,
// so if recipient info is broken, try to fix it with unknown recipient
recipients = Lists.newArrayList();
recipients.add(ParticipantData.getUnknownSenderDestination());
}
return recipients;
}
}
}
@@ -1,295 +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.datamodel.action;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelException;
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
import com.android.messaging.datamodel.action.ActionMonitor.ActionExecutedListener;
import com.android.messaging.util.LogUtil;
import java.util.LinkedList;
import java.util.List;
/**
* Base class for operations that perform application business logic off the main UI thread while
* holding a wake lock.
* .
* Note all derived classes need to provide real implementation of Parcelable (this is abstract)
*/
public abstract class Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
// Members holding the parameters common to all actions - no action state
public final String actionKey;
// If derived classes keep their data in actionParameters then parcelable is trivial
protected Bundle actionParameters;
// This does not get written to the parcel
private final List<Action> mBackgroundActions = new LinkedList<Action>();
/**
* Process the action locally - runs on action service thread.
* TODO: Currently, there is no way for this method to indicate failure
* @return result to be passed in to {@link ActionExecutedListener#onActionExecuted}. It is
* also the result passed in to {@link ActionCompletedListener#onActionSucceeded} if
* there is no background work.
*/
protected Object executeAction() {
return null;
}
/**
* Queues up background work ie. {@link #doBackgroundWork} will be called on the
* background worker thread.
*/
protected void requestBackgroundWork() {
mBackgroundActions.add(this);
}
/**
* Queues up background actions for background processing after the current action has
* completed its processing ({@link #executeAction}, {@link processBackgroundCompletion}
* or {@link #processBackgroundFailure}) on the Action thread.
* @param backgroundAction
*/
protected void requestBackgroundWork(final Action backgroundAction) {
mBackgroundActions.add(backgroundAction);
}
/**
* Return flag indicating if any actions have been queued
*/
public boolean hasBackgroundActions() {
return !mBackgroundActions.isEmpty();
}
/**
* Send queued actions to the background worker provided
*/
public void sendBackgroundActions(final BackgroundWorker worker) {
worker.queueBackgroundWork(mBackgroundActions);
mBackgroundActions.clear();
}
/**
* Do work in a long running background worker thread.
* {@link #requestBackgroundWork} needs to be called for this method to
* be called. {@link #processBackgroundFailure} will be called on the Action service thread
* if this method throws {@link DataModelException}.
* @return response that is to be passed to {@link #processBackgroundResponse}
*/
protected Bundle doBackgroundWork() throws DataModelException {
return null;
}
/**
* Process the success response from the background worker. Runs on action service thread.
* @param response the response returned by {@link #doBackgroundWork}
* @return result to be passed in to {@link ActionCompletedListener#onActionSucceeded}
*/
protected Object processBackgroundResponse(final Bundle response) {
return null;
}
/**
* Called in case of failures when sending background actions. Runs on action service thread
* @return result to be passed in to {@link ActionCompletedListener#onActionFailed}
*/
protected Object processBackgroundFailure() {
return null;
}
/**
* Constructor
*/
protected Action(final String key) {
this.actionKey = key;
this.actionParameters = new Bundle();
}
/**
* Constructor
*/
protected Action() {
this.actionKey = generateUniqueActionKey(getClass().getSimpleName());
this.actionParameters = new Bundle();
}
/**
* Queue an action and monitor for processing by the ActionService via the factory helper
*/
protected void start(final ActionMonitor monitor) {
ActionMonitor.registerActionMonitor(this.actionKey, monitor);
DataModel.startActionService(this);
}
/**
* Queue an action for processing by the ActionService via the factory helper
*/
public void start() {
DataModel.startActionService(this);
}
/**
* Queue an action for delayed processing by the ActionService via the factory helper
*/
public void schedule(final int requestCode, final long delayMs) {
DataModel.scheduleAction(this, requestCode, delayMs);
}
/**
* Called when action queues ActionService intent
*/
protected final void markStart() {
ActionMonitor.setState(this, ActionMonitor.STATE_CREATED,
ActionMonitor.STATE_QUEUED);
}
/**
* Mark the beginning of local action execution
*/
protected final void markBeginExecute() {
ActionMonitor.setState(this, ActionMonitor.STATE_QUEUED,
ActionMonitor.STATE_EXECUTING);
}
/**
* Mark the end of local action execution - either completes the action or queues
* background actions
*/
protected final void markEndExecute(final Object result) {
final boolean hasBackgroundActions = hasBackgroundActions();
ActionMonitor.setExecutedState(this, ActionMonitor.STATE_EXECUTING,
hasBackgroundActions, result);
if (!hasBackgroundActions) {
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_EXECUTING,
result, true);
}
}
/**
* Update action state to indicate that the background worker is starting
*/
protected final void markBackgroundWorkStarting() {
ActionMonitor.setState(this,
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
}
/**
* Update action state to indicate that the background worker has posted its response
* (or failure) to the Action service
*/
protected final void markBackgroundCompletionQueued() {
ActionMonitor.setState(this,
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED);
}
/**
* Update action state to indicate the background action failed but is being re-queued for retry
*/
protected final void markBackgroundWorkQueued() {
ActionMonitor.setState(this,
ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
}
/**
* Called by ActionService to process a response from the background worker
* @param response the response returned by {@link #doBackgroundWork}
*/
protected final void processBackgroundWorkResponse(final Bundle response) {
ActionMonitor.setState(this,
ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED,
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE);
final Object result = processBackgroundResponse(response);
ActionMonitor.setCompleteState(this,
ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE, result, true);
}
/**
* Called by ActionService when a background action fails
*/
protected final void processBackgroundWorkFailure() {
final Object result = processBackgroundFailure();
ActionMonitor.setCompleteState(this, ActionMonitor.STATE_UNDEFINED,
result, false);
}
private static final Object sLock = new Object();
private static long sActionIdx = System.currentTimeMillis() * 1000;
/**
* Helper method to generate a unique operation index
*/
protected static long getActionIdx() {
long idx = 0;
synchronized (sLock) {
idx = ++sActionIdx;
}
return idx;
}
/**
* This helper can be used to generate a unique key used to identify an action.
* @param baseKey - key generated to identify the action parameters
* @return - composite key generated by appending unique index
*/
protected static String generateUniqueActionKey(final String baseKey) {
final StringBuilder key = new StringBuilder();
if (!TextUtils.isEmpty(baseKey)) {
key.append(baseKey);
}
key.append(":");
key.append(getActionIdx());
return key.toString();
}
/**
* Most derived classes use this base implementation (unless they include files handles)
*/
@Override
public int describeContents() {
return 0;
}
/**
* Derived classes need to implement writeToParcel (but typically should call this method
* to parcel Action member variables before they parcel their member variables).
*/
public void writeActionToParcel(final Parcel parcel, final int flags) {
parcel.writeString(this.actionKey);
parcel.writeBundle(this.actionParameters);
}
/**
* Helper for derived classes to implement parcelable
*/
public Action(final Parcel in) {
this.actionKey = in.readString();
// Note: Need to set classloader to ensure we can un-parcel classes from this package
this.actionParameters = in.readBundle(Action.class.getClassLoader());
}
}
@@ -1,477 +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.datamodel.action;
import android.os.Handler;
import android.support.v4.util.SimpleArrayMap;
import android.text.TextUtils;
import com.android.messaging.util.Assert.RunsOnAnyThread;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.ThreadUtil;
import com.google.common.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Base class for action monitors
* Actions come in various flavors but
* o) Fire and forget - no monitor
* o) Immediate local processing only - will trigger ActionCompletedListener when done
* o) Background worker processing only - will trigger ActionCompletedListener when done
* o) Immediate local processing followed by background work followed by more local processing
* - will trigger ActionExecutedListener once local processing complete and
* ActionCompletedListener when second set of local process (dealing with background
* worker response) is complete
*/
public class ActionMonitor {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
/**
* Interface used to notify on completion of local execution for an action
*/
public interface ActionExecutedListener {
/**
* @param result value returned by {@link Action#executeAction}
*/
@RunsOnMainThread
abstract void onActionExecuted(ActionMonitor monitor, final Action action,
final Object data, final Object result);
}
/**
* Interface used to notify action completion
*/
public interface ActionCompletedListener {
/**
* @param result object returned from processing the action. This is the value returned by
* {@link Action#executeAction} if there is no background work, or
* else the value returned by
* {@link Action#processBackgroundResponse}
*/
@RunsOnMainThread
abstract void onActionSucceeded(ActionMonitor monitor,
final Action action, final Object data, final Object result);
/**
* @param result value returned by {@link Action#processBackgroundFailure}
*/
@RunsOnMainThread
abstract void onActionFailed(ActionMonitor monitor, final Action action,
final Object data, final Object result);
}
/**
* Interface for being notified of action state changes - used for profiling, testing only
*/
protected interface ActionStateChangedListener {
/**
* @param action the action that is changing state
* @param state the new state of the action
*/
@RunsOnAnyThread
void onActionStateChanged(Action action, int state);
}
/**
* Operations always start out as STATE_CREATED and finish as STATE_COMPLETE.
* Some common state transition sequences in between include:
* <ul>
* <li>Local data change only : STATE_QUEUED - STATE_EXECUTING
* <li>Background worker request only : STATE_BACKGROUND_ACTIONS_QUEUED
* - STATE_EXECUTING_BACKGROUND_ACTION
* - STATE_BACKGROUND_COMPLETION_QUEUED
* - STATE_PROCESSING_BACKGROUND_RESPONSE
* <li>Local plus background worker request : STATE_QUEUED - STATE_EXECUTING
* - STATE_BACKGROUND_ACTIONS_QUEUED
* - STATE_EXECUTING_BACKGROUND_ACTION
* - STATE_BACKGROUND_COMPLETION_QUEUED
* - STATE_PROCESSING_BACKGROUND_RESPONSE
* </ul>
*/
protected static final int STATE_UNDEFINED = 0;
protected static final int STATE_CREATED = 1; // Just created
protected static final int STATE_QUEUED = 2; // Action queued for processing
protected static final int STATE_EXECUTING = 3; // Action processing on datamodel thread
protected static final int STATE_BACKGROUND_ACTIONS_QUEUED = 4;
protected static final int STATE_EXECUTING_BACKGROUND_ACTION = 5;
// The background work has completed, either returning a success response or resulting in a
// failure
protected static final int STATE_BACKGROUND_COMPLETION_QUEUED = 6;
protected static final int STATE_PROCESSING_BACKGROUND_RESPONSE = 7;
protected static final int STATE_COMPLETE = 8; // Action complete
/**
* Lock used to protect access to state and listeners
*/
private final Object mLock = new Object();
/**
* Current state of action
*/
@VisibleForTesting
protected int mState;
/**
* Listener which is notified on action completion
*/
private ActionCompletedListener mCompletedListener;
/**
* Listener which is notified on action executed
*/
private ActionExecutedListener mExecutedListener;
/**
* Listener which is notified of state changes
*/
private ActionStateChangedListener mStateChangedListener;
/**
* Handler used to post results back to caller
*/
private final Handler mHandler;
/**
* Data passed back to listeners (associated with the action when it is created)
*/
private final Object mData;
/**
* The action key is used to determine equivalence of operations and their requests
*/
private final String mActionKey;
/**
* Get action key identifying associated action
*/
public String getActionKey() {
return mActionKey;
}
/**
* Unregister listeners so that they will not be called back - override this method if needed
*/
public void unregister() {
clearListeners();
}
/**
* Unregister listeners so that they will not be called
*/
protected final void clearListeners() {
synchronized (mLock) {
mCompletedListener = null;
mExecutedListener = null;
}
}
/**
* Create a monitor associated with a particular action instance
*/
protected ActionMonitor(final int initialState, final String actionKey,
final Object data) {
mHandler = ThreadUtil.getMainThreadHandler();
mActionKey = actionKey;
mState = initialState;
mData = data;
}
/**
* Return flag to indicate if action is complete
*/
public boolean isComplete() {
boolean complete = false;
synchronized (mLock) {
complete = (mState == STATE_COMPLETE);
}
return complete;
}
/**
* Set listener that will be called with action completed result
*/
protected final void setCompletedListener(final ActionCompletedListener listener) {
synchronized (mLock) {
mCompletedListener = listener;
}
}
/**
* Set listener that will be called with local execution result
*/
protected final void setExecutedListener(final ActionExecutedListener listener) {
synchronized (mLock) {
mExecutedListener = listener;
}
}
/**
* Set listener that will be called with local execution result
*/
protected final void setStateChangedListener(final ActionStateChangedListener listener) {
synchronized (mLock) {
mStateChangedListener = listener;
}
}
/**
* Perform a state update transition
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param newState - new state which will be set
*/
@VisibleForTesting
protected void updateState(final Action action, final int expectedOldState,
final int newState) {
ActionStateChangedListener listener = null;
synchronized (mLock) {
if (expectedOldState != STATE_UNDEFINED &&
mState != expectedOldState) {
throw new IllegalStateException("On updateState to " + newState + " was " + mState
+ " expecting " + expectedOldState);
}
if (newState != mState) {
mState = newState;
listener = mStateChangedListener;
}
}
if (listener != null) {
listener.onActionStateChanged(action, newState);
}
}
/**
* Perform a state update transition
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param newState - new state which will be set
*/
static void setState(final Action action, final int expectedOldState,
final int newState) {
int oldMonitorState = expectedOldState;
int newMonitorState = newState;
final ActionMonitor monitor
= ActionMonitor.lookupActionMonitor(action.actionKey);
if (monitor != null) {
oldMonitorState = monitor.mState;
monitor.updateState(action, expectedOldState, newState);
newMonitorState = monitor.mState;
}
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
+ "UTC State = " + oldMonitorState + " - " + newMonitorState);
}
}
/**
* Mark action complete
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param result - object returned from processing the action. This is the value returned by
* {@link Action#executeAction} if there is no background work, or
* else the value returned by {@link Action#processBackgroundResponse}
* or {@link Action#processBackgroundFailure}
*/
private final void complete(final Action action,
final int expectedOldState, final Object result,
final boolean succeeded) {
ActionCompletedListener completedListener = null;
synchronized (mLock) {
setState(action, expectedOldState, STATE_COMPLETE);
completedListener = mCompletedListener;
mExecutedListener = null;
mStateChangedListener = null;
}
if (completedListener != null) {
// Marshal to UI thread
mHandler.post(new Runnable() {
@Override
public void run() {
ActionCompletedListener listener = null;
synchronized (mLock) {
if (mCompletedListener != null) {
listener = mCompletedListener;
}
mCompletedListener = null;
}
if (listener != null) {
if (succeeded) {
listener.onActionSucceeded(ActionMonitor.this,
action, mData, result);
} else {
listener.onActionFailed(ActionMonitor.this,
action, mData, result);
}
}
}
});
}
}
/**
* Mark action complete
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param result - object returned from processing the action. This is the value returned by
* {@link Action#executeAction} if there is no background work, or
* else the value returned by {@link Action#processBackgroundResponse}
* or {@link Action#processBackgroundFailure}
*/
static void setCompleteState(final Action action, final int expectedOldState,
final Object result, final boolean succeeded) {
int oldMonitorState = expectedOldState;
final ActionMonitor monitor
= ActionMonitor.lookupActionMonitor(action.actionKey);
if (monitor != null) {
oldMonitorState = monitor.mState;
monitor.complete(action, expectedOldState, result, succeeded);
unregisterActionMonitorIfComplete(action.actionKey, monitor);
}
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
+ "UTC State = " + oldMonitorState + " - " + STATE_COMPLETE);
}
}
/**
* Mark action complete
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param hasBackgroundActions - has the completing action requested background work
* @param result - the return value of {@link Action#executeAction}
*/
final void executed(final Action action,
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
ActionExecutedListener executedListener = null;
synchronized (mLock) {
if (hasBackgroundActions) {
setState(action, expectedOldState, STATE_BACKGROUND_ACTIONS_QUEUED);
}
executedListener = mExecutedListener;
}
if (executedListener != null) {
// Marshal to UI thread
mHandler.post(new Runnable() {
@Override
public void run() {
ActionExecutedListener listener = null;
synchronized (mLock) {
if (mExecutedListener != null) {
listener = mExecutedListener;
mExecutedListener = null;
}
}
if (listener != null) {
listener.onActionExecuted(ActionMonitor.this,
action, mData, result);
}
}
});
}
}
/**
* Mark action complete
* @param action - action whose state is updating
* @param expectedOldState - expected existing state of action (can be UNKNOWN)
* @param hasBackgroundActions - has the completing action requested background work
* @param result - the return value of {@link Action#executeAction}
*/
static void setExecutedState(final Action action,
final int expectedOldState, final boolean hasBackgroundActions, final Object result) {
int oldMonitorState = expectedOldState;
final ActionMonitor monitor
= ActionMonitor.lookupActionMonitor(action.actionKey);
if (monitor != null) {
oldMonitorState = monitor.mState;
monitor.executed(action, expectedOldState, hasBackgroundActions, result);
}
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
LogUtil.v(TAG, "Operation-" + action.actionKey + ": @" + df.format(new Date())
+ "UTC State = " + oldMonitorState + " - EXECUTED");
}
}
/**
* Map of action monitors indexed by actionKey
*/
@VisibleForTesting
static SimpleArrayMap<String, ActionMonitor> sActionMonitors =
new SimpleArrayMap<String, ActionMonitor>();
/**
* Insert new monitor into map
*/
static void registerActionMonitor(final String actionKey,
final ActionMonitor monitor) {
if (monitor != null
&& (TextUtils.isEmpty(monitor.getActionKey())
|| TextUtils.isEmpty(actionKey)
|| !actionKey.equals(monitor.getActionKey()))) {
throw new IllegalArgumentException("Monitor key " + monitor.getActionKey()
+ " not compatible with action key " + actionKey);
}
synchronized (sActionMonitors) {
sActionMonitors.put(actionKey, monitor);
}
}
/**
* Find monitor associated with particular action
*/
private static ActionMonitor lookupActionMonitor(final String actionKey) {
ActionMonitor monitor = null;
synchronized (sActionMonitors) {
monitor = sActionMonitors.get(actionKey);
}
return monitor;
}
/**
* Remove monitor from map
*/
@VisibleForTesting
static void unregisterActionMonitor(final String actionKey,
final ActionMonitor monitor) {
if (monitor != null) {
synchronized (sActionMonitors) {
sActionMonitors.remove(actionKey);
}
}
}
/**
* Remove monitor from map if the action is complete
*/
static void unregisterActionMonitorIfComplete(final String actionKey,
final ActionMonitor monitor) {
if (monitor != null && monitor.isComplete()) {
synchronized (sActionMonitors) {
sActionMonitors.remove(actionKey);
}
}
}
}
@@ -1,63 +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.datamodel.action;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Bundle;
/**
* Class providing interface for the ActionService - can be stubbed for testing
*/
public class ActionService {
protected static PendingIntent makeStartActionPendingIntent(final Context context,
final Action action, final int requestCode, final boolean launchesAnActivity) {
return ActionServiceImpl.makeStartActionPendingIntent(context, action, requestCode,
launchesAnActivity);
}
/**
* Start an action by posting it over the the ActionService
*/
public void startAction(final Action action) {
ActionServiceImpl.startAction(action);
}
/**
* Schedule a delayed action by posting it over the the ActionService
*/
public void scheduleAction(final Action action, final int code,
final long delayMs) {
ActionServiceImpl.scheduleAction(action, code, delayMs);
}
/**
* Process a response from the BackgroundWorker in the ActionService
*/
protected void handleResponseFromBackgroundWorker(
final Action action, final Bundle response) {
ActionServiceImpl.handleResponseFromBackgroundWorker(action, response);
}
/**
* Process a failure from the BackgroundWorker in the ActionService
*/
protected void handleFailureFromBackgroundWorker(final Action action,
final Exception exception) {
ActionServiceImpl.handleFailureFromBackgroundWorker(action, exception);
}
}
@@ -1,341 +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.datamodel.action;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.LoggingTimer;
import com.android.messaging.util.WakeLockHelper;
import com.google.common.annotations.VisibleForTesting;
/**
* ActionService used to perform background processing for data model
*/
public class ActionServiceImpl extends IntentService {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final boolean VERBOSE = false;
public ActionServiceImpl() {
super("ActionService");
}
/**
* Start action by sending intent to the service
* @param action - action to start
*/
protected static void startAction(final Action action) {
final Intent intent = makeIntent(OP_START_ACTION);
final Bundle actionBundle = new Bundle();
actionBundle.putParcelable(BUNDLE_ACTION, action);
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
action.markStart();
startServiceWithIntent(intent);
}
/**
* Schedule an action to run after specified delay using alarm manager to send pendingintent
* @param action - action to start
* @param requestCode - request code used to collapse requests
* @param delayMs - delay in ms (from now) before action will start
*/
protected static void scheduleAction(final Action action, final int requestCode,
final long delayMs) {
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
final Bundle actionBundle = new Bundle();
actionBundle.putParcelable(BUNDLE_ACTION, action);
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
PendingActionReceiver.scheduleAlarm(intent, requestCode, delayMs);
}
/**
* Handle response returned by BackgroundWorker
* @param request - request generating response
* @param response - response from service
*/
protected static void handleResponseFromBackgroundWorker(final Action action,
final Bundle response) {
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_RESPONSE);
final Bundle actionBundle = new Bundle();
actionBundle.putParcelable(BUNDLE_ACTION, action);
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
intent.putExtra(EXTRA_WORKER_RESPONSE, response);
startServiceWithIntent(intent);
}
/**
* Handle response returned by BackgroundWorker
* @param request - request generating failure
*/
protected static void handleFailureFromBackgroundWorker(final Action action,
final Exception exception) {
final Intent intent = makeIntent(OP_RECEIVE_BACKGROUND_FAILURE);
final Bundle actionBundle = new Bundle();
actionBundle.putParcelable(BUNDLE_ACTION, action);
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
intent.putExtra(EXTRA_WORKER_EXCEPTION, exception);
startServiceWithIntent(intent);
}
// ops
@VisibleForTesting
protected static final int OP_START_ACTION = 200;
@VisibleForTesting
protected static final int OP_RECEIVE_BACKGROUND_RESPONSE = 201;
@VisibleForTesting
protected static final int OP_RECEIVE_BACKGROUND_FAILURE = 202;
// extras
@VisibleForTesting
protected static final String EXTRA_OP_CODE = "op";
@VisibleForTesting
protected static final String EXTRA_ACTION_BUNDLE = "datamodel_action_bundle";
@VisibleForTesting
protected static final String EXTRA_WORKER_EXCEPTION = "worker_exception";
@VisibleForTesting
protected static final String EXTRA_WORKER_RESPONSE = "worker_response";
@VisibleForTesting
protected static final String EXTRA_WORKER_UPDATE = "worker_update";
@VisibleForTesting
protected static final String BUNDLE_ACTION = "bundle_action";
private BackgroundWorker mBackgroundWorker;
/**
* Allocate an intent with a specific opcode.
*/
private static Intent makeIntent(final int opcode) {
final Intent intent = new Intent(Factory.get().getApplicationContext(),
ActionServiceImpl.class);
intent.putExtra(EXTRA_OP_CODE, opcode);
return intent;
}
/**
* Broadcast receiver for alarms scheduled through ActionService.
*/
public static class PendingActionReceiver extends BroadcastReceiver {
static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
/**
* Allocate an intent with a specific opcode and alarm action.
*/
public static Intent makeIntent(final int opcode) {
final Intent intent = new Intent(Factory.get().getApplicationContext(),
PendingActionReceiver.class);
intent.setAction(ACTION);
intent.putExtra(EXTRA_OP_CODE, opcode);
return intent;
}
public static void scheduleAlarm(final Intent intent, final int requestCode,
final long delayMs) {
final Context context = Factory.get().getApplicationContext();
final PendingIntent pendingIntent = PendingIntent.getBroadcast(
context, requestCode, intent, PendingIntent.FLAG_CANCEL_CURRENT);
final AlarmManager mgr =
(AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
if (delayMs < Long.MAX_VALUE) {
mgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + delayMs, pendingIntent);
} else {
mgr.cancel(pendingIntent);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onReceive(final Context context, final Intent intent) {
ActionServiceImpl.startServiceWithIntent(intent);
}
}
/**
* Creates a pending intent that will trigger a data model action when the intent is
* triggered
*/
public static PendingIntent makeStartActionPendingIntent(final Context context,
final Action action, final int requestCode, final boolean launchesAnActivity) {
final Intent intent = PendingActionReceiver.makeIntent(OP_START_ACTION);
final Bundle actionBundle = new Bundle();
actionBundle.putParcelable(BUNDLE_ACTION, action);
intent.putExtra(EXTRA_ACTION_BUNDLE, actionBundle);
if (launchesAnActivity) {
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
}
return PendingIntent.getBroadcast(context, requestCode, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
/**
* {@inheritDoc}
*/
@Override
public void onCreate() {
super.onCreate();
mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
DataModel.get().getConnectivityUtil().registerForSignalStrength();
}
@Override
public void onDestroy() {
super.onDestroy();
DataModel.get().getConnectivityUtil().unregisterForSignalStrength();
}
private static final String WAKELOCK_ID = "bugle_datamodel_service_wakelock";
@VisibleForTesting
static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID);
/**
* Queue intent to the ActionService after acquiring wake lock
*/
private static void startServiceWithIntent(final Intent intent) {
final Context context = Factory.get().getApplicationContext();
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
// Increase refCount on wake lock - acquiring if necessary
if (VERBOSE) {
LogUtil.v(TAG, "acquiring wakelock for opcode " + opcode);
}
sWakeLock.acquire(context, intent, opcode);
intent.setClass(context, ActionServiceImpl.class);
// TODO: Note that intent will be quietly discarded if it exceeds available rpc
// memory (in total around 1MB). See this article for background
// http://developer.android.com/reference/android/os/TransactionTooLargeException.html
// Perhaps we should keep large structures in the action monitor?
if (context.startService(intent) == null) {
LogUtil.e(TAG,
"ActionService.startServiceWithIntent: failed to start service for intent "
+ intent);
sWakeLock.release(intent, opcode);
}
}
/**
* {@inheritDoc}
*/
@Override
protected void onHandleIntent(final Intent intent) {
if (intent == null) {
// Shouldn't happen but sometimes does following another crash.
LogUtil.w(TAG, "ActionService.onHandleIntent: Called with null intent");
return;
}
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
sWakeLock.ensure(intent, opcode);
try {
Action action;
final Bundle actionBundle = intent.getBundleExtra(EXTRA_ACTION_BUNDLE);
actionBundle.setClassLoader(getClassLoader());
switch(opcode) {
case OP_START_ACTION: {
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
executeAction(action);
break;
}
case OP_RECEIVE_BACKGROUND_RESPONSE: {
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
final Bundle response = intent.getBundleExtra(EXTRA_WORKER_RESPONSE);
processBackgroundResponse(action, response);
break;
}
case OP_RECEIVE_BACKGROUND_FAILURE: {
action = (Action) actionBundle.getParcelable(BUNDLE_ACTION);
processBackgroundFailure(action);
break;
}
default:
throw new RuntimeException("Unrecognized opcode in ActionServiceImpl");
}
action.sendBackgroundActions(mBackgroundWorker);
} finally {
// Decrease refCount on wake lock - releasing if necessary
sWakeLock.release(intent, opcode);
}
}
private static final long EXECUTION_TIME_WARN_LIMIT_MS = 1000; // 1 second
/**
* Local execution of action on ActionService thread
*/
private void executeAction(final Action action) {
action.markBeginExecute();
final LoggingTimer timer = createLoggingTimer(action, "#executeAction");
timer.start();
final Object result = action.executeAction();
timer.stopAndLog();
action.markEndExecute(result);
}
/**
* Process response on ActionService thread
*/
private void processBackgroundResponse(final Action action, final Bundle response) {
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundResponse");
timer.start();
action.processBackgroundWorkResponse(response);
timer.stopAndLog();
}
/**
* Process failure on ActionService thread
*/
private void processBackgroundFailure(final Action action) {
final LoggingTimer timer = createLoggingTimer(action, "#processBackgroundFailure");
timer.start();
action.processBackgroundWorkFailure();
timer.stopAndLog();
}
private static LoggingTimer createLoggingTimer(
final Action action, final String methodName) {
return new LoggingTimer(TAG, action.getClass().getSimpleName() + methodName,
EXECUTION_TIME_WARN_LIMIT_MS);
}
}
@@ -1,32 +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.datamodel.action;
import java.util.List;
/**
* Interface between action service and its workers
*/
public class BackgroundWorker {
/**
* Send list of requests from action service to a worker
*/
public void queueBackgroundWork(final List<Action> backgroundActions) {
BackgroundWorkerService.queueBackgroundWork(backgroundActions);
}
}
@@ -1,168 +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.datamodel.action;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelException;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.LoggingTimer;
import com.android.messaging.util.WakeLockHelper;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
/**
* Background worker service is an initial example of a background work queue handler
* Used to actually "send" messages which may take some time and should not block ActionService
* or UI
*/
public class BackgroundWorkerService extends IntentService {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final boolean VERBOSE = false;
private static final String WAKELOCK_ID = "bugle_background_worker_wakelock";
@VisibleForTesting
static WakeLockHelper sWakeLock = new WakeLockHelper(WAKELOCK_ID);
private final ActionService mHost;
public BackgroundWorkerService() {
super("BackgroundWorker");
mHost = DataModel.get().getActionService();
}
/**
* Queue a list of requests from action service to this worker
*/
public static void queueBackgroundWork(final List<Action> actions) {
for (final Action action : actions) {
startServiceWithAction(action, 0);
}
}
// ops
@VisibleForTesting
protected static final int OP_PROCESS_REQUEST = 400;
// extras
@VisibleForTesting
protected static final String EXTRA_OP_CODE = "op";
@VisibleForTesting
protected static final String EXTRA_ACTION = "action";
@VisibleForTesting
protected static final String EXTRA_ATTEMPT = "retry_attempt";
/**
* Queue action intent to the BackgroundWorkerService after acquiring wake lock
*/
private static void startServiceWithAction(final Action action,
final int retryCount) {
final Intent intent = new Intent();
intent.putExtra(EXTRA_ACTION, action);
intent.putExtra(EXTRA_ATTEMPT, retryCount);
startServiceWithIntent(OP_PROCESS_REQUEST, intent);
}
/**
* Queue intent to the BackgroundWorkerService after acquiring wake lock
*/
private static void startServiceWithIntent(final int opcode, final Intent intent) {
final Context context = Factory.get().getApplicationContext();
intent.setClass(context, BackgroundWorkerService.class);
intent.putExtra(EXTRA_OP_CODE, opcode);
sWakeLock.acquire(context, intent, opcode);
if (VERBOSE) {
LogUtil.v(TAG, "acquiring wakelock for opcode " + opcode);
}
if (context.startService(intent) == null) {
LogUtil.e(TAG,
"BackgroundWorkerService.startServiceWithAction: failed to start service for "
+ opcode);
sWakeLock.release(intent, opcode);
}
}
@Override
protected void onHandleIntent(final Intent intent) {
if (intent == null) {
// Shouldn't happen but sometimes does following another crash.
LogUtil.w(TAG, "BackgroundWorkerService.onHandleIntent: Called with null intent");
return;
}
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
sWakeLock.ensure(intent, opcode);
try {
switch(opcode) {
case OP_PROCESS_REQUEST: {
final Action action = intent.getParcelableExtra(EXTRA_ACTION);
final int attempt = intent.getIntExtra(EXTRA_ATTEMPT, -1);
doBackgroundWork(action, attempt);
break;
}
default:
throw new RuntimeException("Unrecognized opcode in BackgroundWorkerService");
}
} finally {
sWakeLock.release(intent, opcode);
}
}
/**
* Local execution of background work for action on ActionService thread
*/
private void doBackgroundWork(final Action action, final int attempt) {
action.markBackgroundWorkStarting();
Bundle response = null;
try {
final LoggingTimer timer = new LoggingTimer(
TAG, action.getClass().getSimpleName() + "#doBackgroundWork");
timer.start();
response = action.doBackgroundWork();
timer.stopAndLog();
action.markBackgroundCompletionQueued();
mHost.handleResponseFromBackgroundWorker(action, response);
} catch (final Exception exception) {
final boolean retry = false;
LogUtil.e(TAG, "Error in background worker", exception);
if (!(exception instanceof DataModelException)) {
// DataModelException is expected (sort-of) and handled in handleFailureFromWorker
// below, but other exceptions should crash ENG builds
Assert.fail("Unexpected error in background worker - abort");
}
if (retry) {
action.markBackgroundWorkQueued();
startServiceWithAction(action, attempt + 1);
} else {
action.markBackgroundCompletionQueued();
mHost.handleFailureFromBackgroundWorker(action, exception);
}
}
}
}
@@ -1,172 +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.datamodel.action;
import android.content.Context;
import android.content.res.Resources;
import android.widget.Toast;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.ThreadUtil;
import javax.annotation.Nullable;
/**
* Shows one-time, transient notifications in response to action failures (i.e. permanent failures
* when sending a message) by showing toasts.
*/
public class BugleActionToasts {
/**
* Called when SendMessageAction or DownloadMmsAction finishes
* @param conversationId the conversation of the sent or downloaded message
* @param success did the action succeed
* @param status the message sending status
* @param isSms whether the message is sent using SMS
* @param subId the subId of the SIM related to this send
* @param isSend whether it is a send (false for download)
*/
static void onSendMessageOrManualDownloadActionCompleted(
final String conversationId,
final boolean success,
final int status,
final boolean isSms,
final int subId,
final boolean isSend) {
// We only show notifications for two cases, i.e. when mobile data is off or when we are
// in airplane mode, both of which fail fast with permanent failures.
if (!success && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
final PhoneUtils phoneUtils = PhoneUtils.get(subId);
if (phoneUtils.isAirplaneModeOn()) {
if (isSend) {
showToast(R.string.send_message_failure_airplane_mode);
} else {
showToast(R.string.download_message_failure_airplane_mode);
}
return;
} else if (!isSms && !phoneUtils.isMobileDataEnabled()) {
if (isSend) {
showToast(R.string.send_message_failure_no_data);
} else {
showToast(R.string.download_message_failure_no_data);
}
return;
}
}
if (AccessibilityUtil.isTouchExplorationEnabled(Factory.get().getApplicationContext())) {
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(conversationId);
if (isFocusedConversation && success) {
// Using View.announceForAccessibility may be preferable, but we do not have a
// View, and so we use a toast instead.
showToast(isSend ? R.string.send_message_success
: R.string.download_message_success);
return;
}
// {@link MessageNotificationState#checkFailedMessages} does not post a notification for
// failures in observable conversations. For accessibility, we provide an indication
// here.
final boolean isObservableConversation = DataModel.get().isNewMessageObservable(
conversationId);
if (isObservableConversation && !success) {
showToast(isSend ? R.string.send_message_failure
: R.string.download_message_failure);
}
}
}
public static void onMessageReceived(final String conversationId,
@Nullable final ParticipantData sender, @Nullable final MessageData message) {
final Context context = Factory.get().getApplicationContext();
if (AccessibilityUtil.isTouchExplorationEnabled(context)) {
final boolean isFocusedConversation = DataModel.get().isFocusedConversation(
conversationId);
if (isFocusedConversation) {
final Resources res = context.getResources();
final String senderDisplayName = (sender == null)
? res.getString(R.string.unknown_sender) : sender.getDisplayName(false);
final String announcement = res.getString(
R.string.incoming_message_announcement, senderDisplayName,
(message == null) ? "" : message.getMessageText());
showToast(announcement);
}
}
}
public static void onConversationDeleted() {
showToast(R.string.conversation_deleted);
}
private static void showToast(final int messageResId) {
ThreadUtil.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),
getApplicationContext().getString(messageResId), Toast.LENGTH_LONG).show();
}
});
}
private static void showToast(final String message) {
ThreadUtil.getMainThreadHandler().post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
});
}
private static Context getApplicationContext() {
return Factory.get().getApplicationContext();
}
private static class UpdateDestinationBlockedActionToast
implements UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener {
private final Context mContext;
UpdateDestinationBlockedActionToast(final Context context) {
mContext = context;
}
@Override
public void onUpdateDestinationBlockedAction(
final UpdateDestinationBlockedAction action,
final boolean success,
final boolean block,
final String destination) {
if (success) {
Toast.makeText(mContext,
block
? R.string.update_destination_blocked
: R.string.update_destination_unblocked,
Toast.LENGTH_LONG
).show();
}
}
}
public static UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
makeUpdateDestinationBlockedActionListener(final Context context) {
return new UpdateDestinationBlockedActionToast(context);
}
}
@@ -1,205 +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.datamodel.action;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelException;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.widget.WidgetConversationProvider;
import java.util.ArrayList;
import java.util.List;
/**
* Action used to delete a conversation.
*/
public class DeleteConversationAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
public static void deleteConversation(final String conversationId, final long cutoffTimestamp) {
final DeleteConversationAction action = new DeleteConversationAction(conversationId,
cutoffTimestamp);
action.start();
}
private static final String KEY_CONVERSATION_ID = "conversation_id";
private static final String KEY_CUTOFF_TIMESTAMP = "cutoff_timestamp";
private DeleteConversationAction(final String conversationId, final long cutoffTimestamp) {
super();
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
// TODO: Should we set cuttoff timestamp to prevent us deleting new messages?
actionParameters.putLong(KEY_CUTOFF_TIMESTAMP, cutoffTimestamp);
}
// Delete conversation from both the local DB and telephony in the background so sync cannot
// run concurrently and incorrectly try to recreate the conversation's messages locally. The
// telephony database can sometimes be quite slow to delete conversations, so we delete from
// the local DB first, notify the UI, and then delete from telephony.
@Override
protected Bundle doBackgroundWork() throws DataModelException {
final DatabaseWrapper db = DataModel.get().getDatabase();
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
final long cutoffTimestamp = actionParameters.getLong(KEY_CUTOFF_TIMESTAMP);
if (!TextUtils.isEmpty(conversationId)) {
// First find the thread id for this conversation.
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
if (BugleDatabaseOperations.deleteConversation(db, conversationId, cutoffTimestamp)) {
LogUtil.i(TAG, "DeleteConversationAction: Deleted local conversation "
+ conversationId);
BugleActionToasts.onConversationDeleted();
// Remove notifications if necessary
BugleNotifications.update(true /* silent */, null /* conversationId */,
BugleNotifications.UPDATE_MESSAGES);
// We have changed the conversation list
MessagingContentProvider.notifyConversationListChanged();
// Notify the widget the conversation is deleted so it can go into its configure state.
WidgetConversationProvider.notifyConversationDeleted(
Factory.get().getApplicationContext(),
conversationId);
} else {
LogUtil.w(TAG, "DeleteConversationAction: Could not delete local conversation "
+ conversationId);
return null;
}
// Now delete from telephony DB. MmsSmsProvider throws an exception if the thread id is
// less than 0. If it's greater than zero, it will delete all messages with that thread
// id, even if there's no corresponding row in the threads table.
if (threadId >= 0) {
final int count = MmsUtils.deleteThread(threadId, cutoffTimestamp);
if (count > 0) {
LogUtil.i(TAG, "DeleteConversationAction: Deleted telephony thread "
+ threadId + " (cutoffTimestamp = " + cutoffTimestamp + ")");
} else {
LogUtil.w(TAG, "DeleteConversationAction: Could not delete thread from "
+ "telephony: conversationId = " + conversationId + ", thread id = "
+ threadId);
}
} else {
LogUtil.w(TAG, "DeleteConversationAction: Local conversation " + conversationId
+ " has an invalid telephony thread id; will delete messages individually");
deleteConversationMessagesFromTelephony();
}
} else {
LogUtil.e(TAG, "DeleteConversationAction: conversationId is empty");
}
return null;
}
/**
* Deletes all the telephony messages for the local conversation being deleted.
* <p>
* This is a fallback used when the conversation is not associated with any telephony thread,
* or its thread id is invalid (e.g. negative). This is not common, but can happen sometimes
* (e.g. the Unknown Sender conversation). In the usual case of deleting a conversation, we
* don't need this because the telephony provider automatically deletes messages when a thread
* is deleted.
*/
private void deleteConversationMessagesFromTelephony() {
final DatabaseWrapper db = DataModel.get().getDatabase();
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
Assert.notNull(conversationId);
final List<Uri> messageUris = new ArrayList<>();
Cursor cursor = null;
try {
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
new String[] { MessageColumns.SMS_MESSAGE_URI },
MessageColumns.CONVERSATION_ID + "=?",
new String[] { conversationId },
null, null, null);
while (cursor.moveToNext()) {
String messageUri = cursor.getString(0);
try {
messageUris.add(Uri.parse(messageUri));
} catch (Exception e) {
LogUtil.e(TAG, "DeleteConversationAction: Could not parse message uri "
+ messageUri);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
for (Uri messageUri : messageUris) {
int count = MmsUtils.deleteMessage(messageUri);
if (count > 0) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "DeleteConversationAction: Deleted telephony message "
+ messageUri);
}
} else {
LogUtil.w(TAG, "DeleteConversationAction: Could not delete telephony message "
+ messageUri);
}
}
}
@Override
protected Object executeAction() {
requestBackgroundWork();
return null;
}
private DeleteConversationAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<DeleteConversationAction> CREATOR
= new Parcelable.Creator<DeleteConversationAction>() {
@Override
public DeleteConversationAction createFromParcel(final Parcel in) {
return new DeleteConversationAction(in);
}
@Override
public DeleteConversationAction[] newArray(final int size) {
return new DeleteConversationAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,135 +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.datamodel.action;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.LogUtil;
/**
* Action used to delete a single message.
*/
public class DeleteMessageAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
public static void deleteMessage(final String messageId) {
final DeleteMessageAction action = new DeleteMessageAction(messageId);
action.start();
}
private static final String KEY_MESSAGE_ID = "message_id";
private DeleteMessageAction(final String messageId) {
super();
actionParameters.putString(KEY_MESSAGE_ID, messageId);
}
// Doing this work in the background so that we're not competing with sync
// which could bring the deleted message back to life between the time we deleted
// it locally and deleted it in telephony (sync is also done on doBackgroundWork).
//
// Previously this block of code deleted from telephony first but that can be very
// slow (on the order of seconds) so this was modified to first delete locally, trigger
// the UI update, then delete from telephony.
@Override
protected Bundle doBackgroundWork() {
final DatabaseWrapper db = DataModel.get().getDatabase();
// First find the thread id for this conversation.
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
if (!TextUtils.isEmpty(messageId)) {
// Check message still exists
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
if (message != null) {
// Delete from local DB
int count = BugleDatabaseOperations.deleteMessage(db, messageId);
if (count > 0) {
LogUtil.i(TAG, "DeleteMessageAction: Deleted local message "
+ messageId);
} else {
LogUtil.w(TAG, "DeleteMessageAction: Could not delete local message "
+ messageId);
}
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
// We may have changed the conversation list
MessagingContentProvider.notifyConversationListChanged();
final Uri messageUri = message.getSmsMessageUri();
if (messageUri != null) {
// Delete from telephony DB
count = MmsUtils.deleteMessage(messageUri);
if (count > 0) {
LogUtil.i(TAG, "DeleteMessageAction: Deleted telephony message "
+ messageUri);
} else {
LogUtil.w(TAG, "DeleteMessageAction: Could not delete message from "
+ "telephony: messageId = " + messageId + ", telephony uri = "
+ messageUri);
}
} else {
LogUtil.i(TAG, "DeleteMessageAction: Local message " + messageId
+ " has no telephony uri.");
}
} else {
LogUtil.w(TAG, "DeleteMessageAction: Message " + messageId + " no longer exists");
}
}
return null;
}
/**
* Delete the message.
*/
@Override
protected Object executeAction() {
requestBackgroundWork();
return null;
}
private DeleteMessageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<DeleteMessageAction> CREATOR
= new Parcelable.Creator<DeleteMessageAction>() {
@Override
public DeleteMessageAction createFromParcel(final Parcel in) {
return new DeleteMessageAction(in);
}
@Override
public DeleteMessageAction[] newArray(final int size) {
return new DeleteMessageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,340 +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.datamodel.action;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.LogUtil;
/**
* Downloads an MMS message.
* <p>
* This class is public (not package-private) because the SMS/MMS (e.g. MmsUtils) classes need to
* access the EXTRA_* fields for setting up the 'downloaded' pending intent.
*/
public class DownloadMmsAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
/**
* Interface for DownloadMmsAction listeners
*/
public interface DownloadMmsActionListener {
@RunsOnMainThread
abstract void onDownloadMessageStarting(final ActionMonitor monitor,
final Object data, final MessageData message);
@RunsOnMainThread
abstract void onDownloadMessageSucceeded(final ActionMonitor monitor,
final Object data, final MessageData message);
@RunsOnMainThread
abstract void onDownloadMessageFailed(final ActionMonitor monitor,
final Object data, final MessageData message);
}
/**
* Queue download of an mms notification message (can only be called during execute of action)
*/
static boolean queueMmsForDownloadInBackground(final String messageId,
final Action processingAction) {
// When this method is being called, it is always from auto download
final DownloadMmsAction action = new DownloadMmsAction();
// This could queue nothing
return action.queueAction(messageId, processingAction);
}
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_CONVERSATION_ID = "conversation_id";
private static final String KEY_PARTICIPANT_ID = "participant_id";
private static final String KEY_CONTENT_LOCATION = "content_location";
private static final String KEY_TRANSACTION_ID = "transaction_id";
private static final String KEY_NOTIFICATION_URI = "notification_uri";
private static final String KEY_SUB_ID = "sub_id";
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
private static final String KEY_FAILURE_STATUS = "failure_status";
// Values we attach to the pending intent that's fired when the message is downloaded.
// Only applicable when downloading via the platform APIs on L+.
public static final String EXTRA_MESSAGE_ID = "message_id";
public static final String EXTRA_CONTENT_URI = "content_uri";
public static final String EXTRA_NOTIFICATION_URI = "notification_uri";
public static final String EXTRA_SUB_ID = "sub_id";
public static final String EXTRA_SUB_PHONE_NUMBER = "sub_phone_number";
public static final String EXTRA_TRANSACTION_ID = "transaction_id";
public static final String EXTRA_CONTENT_LOCATION = "content_location";
public static final String EXTRA_AUTO_DOWNLOAD = "auto_download";
public static final String EXTRA_RECEIVED_TIMESTAMP = "received_timestamp";
public static final String EXTRA_CONVERSATION_ID = "conversation_id";
public static final String EXTRA_PARTICIPANT_ID = "participant_id";
public static final String EXTRA_STATUS_IF_FAILED = "status_if_failed";
private DownloadMmsAction() {
super();
}
@Override
protected Object executeAction() {
Assert.fail("DownloadMmsAction must be queued rather than started");
return null;
}
protected boolean queueAction(final String messageId, final Action processingAction) {
actionParameters.putString(KEY_MESSAGE_ID, messageId);
final DatabaseWrapper db = DataModel.get().getDatabase();
// Read the message from local db
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
if (message != null && message.canDownloadMessage()) {
final Uri notificationUri = message.getSmsMessageUri();
final String conversationId = message.getConversationId();
final int status = message.getStatus();
final String selfId = message.getSelfId();
final ParticipantData self = BugleDatabaseOperations
.getExistingParticipant(db, selfId);
final int subId = self.getSubId();
actionParameters.putInt(KEY_SUB_ID, subId);
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
actionParameters.putString(KEY_PARTICIPANT_ID, message.getParticipantId());
actionParameters.putString(KEY_CONTENT_LOCATION, message.getMmsContentLocation());
actionParameters.putString(KEY_TRANSACTION_ID, message.getMmsTransactionId());
actionParameters.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
actionParameters.putBoolean(KEY_AUTO_DOWNLOAD, isAutoDownload(status));
final long now = System.currentTimeMillis();
if (message.getInDownloadWindow(now)) {
// We can still retry
actionParameters.putString(KEY_SUB_PHONE_NUMBER, self.getNormalizedDestination());
final int downloadingStatus = getDownloadingStatus(status);
// Update message status to indicate downloading.
updateMessageStatus(notificationUri, messageId, conversationId,
downloadingStatus, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
// Pre-compute the next status when failed so we don't have to load from db again
actionParameters.putInt(KEY_FAILURE_STATUS, getFailureStatus(downloadingStatus));
// Actual download happens in background
processingAction.requestBackgroundWork(this);
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG,
"DownloadMmsAction: Queued download of MMS message " + messageId);
}
return true;
} else {
LogUtil.w(TAG, "DownloadMmsAction: Download of MMS message " + messageId
+ " failed (outside download window)");
// Retries depleted and we failed. Update the message status so we won't retry again
updateMessageStatus(notificationUri, messageId, conversationId,
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED,
MessageData.RAW_TELEPHONY_STATUS_UNDEFINED);
if (status == MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD) {
// For auto download failure, we should send a DEFERRED NotifyRespInd
// to carrier to indicate we will manual download later
ProcessDownloadedMmsAction.sendDeferredRespStatus(
messageId, message.getMmsTransactionId(),
message.getMmsContentLocation(), subId);
return true;
}
}
}
return false;
}
/**
* Find out the auto download state of this message based on its starting status
*
* @param status The starting status of the message.
* @return True if this is a message doing auto downloading, false otherwise
*/
private static boolean isAutoDownload(final int status) {
switch (status) {
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
return false;
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
return true;
default:
Assert.fail("isAutoDownload: invalid input status " + status);
return false;
}
}
/**
* Get the corresponding downloading status based on the starting status of the message
*
* @param status The starting status of the message.
* @return The downloading status
*/
private static int getDownloadingStatus(final int status) {
switch (status) {
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
return MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING;
default:
Assert.fail("isAutoDownload: invalid input status " + status);
return MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING;
}
}
/**
* Get the corresponding failed status based on the current downloading status
*
* @param status The downloading status
* @return The status the message should have if downloading failed
*/
private static int getFailureStatus(final int status) {
switch (status) {
case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD;
case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
default:
Assert.fail("isAutoDownload: invalid input status " + status);
return MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD;
}
}
@Override
protected Bundle doBackgroundWork() {
final Context context = Factory.get().getApplicationContext();
final int subId = actionParameters.getInt(KEY_SUB_ID);
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
final Uri notificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI);
final String subPhoneNumber = actionParameters.getString(KEY_SUB_PHONE_NUMBER);
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
final long receivedTimestampRoundedToSecond =
1000 * ((System.currentTimeMillis() + 500) / 1000);
LogUtil.i(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
+ " (" + (autoDownload ? "auto" : "manual") + ")");
// Bundle some values we'll need after the message is downloaded (via platform APIs)
final Bundle extras = new Bundle();
extras.putString(EXTRA_MESSAGE_ID, messageId);
extras.putString(EXTRA_CONVERSATION_ID, conversationId);
extras.putString(EXTRA_PARTICIPANT_ID, participantId);
extras.putInt(EXTRA_STATUS_IF_FAILED, statusIfFailed);
// Start the download
final MmsUtils.StatusPlusUri status = MmsUtils.downloadMmsMessage(context,
notificationUri, subId, subPhoneNumber, transactionId, contentLocation,
autoDownload, receivedTimestampRoundedToSecond / 1000L, extras);
if (status == MmsUtils.STATUS_PENDING) {
// Async download; no status yet
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "DownloadMmsAction: Downloading MMS message " + messageId
+ " asynchronously; waiting for pending intent to signal completion");
}
} else {
// Inform sync that message has been added at local received timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(receivedTimestampRoundedToSecond);
// Handle downloaded message
ProcessDownloadedMmsAction.processMessageDownloadFastFailed(messageId,
notificationUri, conversationId, participantId, contentLocation, subId,
subPhoneNumber, statusIfFailed, autoDownload, transactionId,
status.resultCode);
}
return null;
}
@Override
protected Object processBackgroundResponse(final Bundle response) {
// Nothing to do here; post-download actions handled by ProcessDownloadedMmsAction
return null;
}
@Override
protected Object processBackgroundFailure() {
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
final String participantId = actionParameters.getString(KEY_PARTICIPANT_ID);
final int statusIfFailed = actionParameters.getInt(KEY_FAILURE_STATUS);
final int subId = actionParameters.getInt(KEY_SUB_ID);
ProcessDownloadedMmsAction.processDownloadActionFailure(messageId,
MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
conversationId, participantId, statusIfFailed, subId, transactionId);
return null;
}
static void updateMessageStatus(final Uri messageUri, final String messageId,
final String conversationId, final int status, final int rawStatus) {
final Context context = Factory.get().getApplicationContext();
// Downloading status just kept in local DB but need to fix up telephony DB first
if (status == MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING ||
status == MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING) {
MmsUtils.clearMmsStatus(context, messageUri);
}
// Then mark downloading status in our local DB
final ContentValues values = new ContentValues();
values.put(MessageColumns.STATUS, status);
values.put(MessageColumns.RAW_TELEPHONY_STATUS, rawStatus);
final DatabaseWrapper db = DataModel.get().getDatabase();
BugleDatabaseOperations.updateMessageRowIfExists(db, messageId, values);
MessagingContentProvider.notifyMessagesChanged(conversationId);
}
private DownloadMmsAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<DownloadMmsAction> CREATOR
= new Parcelable.Creator<DownloadMmsAction>() {
@Override
public DownloadMmsAction createFromParcel(final Parcel in) {
return new DownloadMmsAction(in);
}
@Override
public DownloadMmsAction[] newArray(final int size) {
return new DownloadMmsAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,124 +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.datamodel.action;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.LogUtil;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class DumpDatabaseAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
public static final String DUMP_NAME = "db_copy.db";
private static final int BUFFER_SIZE = 16384;
/**
* Copy the database to external storage
*/
public static void dumpDatabase() {
final DumpDatabaseAction action = new DumpDatabaseAction();
action.start();
}
private DumpDatabaseAction() {
}
@Override
protected Object executeAction() {
final Context context = Factory.get().getApplicationContext();
final String dbName = DatabaseHelper.DATABASE_NAME;
BufferedOutputStream bos = null;
BufferedInputStream bis = null;
long originalSize = 0;
final File inFile = context.getDatabasePath(dbName);
if (inFile.exists() && inFile.isFile()) {
originalSize = inFile.length();
}
final File outFile = DebugUtils.getDebugFile(DUMP_NAME, true);
if (outFile != null) {
int totalBytes = 0;
try {
bos = new BufferedOutputStream(new FileOutputStream(outFile));
bis = new BufferedInputStream(new FileInputStream(inFile));
final byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = bis.read(buffer)) > 0) {
bos.write(buffer, 0, bytesRead);
totalBytes += bytesRead;
}
} catch (final IOException e) {
LogUtil.w(TAG, "Exception copying the database;"
+ " destination may not be complete.", e);
} finally {
if (bos != null) {
try {
bos.close();
} catch (final IOException e) {
// Nothing to do
}
}
if (bis != null) {
try {
bis.close();
} catch (final IOException e) {
// Nothing to do
}
}
DebugUtils.ensureReadable(outFile);
LogUtil.i(TAG, "Dump complete; orig size: " + originalSize +
", copy size: " + totalBytes);
}
}
return null;
}
private DumpDatabaseAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<DumpDatabaseAction> CREATOR
= new Parcelable.Creator<DumpDatabaseAction>() {
@Override
public DumpDatabaseAction createFromParcel(final Parcel in) {
return new DumpDatabaseAction(in);
}
@Override
public DumpDatabaseAction[] newArray(final int size) {
return new DumpDatabaseAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,114 +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.datamodel.action;
import android.content.ContentValues;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.LogUtil;
/**
* Action used to fixup actively downloading or sending status at startup - just in case we
* crash - never run this when a message might actually be sending or downloading.
*/
public class FixupMessageStatusOnStartupAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
public static void fixupMessageStatus() {
final FixupMessageStatusOnStartupAction action = new FixupMessageStatusOnStartupAction();
action.start();
}
private FixupMessageStatusOnStartupAction() {
}
@Override
protected Object executeAction() {
// Now mark any messages in active sending or downloading state as inactive
final DatabaseWrapper db = DataModel.get().getDatabase();
db.beginTransaction();
int downloadFailedCnt = 0;
int sendFailedCnt = 0;
try {
// For both sending and downloading messages, let's assume they failed.
// For MMS sent/downloaded via platform, the sent/downloaded pending intent
// may come back. That will update the message. User may see the message
// in wrong status within a short window if that happens. But this should
// rarely happen. This is a simple solution to situations like app gets killed
// while the pending intent is still in the fly. Alternatively, we could
// keep the status for platform sent/downloaded MMS and timeout these messages.
// But that is much more complex.
final ContentValues values = new ContentValues();
values.put(DatabaseHelper.MessageColumns.STATUS,
MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED);
downloadFailedCnt += db.update(DatabaseHelper.MESSAGES_TABLE, values,
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
new String[]{
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
});
values.clear();
values.clear();
values.put(DatabaseHelper.MessageColumns.STATUS,
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
sendFailedCnt = db.update(DatabaseHelper.MESSAGES_TABLE, values,
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
new String[]{
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)
});
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
LogUtil.i(TAG, "Fixup: Send failed - " + sendFailedCnt
+ " Download failed - " + downloadFailedCnt);
// Don't send contentObserver notifications as displayed text should not change
return null;
}
private FixupMessageStatusOnStartupAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<FixupMessageStatusOnStartupAction> CREATOR
= new Parcelable.Creator<FixupMessageStatusOnStartupAction>() {
@Override
public FixupMessageStatusOnStartupAction createFromParcel(final Parcel in) {
return new FixupMessageStatusOnStartupAction(in);
}
@Override
public FixupMessageStatusOnStartupAction[] newArray(final int size) {
return new FixupMessageStatusOnStartupAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,173 +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.datamodel.action;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
import com.android.messaging.datamodel.data.LaunchConversationData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.LogUtil;
import java.util.ArrayList;
/**
* Action used to get or create a conversation for a list of conversation participants.
*/
public class GetOrCreateConversationAction extends Action implements Parcelable {
/**
* Interface for GetOrCreateConversationAction listeners
*/
public interface GetOrCreateConversationActionListener {
@RunsOnMainThread
abstract void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
final Object data, final String conversationId);
@RunsOnMainThread
abstract void onGetOrCreateConversationFailed(final ActionMonitor monitor,
final Object data);
}
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
final ArrayList<ParticipantData> participants, final Object data,
final GetOrCreateConversationActionListener listener) {
final GetOrCreateConversationActionMonitor monitor = new
GetOrCreateConversationActionMonitor(data, listener);
final GetOrCreateConversationAction action = new GetOrCreateConversationAction(participants,
monitor.getActionKey());
action.start(monitor);
return monitor;
}
public static GetOrCreateConversationActionMonitor getOrCreateConversation(
final String[] recipients, final Object data, final LaunchConversationData listener) {
final ArrayList<ParticipantData> participants = new ArrayList<>();
for (String recipient : recipients) {
recipient = recipient.trim();
if (!TextUtils.isEmpty(recipient)) {
participants.add(ParticipantData.getFromRawPhoneBySystemLocale(recipient));
} else {
LogUtil.w(LogUtil.BUGLE_TAG, "getOrCreateConversation hit empty recipient");
}
}
return getOrCreateConversation(participants, data, listener);
}
private static final String KEY_PARTICIPANTS_LIST = "participants_list";
private GetOrCreateConversationAction(final ArrayList<ParticipantData> participants,
final String actionKey) {
super(actionKey);
actionParameters.putParcelableArrayList(KEY_PARTICIPANTS_LIST, participants);
}
/**
* Lookup the conversation or create a new one.
*/
@Override
protected Object executeAction() {
final DatabaseWrapper db = DataModel.get().getDatabase();
// First find the thread id for this list of participants.
final ArrayList<ParticipantData> participants =
actionParameters.getParcelableArrayList(KEY_PARTICIPANTS_LIST);
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
final ArrayList<String> recipients =
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
recipients);
if (threadId < 0) {
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't create a threadId in SMS db for numbers : " +
LogUtil.sanitizePII(recipients.toString()));
// TODO: Add a better way to indicate an error from executeAction.
return null;
}
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
false, participants, false, false, null);
return conversationId;
}
/**
* A monitor that notifies a listener upon completion
*/
public static class GetOrCreateConversationActionMonitor extends ActionMonitor
implements ActionCompletedListener {
private final GetOrCreateConversationActionListener mListener;
GetOrCreateConversationActionMonitor(final Object data,
final GetOrCreateConversationActionListener listener) {
super(STATE_CREATED, generateUniqueActionKey("GetOrCreateConversationAction"), data);
setCompletedListener(this);
mListener = listener;
}
@Override
public void onActionSucceeded(final ActionMonitor monitor,
final Action action, final Object data, final Object result) {
if (result == null) {
mListener.onGetOrCreateConversationFailed(monitor, data);
} else {
mListener.onGetOrCreateConversationSucceeded(monitor, data, (String) result);
}
}
@Override
public void onActionFailed(final ActionMonitor monitor,
final Action action, final Object data, final Object result) {
// TODO: Currently onActionFailed is only called if there is an error in
// processing requests, not for errors in the local processing.
Assert.fail("Unreachable");
mListener.onGetOrCreateConversationFailed(monitor, data);
}
}
private GetOrCreateConversationAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<GetOrCreateConversationAction> CREATOR
= new Parcelable.Creator<GetOrCreateConversationAction>() {
@Override
public GetOrCreateConversationAction createFromParcel(final Parcel in) {
return new GetOrCreateConversationAction(in);
}
@Override
public GetOrCreateConversationAction[] newArray(final int size) {
return new GetOrCreateConversationAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,94 +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.datamodel.action;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.sms.SmsReleaseStorage;
import com.android.messaging.util.Assert;
/**
* Action used to handle low storage related issues on the device.
*/
public class HandleLowStorageAction extends Action implements Parcelable {
private static final int SUB_OP_CODE_CLEAR_MEDIA_MESSAGES = 100;
private static final int SUB_OP_CODE_CLEAR_OLD_MESSAGES = 101;
public static void handleDeleteMediaMessages(final long durationInMillis) {
final HandleLowStorageAction action = new HandleLowStorageAction(
SUB_OP_CODE_CLEAR_MEDIA_MESSAGES, durationInMillis);
action.start();
}
public static void handleDeleteOldMessages(final long durationInMillis) {
final HandleLowStorageAction action = new HandleLowStorageAction(
SUB_OP_CODE_CLEAR_OLD_MESSAGES, durationInMillis);
action.start();
}
private static final String KEY_SUB_OP_CODE = "sub_op_code";
private static final String KEY_CUTOFF_DURATION_MILLIS = "cutoff_duration_millis";
private HandleLowStorageAction(final int subOpcode, final long durationInMillis) {
super();
actionParameters.putInt(KEY_SUB_OP_CODE, subOpcode);
actionParameters.putLong(KEY_CUTOFF_DURATION_MILLIS, durationInMillis);
}
@Override
protected Object executeAction() {
final int subOpCode = actionParameters.getInt(KEY_SUB_OP_CODE);
final long durationInMillis = actionParameters.getLong(KEY_CUTOFF_DURATION_MILLIS);
switch (subOpCode) {
case SUB_OP_CODE_CLEAR_MEDIA_MESSAGES:
SmsReleaseStorage.deleteMessages(0, durationInMillis);
break;
case SUB_OP_CODE_CLEAR_OLD_MESSAGES:
SmsReleaseStorage.deleteMessages(1, durationInMillis);
break;
default:
Assert.fail("Unsupported action type!");
break;
}
return true;
}
private HandleLowStorageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<HandleLowStorageAction> CREATOR
= new Parcelable.Creator<HandleLowStorageAction>() {
@Override
public HandleLowStorageAction createFromParcel(final Parcel in) {
return new HandleLowStorageAction(in);
}
@Override
public HandleLowStorageAction[] newArray(final int size) {
return new HandleLowStorageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,480 +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.datamodel.action;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.ConversationListItemData;
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.sms.MmsUtils;
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 java.util.ArrayList;
import java.util.List;
/**
* Action used to convert a draft message to an outgoing message. Its writes SMS messages to
* the telephony db, but {@link SendMessageAction} is responsible for inserting MMS message into
* the telephony DB. The latter also does the actual sending of the message in the background.
* The latter is also responsible for re-sending a failed message.
*/
public class InsertNewMessageAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static long sLastSentMessageTimestamp = -1;
/**
* Insert message (no listener)
*/
public static void insertNewMessage(final MessageData message) {
final InsertNewMessageAction action = new InsertNewMessageAction(message);
action.start();
}
/**
* Insert message (no listener) with a given non-default subId.
*/
public static void insertNewMessage(final MessageData message, final int subId) {
Assert.isFalse(subId == ParticipantData.DEFAULT_SELF_SUB_ID);
final InsertNewMessageAction action = new InsertNewMessageAction(message, subId);
action.start();
}
/**
* Insert message (no listener)
*/
public static void insertNewMessage(final int subId, final String recipients,
final String messageText, final String subject) {
final InsertNewMessageAction action = new InsertNewMessageAction(
subId, recipients, messageText, subject);
action.start();
}
public static long getLastSentMessageTimestamp() {
return sLastSentMessageTimestamp;
}
private static final String KEY_SUB_ID = "sub_id";
private static final String KEY_MESSAGE = "message";
private static final String KEY_RECIPIENTS = "recipients";
private static final String KEY_MESSAGE_TEXT = "message_text";
private static final String KEY_SUBJECT_TEXT = "subject_text";
private InsertNewMessageAction(final MessageData message) {
this(message, ParticipantData.DEFAULT_SELF_SUB_ID);
actionParameters.putParcelable(KEY_MESSAGE, message);
}
private InsertNewMessageAction(final MessageData message, final int subId) {
super();
actionParameters.putParcelable(KEY_MESSAGE, message);
actionParameters.putInt(KEY_SUB_ID, subId);
}
private InsertNewMessageAction(final int subId, final String recipients,
final String messageText, final String subject) {
super();
if (TextUtils.isEmpty(recipients) || TextUtils.isEmpty(messageText)) {
Assert.fail("InsertNewMessageAction: Can't have empty recipients or message");
}
actionParameters.putInt(KEY_SUB_ID, subId);
actionParameters.putString(KEY_RECIPIENTS, recipients);
actionParameters.putString(KEY_MESSAGE_TEXT, messageText);
actionParameters.putString(KEY_SUBJECT_TEXT, subject);
}
/**
* Add message to database in pending state and queue actual sending
*/
@Override
protected Object executeAction() {
LogUtil.i(TAG, "InsertNewMessageAction: inserting new message");
MessageData message = actionParameters.getParcelable(KEY_MESSAGE);
if (message == null) {
LogUtil.i(TAG, "InsertNewMessageAction: Creating MessageData with provided data");
message = createMessage();
if (message == null) {
LogUtil.w(TAG, "InsertNewMessageAction: Could not create MessageData");
return null;
}
}
final DatabaseWrapper db = DataModel.get().getDatabase();
final String conversationId = message.getConversationId();
final ParticipantData self = getSelf(db, conversationId, message);
if (self == null) {
return null;
}
message.bindSelfId(self.getId());
// If the user taps the Send button before the conversation draft is created/loaded by
// ReadDraftDataAction (maybe the action service thread was busy), the MessageData may not
// have the participant id set. It should be equal to the self id, so we'll use that.
if (message.getParticipantId() == null) {
message.bindParticipantId(self.getId());
}
final long timestamp = System.currentTimeMillis();
final ArrayList<String> recipients =
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
if (recipients.size() < 1) {
LogUtil.w(TAG, "InsertNewMessageAction: message recipients is empty");
return null;
}
final int subId = self.getSubId();
// TODO: Work out whether to send with SMS or MMS (taking into account recipients)?
final boolean isSms = (message.getProtocol() == MessageData.PROTOCOL_SMS);
if (isSms) {
String sendingConversationId = conversationId;
if (recipients.size() > 1) {
// Broadcast SMS - put message in "fake conversation" before farming out to real 1:1
final long laterTimestamp = timestamp + 1;
// Send a single message
insertBroadcastSmsMessage(conversationId, message, subId,
laterTimestamp, recipients);
sendingConversationId = null;
}
for (final String recipient : recipients) {
// Start actual sending
insertSendingSmsMessage(message, subId, recipient,
timestamp, sendingConversationId);
}
// Can now clear draft from conversation (deleting attachments if necessary)
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
null /* message */, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
} else {
final long timestampRoundedToSecond = 1000 * ((timestamp + 500) / 1000);
// Write place holder message directly referencing parts from the draft
final MessageData messageToSend = insertSendingMmsMessage(conversationId,
message, timestampRoundedToSecond);
// Can now clear draft from conversation (preserving attachments which are now
// referenced by messageToSend)
BugleDatabaseOperations.updateDraftMessageData(db, conversationId,
messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
}
MessagingContentProvider.notifyConversationListChanged();
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
return message;
}
private ParticipantData getSelf(
final DatabaseWrapper db, final String conversationId, final MessageData message) {
ParticipantData self;
// Check if we are asked to bind to a non-default subId. This is directly passed in from
// the UI thread so that the sub id may be locked as soon as the user clicks on the Send
// button.
final int requestedSubId = actionParameters.getInt(
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
if (requestedSubId != ParticipantData.DEFAULT_SELF_SUB_ID) {
self = BugleDatabaseOperations.getOrCreateSelf(db, requestedSubId);
} else {
String selfId = message.getSelfId();
if (selfId == null) {
// The conversation draft provides no self id hint, meaning that 1) conversation
// self id was not loaded AND 2) the user didn't pick a SIM from the SIM selector.
// In this case, use the conversation's self id.
final ConversationListItemData conversation =
ConversationListItemData.getExistingConversation(db, conversationId);
if (conversation != null) {
selfId = conversation.getSelfId();
} else {
LogUtil.w(LogUtil.BUGLE_DATAMODEL_TAG, "Conversation " + conversationId +
"already deleted before sending draft message " +
message.getMessageId() + ". Aborting InsertNewMessageAction.");
return null;
}
}
// We do not use SubscriptionManager.DEFAULT_SUB_ID for sending a message, so we need
// to bind the message to the system default subscription if it's unbound.
final ParticipantData unboundSelf = BugleDatabaseOperations.getExistingParticipant(
db, selfId);
if (unboundSelf.getSubId() == ParticipantData.DEFAULT_SELF_SUB_ID
&& OsUtil.isAtLeastL_MR1()) {
final int defaultSubId = PhoneUtils.getDefault().getDefaultSmsSubscriptionId();
self = BugleDatabaseOperations.getOrCreateSelf(db, defaultSubId);
} else {
self = unboundSelf;
}
}
return self;
}
/** Create MessageData using KEY_RECIPIENTS, KEY_MESSAGE_TEXT and KEY_SUBJECT */
private MessageData createMessage() {
// First find the thread id for this list of participants.
final String recipientsList = actionParameters.getString(KEY_RECIPIENTS);
final String messageText = actionParameters.getString(KEY_MESSAGE_TEXT);
final String subjectText = actionParameters.getString(KEY_SUBJECT_TEXT);
final int subId = actionParameters.getInt(
KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
final ArrayList<ParticipantData> participants = new ArrayList<>();
for (final String recipient : recipientsList.split(",")) {
participants.add(ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
}
if (participants.size() == 0) {
Assert.fail("InsertNewMessage: Empty participants");
return null;
}
final DatabaseWrapper db = DataModel.get().getDatabase();
BugleDatabaseOperations.sanitizeConversationParticipants(participants);
final ArrayList<String> recipients =
BugleDatabaseOperations.getRecipientsFromConversationParticipants(participants);
if (recipients.size() == 0) {
Assert.fail("InsertNewMessage: Empty recipients");
return null;
}
final long threadId = MmsUtils.getOrCreateThreadId(Factory.get().getApplicationContext(),
recipients);
if (threadId < 0) {
Assert.fail("InsertNewMessage: Couldn't get threadId in SMS db for these recipients: "
+ recipients.toString());
// TODO: How do we fail the action?
return null;
}
final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
false, participants, false, false, null);
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
if (TextUtils.isEmpty(subjectText)) {
return MessageData.createDraftSmsMessage(conversationId, self.getId(), messageText);
} else {
return MessageData.createDraftMmsMessage(conversationId, self.getId(), messageText,
subjectText);
}
}
private void insertBroadcastSmsMessage(final String conversationId,
final MessageData message, final int subId, final long laterTimestamp,
final ArrayList<String> recipients) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "InsertNewMessageAction: Inserting broadcast SMS message "
+ message.getMessageId());
}
final Context context = Factory.get().getApplicationContext();
final DatabaseWrapper db = DataModel.get().getDatabase();
// Inform sync that message is being added at timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(laterTimestamp);
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
final String address = TextUtils.join(" ", recipients);
final String messageText = message.getMessageText();
// Insert message into telephony database sms message table
final Uri messageUri = MmsUtils.insertSmsMessage(context,
Telephony.Sms.CONTENT_URI,
subId,
address,
messageText,
laterTimestamp,
Telephony.Sms.STATUS_COMPLETE,
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
db.beginTransaction();
try {
message.updateSendingMessage(conversationId, messageUri, laterTimestamp);
message.markMessageSent(laterTimestamp);
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
conversationId, message.getMessageId(), laterTimestamp,
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "InsertNewMessageAction: Inserted broadcast SMS message "
+ message.getMessageId() + ", uri = " + message.getSmsMessageUri());
}
MessagingContentProvider.notifyMessagesChanged(conversationId);
MessagingContentProvider.notifyPartsChanged();
} else {
// Ignore error as we only really care about the individual messages?
LogUtil.e(TAG,
"InsertNewMessageAction: No uri for broadcast SMS " + message.getMessageId()
+ " inserted into telephony DB");
}
}
/**
* Insert SMS messaging into our database and telephony db.
*/
private MessageData insertSendingSmsMessage(final MessageData content, final int subId,
final String recipient, final long timestamp, final String sendingConversationId) {
sLastSentMessageTimestamp = timestamp;
final Context context = Factory.get().getApplicationContext();
// Inform sync that message is being added at timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(timestamp);
final DatabaseWrapper db = DataModel.get().getDatabase();
// Send a single message
long threadId;
String conversationId;
if (sendingConversationId == null) {
// For 1:1 message generated sending broadcast need to look up threadId+conversationId
threadId = MmsUtils.getOrCreateSmsThreadId(context, recipient);
conversationId = BugleDatabaseOperations.getOrCreateConversationFromRecipient(
db, threadId, false /* sender blocked */,
ParticipantData.getFromRawPhoneBySimLocale(recipient, subId));
} else {
// Otherwise just look up threadId
threadId = BugleDatabaseOperations.getThreadId(db, sendingConversationId);
conversationId = sendingConversationId;
}
final String messageText = content.getMessageText();
// Insert message into telephony database sms message table
final Uri messageUri = MmsUtils.insertSmsMessage(context,
Telephony.Sms.CONTENT_URI,
subId,
recipient,
messageText,
timestamp,
Telephony.Sms.STATUS_NONE,
Telephony.Sms.MESSAGE_TYPE_SENT, threadId);
MessageData message = null;
if (messageUri != null && !TextUtils.isEmpty(messageUri.toString())) {
db.beginTransaction();
try {
message = MessageData.createDraftSmsMessage(conversationId,
content.getSelfId(), messageText);
message.updateSendingMessage(conversationId, messageUri, timestamp);
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
// Do not update the conversation summary to reflect autogenerated 1:1 messages
if (sendingConversationId != null) {
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
conversationId, message.getMessageId(), timestamp,
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "InsertNewMessageAction: Inserted SMS message "
+ message.getMessageId() + " (uri = " + message.getSmsMessageUri()
+ ", timestamp = " + message.getReceivedTimeStamp() + ")");
}
MessagingContentProvider.notifyMessagesChanged(conversationId);
MessagingContentProvider.notifyPartsChanged();
} else {
LogUtil.e(TAG, "InsertNewMessageAction: No uri for SMS inserted into telephony DB");
}
return message;
}
/**
* Insert MMS messaging into our database.
*/
private MessageData insertSendingMmsMessage(final String conversationId,
final MessageData message, final long timestamp) {
final DatabaseWrapper db = DataModel.get().getDatabase();
db.beginTransaction();
final List<MessagePartData> attachmentsUpdated = new ArrayList<>();
try {
sLastSentMessageTimestamp = timestamp;
// Insert "draft" message as placeholder until the final message is written to
// the telephony db
message.updateSendingMessage(conversationId, null/*messageUri*/, timestamp);
// No need to inform SyncManager as message currently has no Uri...
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
conversationId, message.getMessageId(), timestamp,
false /* senderBlocked */, false /* shouldAutoSwitchSelfId */);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "InsertNewMessageAction: Inserted MMS message "
+ message.getMessageId() + " (timestamp = " + timestamp + ")");
}
MessagingContentProvider.notifyMessagesChanged(conversationId);
MessagingContentProvider.notifyPartsChanged();
return message;
}
private InsertNewMessageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<InsertNewMessageAction> CREATOR
= new Parcelable.Creator<InsertNewMessageAction>() {
@Override
public InsertNewMessageAction createFromParcel(final Parcel in) {
return new InsertNewMessageAction(in);
}
@Override
public InsertNewMessageAction[] newArray(final int size) {
return new InsertNewMessageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,153 +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.datamodel.action;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony.Threads;
import android.provider.Telephony.ThreadsColumns;
import com.android.messaging.Factory;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.util.DebugUtils;
import com.android.messaging.util.LogUtil;
public class LogTelephonyDatabaseAction extends Action implements Parcelable {
// Because we use sanitizePII, we should also use BUGLE_TAG
private static final String TAG = LogUtil.BUGLE_TAG;
private static final String[] ALL_THREADS_PROJECTION = {
Threads._ID,
Threads.DATE,
Threads.MESSAGE_COUNT,
Threads.RECIPIENT_IDS,
Threads.SNIPPET,
Threads.SNIPPET_CHARSET,
Threads.READ,
Threads.ERROR,
Threads.HAS_ATTACHMENT };
// Constants from the Telephony Database
private static final int ID = 0;
private static final int DATE = 1;
private static final int MESSAGE_COUNT = 2;
private static final int RECIPIENT_IDS = 3;
private static final int SNIPPET = 4;
private static final int SNIPPET_CHAR_SET = 5;
private static final int READ = 6;
private static final int ERROR = 7;
private static final int HAS_ATTACHMENT = 8;
/**
* Log telephony data to logcat
*/
public static void dumpDatabase() {
final LogTelephonyDatabaseAction action = new LogTelephonyDatabaseAction();
action.start();
}
private LogTelephonyDatabaseAction() {
}
@Override
protected Object executeAction() {
final Context context = Factory.get().getApplicationContext();
if (!DebugUtils.isDebugEnabled()) {
LogUtil.e(TAG, "Can't log telephony database unless debugging is enabled");
return null;
}
if (!LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.w(TAG, "Can't log telephony database unless DEBUG is turned on for TAG: " +
TAG);
return null;
}
LogUtil.d(TAG, "\n");
LogUtil.d(TAG, "Dump of canoncial_addresses table");
LogUtil.d(TAG, "*********************************");
Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
Uri.parse("content://mms-sms/canonical-addresses"), null, null, null, null);
if (cursor == null) {
LogUtil.w(TAG, "null Cursor in content://mms-sms/canonical-addresses");
} else {
try {
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
String number = cursor.getString(1);
LogUtil.d(TAG, LogUtil.sanitizePII("id: " + id + " number: " + number));
}
} finally {
cursor.close();
}
}
LogUtil.d(TAG, "\n");
LogUtil.d(TAG, "Dump of threads table");
LogUtil.d(TAG, "*********************");
cursor = SqliteWrapper.query(context, context.getContentResolver(),
Threads.CONTENT_URI.buildUpon().appendQueryParameter("simple", "true").build(),
ALL_THREADS_PROJECTION, null, null, "date ASC");
try {
while (cursor.moveToNext()) {
LogUtil.d(TAG, LogUtil.sanitizePII("threadId: " + cursor.getLong(ID) +
" " + ThreadsColumns.DATE + " : " + cursor.getLong(DATE) +
" " + ThreadsColumns.MESSAGE_COUNT + " : " + cursor.getInt(MESSAGE_COUNT) +
" " + ThreadsColumns.SNIPPET + " : " + cursor.getString(SNIPPET) +
" " + ThreadsColumns.READ + " : " + cursor.getInt(READ) +
" " + ThreadsColumns.ERROR + " : " + cursor.getInt(ERROR) +
" " + ThreadsColumns.HAS_ATTACHMENT + " : " +
cursor.getInt(HAS_ATTACHMENT) +
" " + ThreadsColumns.RECIPIENT_IDS + " : " +
cursor.getString(RECIPIENT_IDS)));
}
} finally {
cursor.close();
}
return null;
}
private LogTelephonyDatabaseAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<LogTelephonyDatabaseAction> CREATOR
= new Parcelable.Creator<LogTelephonyDatabaseAction>() {
@Override
public LogTelephonyDatabaseAction createFromParcel(final Parcel in) {
return new LogTelephonyDatabaseAction(in);
}
@Override
public LogTelephonyDatabaseAction[] newArray(final int size) {
return new LogTelephonyDatabaseAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,113 +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.datamodel.action;
import android.content.ContentValues;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.LogUtil;
/**
* Action used to mark all the messages in a conversation as read
*/
public class MarkAsReadAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String KEY_CONVERSATION_ID = "conversation_id";
/**
* Mark all the messages as read for a particular conversation.
*/
public static void markAsRead(final String conversationId) {
final MarkAsReadAction action = new MarkAsReadAction(conversationId);
action.start();
}
private MarkAsReadAction(final String conversationId) {
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
}
@Override
protected Object executeAction() {
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
// TODO: Consider doing this in background service to avoid delaying other actions
final DatabaseWrapper db = DataModel.get().getDatabase();
// Mark all messages in thread as read in telephony
final long threadId = BugleDatabaseOperations.getThreadId(db, conversationId);
if (threadId != -1) {
MmsUtils.updateSmsReadStatus(threadId, Long.MAX_VALUE);
}
// Update local db
db.beginTransaction();
try {
final ContentValues values = new ContentValues();
values.put(MessageColumns.CONVERSATION_ID, conversationId);
values.put(MessageColumns.READ, 1);
values.put(MessageColumns.SEEN, 1); // if they read it, they saw it
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
"(" + MessageColumns.READ + " !=1 OR " +
MessageColumns.SEEN + " !=1 ) AND " +
MessageColumns.CONVERSATION_ID + "=?",
new String[] { conversationId });
if (count > 0) {
MessagingContentProvider.notifyMessagesChanged(conversationId);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// After marking messages as read, update the notifications. This will
// clear the now stale notifications.
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
return null;
}
private MarkAsReadAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<MarkAsReadAction> CREATOR
= new Parcelable.Creator<MarkAsReadAction>() {
@Override
public MarkAsReadAction createFromParcel(final Parcel in) {
return new MarkAsReadAction(in);
}
@Override
public MarkAsReadAction[] newArray(final int size) {
return new MarkAsReadAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,126 +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.datamodel.action;
import android.content.ContentValues;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.util.LogUtil;
/**
* Action used to mark all messages as seen
*/
public class MarkAsSeenAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String KEY_CONVERSATION_ID = "conversation_id";
/**
* Mark all messages as seen.
*/
public static void markAllAsSeen() {
final MarkAsSeenAction action = new MarkAsSeenAction((String) null/*conversationId*/);
action.start();
}
/**
* Mark all messages of a given conversation as seen.
*/
public static void markAsSeen(final String conversationId) {
final MarkAsSeenAction action = new MarkAsSeenAction(conversationId);
action.start();
}
/**
* ctor for MarkAsSeenAction.
* @param conversationId the conversation id for which to mark as seen, or null to mark all
* messages as seen
*/
public MarkAsSeenAction(final String conversationId) {
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
}
@Override
protected Object executeAction() {
final String conversationId =
actionParameters.getString(KEY_CONVERSATION_ID);
final boolean hasSpecificConversation = !TextUtils.isEmpty(conversationId);
// Everything in telephony should already have the seen bit set.
// Possible exception are messages which did not have seen set and
// were sync'ed into bugle.
// Now mark the messages as seen in the bugle db
final DatabaseWrapper db = DataModel.get().getDatabase();
db.beginTransaction();
try {
final ContentValues values = new ContentValues();
values.put(MessageColumns.SEEN, 1);
if (hasSpecificConversation) {
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
MessageColumns.SEEN + " != 1 AND " +
MessageColumns.CONVERSATION_ID + "=?",
new String[] { conversationId });
if (count > 0) {
MessagingContentProvider.notifyMessagesChanged(conversationId);
}
} else {
db.update(DatabaseHelper.MESSAGES_TABLE, values,
MessageColumns.SEEN + " != 1", null/*selectionArgs*/);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// After marking messages as seen, update the notifications. This will
// clear the now stale notifications.
BugleNotifications.update(false/*silent*/, BugleNotifications.UPDATE_ALL);
return null;
}
private MarkAsSeenAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<MarkAsSeenAction> CREATOR
= new Parcelable.Creator<MarkAsSeenAction>() {
@Override
public MarkAsSeenAction createFromParcel(final Parcel in) {
return new MarkAsSeenAction(in);
}
@Override
public MarkAsSeenAction[] newArray(final int size) {
return new MarkAsSeenAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,122 +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.datamodel.action;
import android.content.ContentUris;
import android.content.ContentValues;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.util.concurrent.TimeUnit;
public class ProcessDeliveryReportAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String KEY_URI = "uri";
private static final String KEY_STATUS = "status";
private ProcessDeliveryReportAction(final Uri uri, final int status) {
actionParameters.putParcelable(KEY_URI, uri);
actionParameters.putInt(KEY_STATUS, status);
}
public static void deliveryReportReceived(final Uri uri, final int status) {
final ProcessDeliveryReportAction action = new ProcessDeliveryReportAction(uri, status);
action.start();
}
@Override
protected Object executeAction() {
final Uri smsMessageUri = actionParameters.getParcelable(KEY_URI);
final int status = actionParameters.getInt(KEY_STATUS);
final DatabaseWrapper db = DataModel.get().getDatabase();
final long messageRowId = ContentUris.parseId(smsMessageUri);
if (messageRowId < 0) {
LogUtil.e(TAG, "ProcessDeliveryReportAction: can't find message");
return null;
}
final long timeSentInMillis = System.currentTimeMillis();
// Update telephony provider
if (smsMessageUri != null) {
MmsUtils.updateSmsStatusAndDateSent(smsMessageUri, status, timeSentInMillis);
}
// Update local message
db.beginTransaction();
try {
final ContentValues values = new ContentValues();
final int bugleStatus = SyncMessageBatch.bugleStatusForSms(true /*outgoing*/,
Telephony.Sms.MESSAGE_TYPE_SENT /* type */, status);
values.put(DatabaseHelper.MessageColumns.STATUS, bugleStatus);
values.put(DatabaseHelper.MessageColumns.SENT_TIMESTAMP,
TimeUnit.MILLISECONDS.toMicros(timeSentInMillis));
final MessageData messageData =
BugleDatabaseOperations.readMessageData(db, smsMessageUri);
// Check the message was not removed before the delivery report comes in
if (messageData != null) {
Assert.isTrue(smsMessageUri.equals(messageData.getSmsMessageUri()));
// Row must exist as was just loaded above (on ActionService thread)
BugleDatabaseOperations.updateMessageRow(db, messageData.getMessageId(), values);
MessagingContentProvider.notifyMessagesChanged(messageData.getConversationId());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return null;
}
private ProcessDeliveryReportAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ProcessDeliveryReportAction> CREATOR
= new Parcelable.Creator<ProcessDeliveryReportAction>() {
@Override
public ProcessDeliveryReportAction createFromParcel(final Parcel in) {
return new ProcessDeliveryReportAction(in);
}
@Override
public ProcessDeliveryReportAction[] newArray(final int size) {
return new ProcessDeliveryReportAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,573 +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.datamodel.action;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony.Mms;
import android.telephony.SmsManager;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelException;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.MmsFileProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.mmslib.SqliteWrapper;
import com.android.messaging.mmslib.pdu.PduHeaders;
import com.android.messaging.mmslib.pdu.RetrieveConf;
import com.android.messaging.sms.DatabaseMessages;
import com.android.messaging.sms.MmsSender;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.google.common.io.Files;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
/**
* Processes an MMS message after it has been downloaded.
* NOTE: This action must queue a ProcessPendingMessagesAction when it is done (success or failure).
*/
public class ProcessDownloadedMmsAction extends Action {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
// Always set when message downloaded
private static final String KEY_DOWNLOADED_BY_PLATFORM = "downloaded_by_platform";
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_NOTIFICATION_URI = "notification_uri";
private static final String KEY_CONVERSATION_ID = "conversation_id";
private static final String KEY_PARTICIPANT_ID = "participant_id";
private static final String KEY_STATUS_IF_FAILED = "status_if_failed";
// Set when message downloaded by platform (L+)
private static final String KEY_RESULT_CODE = "result_code";
private static final String KEY_HTTP_STATUS_CODE = "http_status_code";
private static final String KEY_CONTENT_URI = "content_uri";
private static final String KEY_SUB_ID = "sub_id";
private static final String KEY_SUB_PHONE_NUMBER = "sub_phone_number";
private static final String KEY_TRANSACTION_ID = "transaction_id";
private static final String KEY_CONTENT_LOCATION = "content_location";
private static final String KEY_AUTO_DOWNLOAD = "auto_download";
private static final String KEY_RECEIVED_TIMESTAMP = "received_timestamp";
// Set when message downloaded by us (legacy)
private static final String KEY_STATUS = "status";
private static final String KEY_RAW_STATUS = "raw_status";
private static final String KEY_MMS_URI = "mms_uri";
// Used to send a deferred response in response to auto-download failure
private static final String KEY_SEND_DEFERRED_RESP_STATUS = "send_deferred_resp_status";
// Results passed from background worker to processCompletion
private static final String BUNDLE_REQUEST_STATUS = "request_status";
private static final String BUNDLE_RAW_TELEPHONY_STATUS = "raw_status";
private static final String BUNDLE_MMS_URI = "mms_uri";
// This is called when MMS lib API returns via PendingIntent
public static void processMessageDownloaded(final int resultCode, final Bundle extras) {
final String messageId = extras.getString(DownloadMmsAction.EXTRA_MESSAGE_ID);
final Uri contentUri = extras.getParcelable(DownloadMmsAction.EXTRA_CONTENT_URI);
final Uri notificationUri = extras.getParcelable(DownloadMmsAction.EXTRA_NOTIFICATION_URI);
final String conversationId = extras.getString(DownloadMmsAction.EXTRA_CONVERSATION_ID);
final String participantId = extras.getString(DownloadMmsAction.EXTRA_PARTICIPANT_ID);
Assert.notNull(messageId);
Assert.notNull(contentUri);
Assert.notNull(notificationUri);
Assert.notNull(conversationId);
Assert.notNull(participantId);
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
final Bundle params = action.actionParameters;
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
params.putString(KEY_MESSAGE_ID, messageId);
params.putInt(KEY_RESULT_CODE, resultCode);
params.putInt(KEY_HTTP_STATUS_CODE,
extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
params.putParcelable(KEY_CONTENT_URI, contentUri);
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
params.putInt(KEY_SUB_ID,
extras.getInt(DownloadMmsAction.EXTRA_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
params.putString(KEY_SUB_PHONE_NUMBER,
extras.getString(DownloadMmsAction.EXTRA_SUB_PHONE_NUMBER));
params.putString(KEY_TRANSACTION_ID,
extras.getString(DownloadMmsAction.EXTRA_TRANSACTION_ID));
params.putString(KEY_CONTENT_LOCATION,
extras.getString(DownloadMmsAction.EXTRA_CONTENT_LOCATION));
params.putBoolean(KEY_AUTO_DOWNLOAD,
extras.getBoolean(DownloadMmsAction.EXTRA_AUTO_DOWNLOAD));
params.putLong(KEY_RECEIVED_TIMESTAMP,
extras.getLong(DownloadMmsAction.EXTRA_RECEIVED_TIMESTAMP));
params.putString(KEY_CONVERSATION_ID, conversationId);
params.putString(KEY_PARTICIPANT_ID, participantId);
params.putInt(KEY_STATUS_IF_FAILED,
extras.getInt(DownloadMmsAction.EXTRA_STATUS_IF_FAILED));
action.start();
}
// This is called for fast failing downloading (due to airplane mode or mobile data )
public static void processMessageDownloadFastFailed(final String messageId,
final Uri notificationUri, final String conversationId, final String participantId,
final String contentLocation, final int subId, final String subPhoneNumber,
final int statusIfFailed, final boolean autoDownload, final String transactionId,
final int resultCode) {
Assert.notNull(messageId);
Assert.notNull(notificationUri);
Assert.notNull(conversationId);
Assert.notNull(participantId);
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
final Bundle params = action.actionParameters;
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, true);
params.putString(KEY_MESSAGE_ID, messageId);
params.putInt(KEY_RESULT_CODE, resultCode);
params.putParcelable(KEY_NOTIFICATION_URI, notificationUri);
params.putInt(KEY_SUB_ID, subId);
params.putString(KEY_SUB_PHONE_NUMBER, subPhoneNumber);
params.putString(KEY_CONTENT_LOCATION, contentLocation);
params.putBoolean(KEY_AUTO_DOWNLOAD, autoDownload);
params.putString(KEY_CONVERSATION_ID, conversationId);
params.putString(KEY_PARTICIPANT_ID, participantId);
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
params.putString(KEY_TRANSACTION_ID, transactionId);
action.start();
}
public static void processDownloadActionFailure(final String messageId, final int status,
final int rawStatus, final String conversationId, final String participantId,
final int statusIfFailed, final int subId, final String transactionId) {
Assert.notNull(messageId);
Assert.notNull(conversationId);
Assert.notNull(participantId);
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
final Bundle params = action.actionParameters;
params.putBoolean(KEY_DOWNLOADED_BY_PLATFORM, false);
params.putString(KEY_MESSAGE_ID, messageId);
params.putInt(KEY_STATUS, status);
params.putInt(KEY_RAW_STATUS, rawStatus);
params.putString(KEY_CONVERSATION_ID, conversationId);
params.putString(KEY_PARTICIPANT_ID, participantId);
params.putInt(KEY_STATUS_IF_FAILED, statusIfFailed);
params.putInt(KEY_SUB_ID, subId);
params.putString(KEY_TRANSACTION_ID, transactionId);
action.start();
}
public static void sendDeferredRespStatus(final String messageId, final String transactionId,
final String contentLocation, final int subId) {
final ProcessDownloadedMmsAction action = new ProcessDownloadedMmsAction();
final Bundle params = action.actionParameters;
params.putString(KEY_MESSAGE_ID, messageId);
params.putString(KEY_TRANSACTION_ID, transactionId);
params.putString(KEY_CONTENT_LOCATION, contentLocation);
params.putBoolean(KEY_SEND_DEFERRED_RESP_STATUS, true);
params.putInt(KEY_SUB_ID, subId);
action.start();
}
private ProcessDownloadedMmsAction() {
// Callers must use one of the static methods above
}
@Override
protected Object executeAction() {
// Fire up the background worker
requestBackgroundWork();
return null;
}
@Override
protected Bundle doBackgroundWork() throws DataModelException {
final Context context = Factory.get().getApplicationContext();
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
final boolean sendDeferredRespStatus =
actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS, false);
// Send a response indicating that auto-download failed
if (sendDeferredRespStatus) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "DownloadMmsAction: Auto-download of message " + messageId
+ " failed; sending DEFERRED NotifyRespInd");
}
MmsUtils.sendNotifyResponseForMmsDownload(
context,
subId,
MmsUtils.stringToBytes(transactionId, "UTF-8"),
contentLocation,
PduHeaders.STATUS_DEFERRED);
return null;
}
// Processing a real MMS download
final boolean downloadedByPlatform = actionParameters.getBoolean(
KEY_DOWNLOADED_BY_PLATFORM);
final int status;
int rawStatus = MmsUtils.PDU_HEADER_VALUE_UNDEFINED;
Uri mmsUri = null;
if (downloadedByPlatform) {
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
if (resultCode == Activity.RESULT_OK) {
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
final File downloadedFile = MmsFileProvider.getFile(contentUri);
byte[] downloadedData = null;
try {
downloadedData = Files.toByteArray(downloadedFile);
} catch (final FileNotFoundException e) {
LogUtil.e(TAG, "ProcessDownloadedMmsAction: MMS download file not found: "
+ downloadedFile.getAbsolutePath());
} catch (final IOException e) {
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Error reading MMS download file: "
+ downloadedFile.getAbsolutePath(), e);
}
// Can delete the temp file now
if (downloadedFile.exists()) {
downloadedFile.delete();
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ProcessDownloadedMmsAction: Deleted temp file with "
+ "downloaded MMS pdu: " + downloadedFile.getAbsolutePath());
}
}
if (downloadedData != null) {
final RetrieveConf retrieveConf =
MmsSender.parseRetrieveConf(downloadedData, subId);
if (MmsUtils.isDumpMmsEnabled()) {
MmsUtils.dumpPdu(downloadedData, retrieveConf);
}
if (retrieveConf != null) {
// Insert the downloaded MMS into telephony
final Uri notificationUri = actionParameters.getParcelable(
KEY_NOTIFICATION_URI);
final String subPhoneNumber = actionParameters.getString(
KEY_SUB_PHONE_NUMBER);
final boolean autoDownload = actionParameters.getBoolean(
KEY_AUTO_DOWNLOAD);
final long receivedTimestampInSeconds =
actionParameters.getLong(KEY_RECEIVED_TIMESTAMP);
// Inform sync we're adding a message to telephony
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(receivedTimestampInSeconds * 1000L);
final MmsUtils.StatusPlusUri result =
MmsUtils.insertDownloadedMessageAndSendResponse(context,
notificationUri, subId, subPhoneNumber, transactionId,
contentLocation, autoDownload, receivedTimestampInSeconds,
retrieveConf);
status = result.status;
rawStatus = result.rawStatus;
mmsUri = result.uri;
} else {
// Invalid response PDU
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
}
} else {
// Failed to read download file
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
}
} else {
LogUtil.w(TAG, "ProcessDownloadedMmsAction: Platform returned error resultCode: "
+ resultCode);
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
}
} else {
// Message was already processed by the internal API, or the download action failed.
// In either case, we just need to copy the status to the response bundle.
status = actionParameters.getInt(KEY_STATUS);
rawStatus = actionParameters.getInt(KEY_RAW_STATUS);
mmsUri = actionParameters.getParcelable(KEY_MMS_URI);
}
final Bundle response = new Bundle();
response.putInt(BUNDLE_REQUEST_STATUS, status);
response.putInt(BUNDLE_RAW_TELEPHONY_STATUS, rawStatus);
response.putParcelable(BUNDLE_MMS_URI, mmsUri);
return response;
}
@Override
protected Object processBackgroundResponse(final Bundle response) {
if (response == null) {
// No message download to process; doBackgroundWork sent a notify deferred response
Assert.isTrue(actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS));
return null;
}
final int status = response.getInt(BUNDLE_REQUEST_STATUS);
final int rawStatus = response.getInt(BUNDLE_RAW_TELEPHONY_STATUS);
final Uri messageUri = response.getParcelable(BUNDLE_MMS_URI);
final boolean autoDownload = actionParameters.getBoolean(KEY_AUTO_DOWNLOAD);
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
// Do post-processing on downloaded message
final MessageData message = processResult(status, rawStatus, messageUri);
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
// If we were trying to auto-download but have failed need to send the deferred response
if (autoDownload && message == null && status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
sendDeferredRespStatus(messageId, transactionId, contentLocation, subId);
}
if (autoDownload) {
final DatabaseWrapper db = DataModel.get().getDatabase();
MessageData toastMessage = message;
if (toastMessage == null) {
// If the downloaded failed (message is null), then we should announce the
// receiving of the wap push message. Load the wap push message here instead.
toastMessage = BugleDatabaseOperations.readMessageData(db, messageId);
}
if (toastMessage != null) {
final ParticipantData sender = ParticipantData.getFromId(
db, toastMessage.getParticipantId());
BugleActionToasts.onMessageReceived(
toastMessage.getConversationId(), sender, toastMessage);
}
} else {
final boolean success = message != null && status == MmsUtils.MMS_REQUEST_SUCCEEDED;
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
// If download failed, use the wap push message's conversation instead
success ? message.getConversationId()
: actionParameters.getString(KEY_CONVERSATION_ID),
success, status, false/*isSms*/, subId, false /*isSend*/);
}
final boolean failed = (messageUri == null);
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(failed, this);
if (failed) {
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
}
return message;
}
@Override
protected Object processBackgroundFailure() {
if (actionParameters.getBoolean(KEY_SEND_DEFERRED_RESP_STATUS)) {
// We can early-out for these failures. processResult is only designed to handle
// post-processing of MMS downloads (whether successful or not).
LogUtil.w(TAG,
"ProcessDownloadedMmsAction: Exception while sending deferred NotifyRespInd");
return null;
}
// Background worker threw an exception; require manual retry
processResult(MmsUtils.MMS_REQUEST_MANUAL_RETRY, MessageData.RAW_TELEPHONY_STATUS_UNDEFINED,
null /* mmsUri */);
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(true /* failed */,
this);
return null;
}
private MessageData processResult(final int status, final int rawStatus, final Uri mmsUri) {
final Context context = Factory.get().getApplicationContext();
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
final Uri mmsNotificationUri = actionParameters.getParcelable(KEY_NOTIFICATION_URI);
final String notificationConversationId = actionParameters.getString(KEY_CONVERSATION_ID);
final String notificationParticipantId = actionParameters.getString(KEY_PARTICIPANT_ID);
final int statusIfFailed = actionParameters.getInt(KEY_STATUS_IF_FAILED);
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
Assert.notNull(messageId);
LogUtil.i(TAG, "ProcessDownloadedMmsAction: Processed MMS download of message " + messageId
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
DatabaseMessages.MmsMessage mms = null;
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED && mmsUri != null) {
// Delete the initial M-Notification.ind from telephony
SqliteWrapper.delete(context, context.getContentResolver(),
mmsNotificationUri, null, null);
// Read the sent MMS from the telephony provider
mms = MmsUtils.loadMms(mmsUri);
}
boolean messageInFocusedConversation = false;
boolean messageInObservableConversation = false;
String conversationId = null;
MessageData message = null;
final DatabaseWrapper db = DataModel.get().getDatabase();
db.beginTransaction();
try {
if (mms != null) {
final ParticipantData self = ParticipantData.getSelfParticipant(mms.getSubId());
final String selfId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
if (from == null) {
LogUtil.w(TAG,
"Downloaded an MMS without sender address; using unknown sender.");
from = ParticipantData.getUnknownSenderDestination();
}
final ParticipantData sender = ParticipantData.getFromRawPhoneBySimLocale(from,
subId);
final String senderParticipantId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, sender);
if (!senderParticipantId.equals(notificationParticipantId)) {
LogUtil.e(TAG, "ProcessDownloadedMmsAction: Downloaded MMS message "
+ messageId + " has different sender (participantId = "
+ senderParticipantId + ") than notification ("
+ notificationParticipantId + ")");
}
final boolean blockedSender = BugleDatabaseOperations.isBlockedDestination(
db, sender.getNormalizedDestination());
conversationId = BugleDatabaseOperations.getOrCreateConversationFromThreadId(db,
mms.mThreadId, blockedSender, subId);
messageInFocusedConversation =
DataModel.get().isFocusedConversation(conversationId);
messageInObservableConversation =
DataModel.get().isNewMessageObservable(conversationId);
// TODO: Also write these values to the telephony provider
mms.mRead = messageInFocusedConversation;
mms.mSeen = messageInObservableConversation;
// Translate to our format
message = MmsUtils.createMmsMessage(mms, conversationId, senderParticipantId,
selfId, MessageData.BUGLE_STATUS_INCOMING_COMPLETE);
// Update image sizes.
message.updateSizesForImageParts();
// Inform sync that message has been added at local received timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(message.getReceivedTimeStamp());
final MessageData current = BugleDatabaseOperations.readMessageData(db, messageId);
if (current == null) {
LogUtil.w(TAG, "Message deleted prior to update");
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
} else {
// Overwrite existing notification message
message.updateMessageId(messageId);
// Write message
BugleDatabaseOperations.updateMessageInTransaction(db, message);
}
if (!TextUtils.equals(notificationConversationId, conversationId)) {
// If this is a group conversation, the message is moved. So the original
// 1v1 conversation (as referenced by notificationConversationId) could
// be left with no non-draft message. Delete the conversation if that
// happens. See the comment for the method below for why we need to do this.
if (!BugleDatabaseOperations.deleteConversationIfEmptyInTransaction(
db, notificationConversationId)) {
BugleDatabaseOperations.maybeRefreshConversationMetadataInTransaction(
db, notificationConversationId, messageId,
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
}
}
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db, conversationId,
true /*shouldAutoSwitchSelfId*/, blockedSender /*keepArchived*/);
} else {
messageInFocusedConversation =
DataModel.get().isFocusedConversation(notificationConversationId);
// Default to retry status unless status indicates otherwise
int bugleStatus = statusIfFailed;
if (status == MmsUtils.MMS_REQUEST_MANUAL_RETRY) {
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED;
} else if (status == MmsUtils.MMS_REQUEST_NO_RETRY) {
bugleStatus = MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE;
}
DownloadMmsAction.updateMessageStatus(mmsNotificationUri, messageId,
notificationConversationId, bugleStatus, rawStatus);
// Log MMS download failed
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
// Just in case this was the latest message update the summary data
BugleDatabaseOperations.refreshConversationMetadataInTransaction(db,
notificationConversationId, true /*shouldAutoSwitchSelfId*/,
false /*keepArchived*/);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
if (mmsUri != null) {
// Update mms table with read status now we know the conversation id
final ContentValues values = new ContentValues(1);
values.put(Mms.READ, messageInFocusedConversation);
SqliteWrapper.update(context, context.getContentResolver(), mmsUri, values,
null, null);
}
// Show a notification to let the user know a new message has arrived
BugleNotifications.update(false /*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
// Messages may have changed in two conversations
if (conversationId != null) {
MessagingContentProvider.notifyMessagesChanged(conversationId);
}
MessagingContentProvider.notifyMessagesChanged(notificationConversationId);
MessagingContentProvider.notifyPartsChanged();
return message;
}
private ProcessDownloadedMmsAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ProcessDownloadedMmsAction> CREATOR
= new Parcelable.Creator<ProcessDownloadedMmsAction>() {
@Override
public ProcessDownloadedMmsAction createFromParcel(final Parcel in) {
return new ProcessDownloadedMmsAction(in);
}
@Override
public ProcessDownloadedMmsAction[] newArray(final int size) {
return new ProcessDownloadedMmsAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,470 +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.datamodel.action;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.ServiceState;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.BugleGservices;
import com.android.messaging.util.BugleGservicesKeys;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.ConnectivityUtil.ConnectivityListener;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils;
import java.util.HashSet;
import java.util.Set;
/**
* Action used to lookup any messages in the pending send/download state and either fail them or
* retry their action. This action only initiates one retry at a time - further retries should be
* triggered by successful sending of a message, network status change or exponential backoff timer.
*/
public class ProcessPendingMessagesAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final int PENDING_INTENT_REQUEST_CODE = 101;
public static void processFirstPendingMessage() {
// Clear any pending alarms or connectivity events
unregister();
// Clear retry count
setRetry(0);
// Start action
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
action.start();
}
public static void scheduleProcessPendingMessagesAction(final boolean failed,
final Action processingAction) {
LogUtil.i(TAG, "ProcessPendingMessagesAction: Scheduling pending messages"
+ (failed ? "(message failed)" : ""));
// Can safely clear any pending alarms or connectivity events as either an action
// is currently running or we will run now or register if pending actions possible.
unregister();
final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
boolean scheduleAlarm = false;
// If message succeeded and if Bugle is default SMS app just carry on with next message
if (!failed && isDefaultSmsApp) {
// Clear retry attempt count as something just succeeded
setRetry(0);
// Lookup and queue next message for immediate processing by background worker
// iff there are no pending messages this will do nothing and return true.
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
if (action.queueActions(processingAction)) {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
if (processingAction.hasBackgroundActions()) {
LogUtil.v(TAG, "ProcessPendingMessagesAction: Action queued");
} else {
LogUtil.v(TAG, "ProcessPendingMessagesAction: No actions to queue");
}
}
// Have queued next action if needed, nothing more to do
return;
}
// In case of error queuing schedule a retry
scheduleAlarm = true;
LogUtil.w(TAG, "ProcessPendingMessagesAction: Action failed to queue; retrying");
}
if (getHavePendingMessages() || scheduleAlarm) {
// Still have a pending message that needs to be queued for processing
final ConnectivityListener listener = new ConnectivityListener() {
@Override
public void onConnectivityStateChanged(final Context context, final Intent intent) {
final int networkType =
MmsUtils.getConnectivityEventNetworkType(context, intent);
if (networkType != ConnectivityManager.TYPE_MOBILE) {
return;
}
final boolean isConnected = !intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
// TODO: Should we check in more detail?
if (isConnected) {
onConnected();
}
}
@Override
public void onPhoneStateChanged(final Context context, final int serviceState) {
if (serviceState == ServiceState.STATE_IN_SERVICE) {
onConnected();
}
}
private void onConnected() {
LogUtil.i(TAG, "ProcessPendingMessagesAction: Now connected; starting action");
// Clear any pending alarms or connectivity events but leave attempt count alone
unregister();
// Start action
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
action.start();
}
};
// Read and increment attempt number from shared prefs
final int retryAttempt = getNextRetry();
register(listener, retryAttempt);
} else {
// No more pending messages (presumably the message that failed has expired) or it
// may be possible that a send and a download are already in process.
// Clear retry attempt count.
// TODO Might be premature if send and download in process...
// but worst case means we try to send a bit more often.
setRetry(0);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ProcessPendingMessagesAction: No more pending messages");
}
}
}
private static void register(final ConnectivityListener listener, final int retryAttempt) {
int retryNumber = retryAttempt;
// Register to be notified about connectivity changes
DataModel.get().getConnectivityUtil().register(listener);
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
final long initialBackoffMs = BugleGservices.get().getLong(
BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS,
BugleGservicesKeys.INITIAL_MESSAGE_RESEND_DELAY_MS_DEFAULT);
final long maxDelayMs = BugleGservices.get().getLong(
BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS,
BugleGservicesKeys.MAX_MESSAGE_RESEND_DELAY_MS_DEFAULT);
long delayMs;
long nextDelayMs = initialBackoffMs;
do {
delayMs = nextDelayMs;
retryNumber--;
nextDelayMs = delayMs * 2;
}
while (retryNumber > 0 && nextDelayMs < maxDelayMs);
LogUtil.i(TAG, "ProcessPendingMessagesAction: Registering for retry #" + retryAttempt
+ " in " + delayMs + " ms");
action.schedule(PENDING_INTENT_REQUEST_CODE, delayMs);
}
private static void unregister() {
// Clear any pending alarms or connectivity events
DataModel.get().getConnectivityUtil().unregister();
final ProcessPendingMessagesAction action = new ProcessPendingMessagesAction();
action.schedule(PENDING_INTENT_REQUEST_CODE, Long.MAX_VALUE);
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ProcessPendingMessagesAction: Unregistering for connectivity changed "
+ "events and clearing scheduled alarm");
}
}
private static void setRetry(final int retryAttempt) {
final BuglePrefs prefs = Factory.get().getApplicationPrefs();
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
}
private static int getNextRetry() {
final BuglePrefs prefs = Factory.get().getApplicationPrefs();
final int retryAttempt =
prefs.getInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, 0) + 1;
prefs.putInt(BuglePrefsKeys.PROCESS_PENDING_MESSAGES_RETRY_COUNT, retryAttempt);
return retryAttempt;
}
private ProcessPendingMessagesAction() {
}
/**
* Read from the DB and determine if there are any messages we should process
* @return true if we have pending messages
*/
private static boolean getHavePendingMessages() {
final DatabaseWrapper db = DataModel.get().getDatabase();
final long now = System.currentTimeMillis();
final String toSendMessageId = findNextMessageToSend(db, now);
if (toSendMessageId != null) {
return true;
} else {
final String toDownloadMessageId = findNextMessageToDownload(db, now);
if (toDownloadMessageId != null) {
return true;
}
}
// Messages may be in the process of sending/downloading even when there are no pending
// messages...
return false;
}
/**
* Queue any pending actions
* @param actionState
* @return true if action queued (or no actions to queue) else false
*/
private boolean queueActions(final Action processingAction) {
final DatabaseWrapper db = DataModel.get().getDatabase();
final long now = System.currentTimeMillis();
boolean succeeded = true;
// Will queue no more than one message to send plus one message to download
// This keeps outgoing messages "in order" but allow downloads to happen even if sending
// gets blocked until messages time out. Manual resend bumps messages to head of queue.
final String toSendMessageId = findNextMessageToSend(db, now);
final String toDownloadMessageId = findNextMessageToDownload(db, now);
if (toSendMessageId != null) {
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toSendMessageId
+ " for sending");
// This could queue nothing
if (!SendMessageAction.queueForSendInBackground(toSendMessageId, processingAction)) {
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
+ toSendMessageId + " for sending");
succeeded = false;
}
}
if (toDownloadMessageId != null) {
LogUtil.i(TAG, "ProcessPendingMessagesAction: Queueing message " + toDownloadMessageId
+ " for download");
// This could queue nothing
if (!DownloadMmsAction.queueMmsForDownloadInBackground(toDownloadMessageId,
processingAction)) {
LogUtil.w(TAG, "ProcessPendingMessagesAction: Failed to queue message "
+ toDownloadMessageId + " for download");
succeeded = false;
}
}
if (toSendMessageId == null && toDownloadMessageId == null) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ProcessPendingMessagesAction: No messages to send or download");
}
}
return succeeded;
}
@Override
protected Object executeAction() {
// If triggered by alarm will not have unregistered yet
unregister();
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
queueActions(this);
} else {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ProcessPendingMessagesAction: Not default SMS app; rescheduling");
}
scheduleProcessPendingMessagesAction(true, this);
}
return null;
}
private static String findNextMessageToSend(final DatabaseWrapper db, final long now) {
String toSendMessageId = null;
db.beginTransaction();
Cursor sending = null;
Cursor cursor = null;
int sendingCnt = 0;
int pendingCnt = 0;
int failedCnt = 0;
try {
// First check to see if we have any messages already sending
sending = db.query(DatabaseHelper.MESSAGES_TABLE,
MessageData.getProjection(),
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
new String[]{Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_SENDING),
Integer.toString(MessageData.BUGLE_STATUS_OUTGOING_RESENDING)},
null,
null,
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
final boolean messageCurrentlySending = sending.moveToNext();
sendingCnt = sending.getCount();
// Look for messages we could send
final ContentValues values = new ContentValues();
values.put(DatabaseHelper.MessageColumns.STATUS,
MessageData.BUGLE_STATUS_OUTGOING_FAILED);
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
MessageData.getProjection(),
DatabaseHelper.MessageColumns.STATUS + " IN ("
+ MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND + ","
+ MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY + ")",
null,
null,
null,
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
pendingCnt = cursor.getCount();
while (cursor.moveToNext()) {
final MessageData message = new MessageData();
message.bind(cursor);
if (message.getInResendWindow(now)) {
// If no messages currently sending
if (!messageCurrentlySending) {
// Resend this message
toSendMessageId = message.getMessageId();
// Before queuing the message for resending, check if the message's self is
// active. If not, switch back to the system's default subscription.
if (OsUtil.isAtLeastL_MR1()) {
final ParticipantData messageSelf = BugleDatabaseOperations
.getExistingParticipant(db, message.getSelfId());
if (messageSelf == null || !messageSelf.isActiveSubscription()) {
final ParticipantData defaultSelf = BugleDatabaseOperations
.getOrCreateSelf(db, PhoneUtils.getDefault()
.getDefaultSmsSubscriptionId());
if (defaultSelf != null) {
message.bindSelfId(defaultSelf.getId());
final ContentValues selfValues = new ContentValues();
selfValues.put(MessageColumns.SELF_PARTICIPANT_ID,
defaultSelf.getId());
BugleDatabaseOperations.updateMessageRow(db,
message.getMessageId(), selfValues);
MessagingContentProvider.notifyMessagesChanged(
message.getConversationId());
}
}
}
}
break;
} else {
failedCnt++;
// Mark message as failed
BugleDatabaseOperations.updateMessageRow(db, message.getMessageId(), values);
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
}
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
if (cursor != null) {
cursor.close();
}
if (sending != null) {
sending.close();
}
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
+ sendingCnt + " messages already sending, "
+ pendingCnt + " messages to send, "
+ failedCnt + " failed messages");
}
return toSendMessageId;
}
private static String findNextMessageToDownload(final DatabaseWrapper db, final long now) {
String toDownloadMessageId = null;
db.beginTransaction();
Cursor cursor = null;
int downloadingCnt = 0;
int pendingCnt = 0;
try {
// First check if we have any messages already downloading
downloadingCnt = (int) db.queryNumEntries(DatabaseHelper.MESSAGES_TABLE,
DatabaseHelper.MessageColumns.STATUS + " IN (?, ?)",
new String[] {
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING),
Integer.toString(MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING)
});
// TODO: This query is not actually needed if downloadingCnt == 0.
cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
MessageData.getProjection(),
DatabaseHelper.MessageColumns.STATUS + " =? OR "
+ DatabaseHelper.MessageColumns.STATUS + " =?",
new String[]{
Integer.toString(
MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD),
Integer.toString(
MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD)
},
null,
null,
DatabaseHelper.MessageColumns.RECEIVED_TIMESTAMP + " ASC");
pendingCnt = cursor.getCount();
// If no messages are currently downloading and there is a download pending,
// queue the download of the oldest pending message.
if (downloadingCnt == 0 && cursor.moveToNext()) {
// Always start the next pending message. We will check if a download has
// expired in DownloadMmsAction and mark message failed there.
final MessageData message = new MessageData();
message.bind(cursor);
toDownloadMessageId = message.getMessageId();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
if (cursor != null) {
cursor.close();
}
}
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ProcessPendingMessagesAction: "
+ downloadingCnt + " messages already downloading, "
+ pendingCnt + " messages to download");
}
return toDownloadMessageId;
}
private ProcessPendingMessagesAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ProcessPendingMessagesAction> CREATOR
= new Parcelable.Creator<ProcessPendingMessagesAction>() {
@Override
public ProcessPendingMessagesAction createFromParcel(final Parcel in) {
return new ProcessPendingMessagesAction(in);
}
@Override
public ProcessPendingMessagesAction[] newArray(final int size) {
return new ProcessPendingMessagesAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,310 +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.datamodel.action;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.telephony.PhoneNumberUtils;
import android.telephony.SmsManager;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MmsFileProvider;
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.pdu.SendConf;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.sms.MmsSender;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.io.File;
import java.util.ArrayList;
/**
* Update message status to reflect success or failure
* Can also update the message itself if a "final" message is now available from telephony db
*/
public class ProcessSentMessageAction extends Action {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
// These are always set
private static final String KEY_SMS = "is_sms";
private static final String KEY_SENT_BY_PLATFORM = "sent_by_platform";
// These are set when we're processing a message sent by the user. They are null for messages
// sent automatically (e.g. a NotifyRespInd/AcknowledgeInd sent in response to a download).
private static final String KEY_MESSAGE_ID = "message_id";
private static final String KEY_MESSAGE_URI = "message_uri";
private static final String KEY_UPDATED_MESSAGE_URI = "updated_message_uri";
private static final String KEY_SUB_ID = "sub_id";
// These are set for messages sent by the platform (L+)
public static final String KEY_RESULT_CODE = "result_code";
public static final String KEY_HTTP_STATUS_CODE = "http_status_code";
private static final String KEY_CONTENT_URI = "content_uri";
private static final String KEY_RESPONSE = "response";
private static final String KEY_RESPONSE_IMPORTANT = "response_important";
// These are set for messages we sent ourself (legacy), or which we fast-failed before sending.
private static final String KEY_STATUS = "status";
private static final String KEY_RAW_STATUS = "raw_status";
// This is called when MMS lib API returns via PendingIntent
public static void processMmsSent(final int resultCode, final Uri messageUri,
final Bundle extras) {
final ProcessSentMessageAction action = new ProcessSentMessageAction();
final Bundle params = action.actionParameters;
params.putBoolean(KEY_SMS, false);
params.putBoolean(KEY_SENT_BY_PLATFORM, true);
params.putString(KEY_MESSAGE_ID, extras.getString(SendMessageAction.EXTRA_MESSAGE_ID));
params.putParcelable(KEY_MESSAGE_URI, messageUri);
params.putParcelable(KEY_UPDATED_MESSAGE_URI,
extras.getParcelable(SendMessageAction.EXTRA_UPDATED_MESSAGE_URI));
params.putInt(KEY_SUB_ID,
extras.getInt(SendMessageAction.KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID));
params.putInt(KEY_RESULT_CODE, resultCode);
params.putInt(KEY_HTTP_STATUS_CODE, extras.getInt(SmsManager.EXTRA_MMS_HTTP_STATUS, 0));
params.putParcelable(KEY_CONTENT_URI,
extras.getParcelable(SendMessageAction.EXTRA_CONTENT_URI));
params.putByteArray(KEY_RESPONSE, extras.getByteArray(SmsManager.EXTRA_MMS_DATA));
params.putBoolean(KEY_RESPONSE_IMPORTANT,
extras.getBoolean(SendMessageAction.EXTRA_RESPONSE_IMPORTANT));
action.start();
}
public static void processMessageSentFastFailed(final String messageId,
final Uri messageUri, final Uri updatedMessageUri, final int subId, final boolean isSms,
final int status, final int rawStatus, final int resultCode) {
final ProcessSentMessageAction action = new ProcessSentMessageAction();
final Bundle params = action.actionParameters;
params.putBoolean(KEY_SMS, isSms);
params.putBoolean(KEY_SENT_BY_PLATFORM, false);
params.putString(KEY_MESSAGE_ID, messageId);
params.putParcelable(KEY_MESSAGE_URI, messageUri);
params.putParcelable(KEY_UPDATED_MESSAGE_URI, updatedMessageUri);
params.putInt(KEY_SUB_ID, subId);
params.putInt(KEY_STATUS, status);
params.putInt(KEY_RAW_STATUS, rawStatus);
params.putInt(KEY_RESULT_CODE, resultCode);
action.start();
}
private ProcessSentMessageAction() {
// Callers must use one of the static methods above
}
/**
* Update message status to reflect success or failure
* Can also update the message itself if a "final" message is now available from telephony db
*/
@Override
protected Object executeAction() {
final Context context = Factory.get().getApplicationContext();
final String messageId = actionParameters.getString(KEY_MESSAGE_ID);
final Uri messageUri = actionParameters.getParcelable(KEY_MESSAGE_URI);
final Uri updatedMessageUri = actionParameters.getParcelable(KEY_UPDATED_MESSAGE_URI);
final boolean isSms = actionParameters.getBoolean(KEY_SMS);
final boolean sentByPlatform = actionParameters.getBoolean(KEY_SENT_BY_PLATFORM);
int status = actionParameters.getInt(KEY_STATUS, MmsUtils.MMS_REQUEST_MANUAL_RETRY);
int rawStatus = actionParameters.getInt(KEY_RAW_STATUS,
MmsUtils.PDU_HEADER_VALUE_UNDEFINED);
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
if (sentByPlatform) {
// Delete temporary file backing the contentUri passed to MMS service
final Uri contentUri = actionParameters.getParcelable(KEY_CONTENT_URI);
Assert.isTrue(contentUri != null);
final File tempFile = MmsFileProvider.getFile(contentUri);
long messageSize = 0;
if (tempFile.exists()) {
messageSize = tempFile.length();
tempFile.delete();
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ProcessSentMessageAction: Deleted temp file with outgoing "
+ "MMS pdu: " + contentUri);
}
}
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
final boolean responseImportant = actionParameters.getBoolean(KEY_RESPONSE_IMPORTANT);
if (resultCode == Activity.RESULT_OK) {
if (responseImportant) {
// Get the status from the response PDU and update telephony
final byte[] response = actionParameters.getByteArray(KEY_RESPONSE);
final SendConf sendConf = MmsSender.parseSendConf(response, subId);
if (sendConf != null) {
final MmsUtils.StatusPlusUri result =
MmsUtils.updateSentMmsMessageStatus(context, messageUri, sendConf);
status = result.status;
rawStatus = result.rawStatus;
}
}
} else {
String errorMsg = "ProcessSentMessageAction: Platform returned error resultCode: "
+ resultCode;
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
if (httpStatusCode != 0) {
errorMsg += (", HTTP status code: " + httpStatusCode);
}
LogUtil.w(TAG, errorMsg);
status = MmsSender.getErrorResultStatus(resultCode, httpStatusCode);
// Check for MMS messages that failed because they exceeded the maximum size,
// indicated by an I/O error from the platform.
if (resultCode == SmsManager.MMS_ERROR_IO_ERROR) {
if (messageSize > MmsConfig.get(subId).getMaxMessageSize()) {
rawStatus = MessageData.RAW_TELEPHONY_STATUS_MESSAGE_TOO_BIG;
}
}
}
}
if (messageId != null) {
final int resultCode = actionParameters.getInt(KEY_RESULT_CODE);
final int httpStatusCode = actionParameters.getInt(KEY_HTTP_STATUS_CODE);
processResult(
messageId, updatedMessageUri, status, rawStatus, isSms, this, subId,
resultCode, httpStatusCode);
} else {
if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
LogUtil.v(TAG, "ProcessSentMessageAction: No sent message to process (it was "
+ "probably a notify response for an MMS download)");
}
}
return null;
}
static void processResult(final String messageId, Uri updatedMessageUri, int status,
final int rawStatus, final boolean isSms, final Action processingAction,
final int subId, final int resultCode, final int httpStatusCode) {
final DatabaseWrapper db = DataModel.get().getDatabase();
MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
final MessageData originalMessage = message;
if (message == null) {
LogUtil.w(TAG, "ProcessSentMessageAction: Sent message " + messageId
+ " missing from local database");
return;
}
final String conversationId = message.getConversationId();
if (updatedMessageUri != null) {
// Update message if we have newly written final message in the telephony db
final MessageData update = MmsUtils.readSendingMmsMessage(updatedMessageUri,
conversationId, message.getParticipantId(), message.getSelfId());
if (update != null) {
// Set message Id of final message to that of the existing place holder.
update.updateMessageId(message.getMessageId());
// Update image sizes.
update.updateSizesForImageParts();
// Temp attachments are no longer needed
for (final MessagePartData part : message.getParts()) {
part.destroySync();
}
message = update;
// processResult will rewrite the complete message as part of update
} else {
updatedMessageUri = null;
status = MmsUtils.MMS_REQUEST_MANUAL_RETRY;
LogUtil.e(TAG, "ProcessSentMessageAction: Unable to read sending message");
}
}
final long timestamp = System.currentTimeMillis();
boolean failed;
if (status == MmsUtils.MMS_REQUEST_SUCCEEDED) {
message.markMessageSent(timestamp);
failed = false;
} else if (status == MmsUtils.MMS_REQUEST_AUTO_RETRY
&& message.getInResendWindow(timestamp)) {
message.markMessageNotSent(timestamp);
message.setRawTelephonyStatus(rawStatus);
failed = false;
} else {
message.markMessageFailed(timestamp);
message.setRawTelephonyStatus(rawStatus);
message.setMessageSeen(false);
failed = true;
}
// We have special handling for when a message to an emergency number fails. In this case,
// we notify immediately of any failure (even if we auto-retry), and instruct the user to
// try calling the emergency number instead.
if (status != MmsUtils.MMS_REQUEST_SUCCEEDED) {
final ArrayList<String> recipients =
BugleDatabaseOperations.getRecipientsForConversation(db, conversationId);
for (final String recipient : recipients) {
if (PhoneNumberUtils.isEmergencyNumber(recipient)) {
BugleNotifications.notifyEmergencySmsFailed(recipient, conversationId);
message.markMessageFailedEmergencyNumber(timestamp);
failed = true;
break;
}
}
}
// Update the message status and optionally refresh the message with final parts/values.
if (SendMessageAction.updateMessageAndStatus(isSms, message, updatedMessageUri, failed)) {
// We shouldn't show any notifications if we're not allowed to modify Telephony for
// this message.
if (failed) {
BugleNotifications.update(false, BugleNotifications.UPDATE_ERRORS);
}
BugleActionToasts.onSendMessageOrManualDownloadActionCompleted(
conversationId, !failed, status, isSms, subId, true/*isSend*/);
}
LogUtil.i(TAG, "ProcessSentMessageAction: Done sending " + (isSms ? "SMS" : "MMS")
+ " message " + message.getMessageId()
+ " in conversation " + conversationId
+ "; status is " + MmsUtils.getRequestStatusDescription(status));
// Whether we succeeded or failed we will check and maybe schedule some more work
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(
status != MmsUtils.MMS_REQUEST_SUCCEEDED, processingAction);
}
private ProcessSentMessageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ProcessSentMessageAction> CREATOR
= new Parcelable.Creator<ProcessSentMessageAction>() {
@Override
public ProcessSentMessageAction createFromParcel(final Parcel in) {
return new ProcessSentMessageAction(in);
}
@Override
public ProcessSentMessageAction[] newArray(final int size) {
return new ProcessSentMessageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -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.datamodel.action;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.Assert;
import com.android.messaging.util.Assert.RunsOnMainThread;
import com.android.messaging.util.LogUtil;
import com.google.common.annotations.VisibleForTesting;
public class ReadDraftDataAction extends Action implements Parcelable {
/**
* Interface for ReadDraftDataAction listeners
*/
public interface ReadDraftDataActionListener {
@RunsOnMainThread
abstract void onReadDraftDataSucceeded(final ReadDraftDataAction action,
final Object data, final MessageData message,
final ConversationListItemData conversation);
@RunsOnMainThread
abstract void onReadDraftDataFailed(final ReadDraftDataAction action, final Object data);
}
/**
* Read draft message and associated data (with listener)
*/
public static ReadDraftDataActionMonitor readDraftData(final String conversationId,
final MessageData incomingDraft, final Object data,
final ReadDraftDataActionListener listener) {
final ReadDraftDataActionMonitor monitor = new ReadDraftDataActionMonitor(data,
listener);
final ReadDraftDataAction action = new ReadDraftDataAction(conversationId,
incomingDraft, monitor.getActionKey());
action.start(monitor);
return monitor;
}
private static final String KEY_CONVERSATION_ID = "conversationId";
private static final String KEY_INCOMING_DRAFT = "draftMessage";
private ReadDraftDataAction(final String conversationId, final MessageData incomingDraft,
final String actionKey) {
super(actionKey);
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
actionParameters.putParcelable(KEY_INCOMING_DRAFT, incomingDraft);
}
@VisibleForTesting
class DraftData {
public final MessageData message;
public final ConversationListItemData conversation;
DraftData(final MessageData message, final ConversationListItemData conversation) {
this.message = message;
this.conversation = conversation;
}
}
@Override
protected Object executeAction() {
final DatabaseWrapper db = DataModel.get().getDatabase();
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
final MessageData incomingDraft = actionParameters.getParcelable(KEY_INCOMING_DRAFT);
final ConversationListItemData conversation =
ConversationListItemData.getExistingConversation(db, conversationId);
MessageData message = null;
if (conversation != null) {
if (incomingDraft == null) {
message = BugleDatabaseOperations.readDraftMessageData(db, conversationId,
conversation.getSelfId());
}
if (message == null) {
message = MessageData.createDraftMessage(conversationId, conversation.getSelfId(),
incomingDraft);
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: created draft. "
+ "conversationId=" + conversationId
+ " selfId=" + conversation.getSelfId());
} else {
LogUtil.d(LogUtil.BUGLE_TAG, "ReadDraftMessage: read draft. "
+ "conversationId=" + conversationId
+ " selfId=" + conversation.getSelfId());
}
return new DraftData(message, conversation);
}
return null;
}
/**
* An operation that notifies a listener upon completion
*/
public static class ReadDraftDataActionMonitor extends ActionMonitor
implements ActionCompletedListener {
private final ReadDraftDataActionListener mListener;
ReadDraftDataActionMonitor(final Object data,
final ReadDraftDataActionListener completed) {
super(STATE_CREATED, generateUniqueActionKey("ReadDraftDataAction"), data);
setCompletedListener(this);
mListener = completed;
}
@Override
public void onActionSucceeded(final ActionMonitor monitor,
final Action action, final Object data, final Object result) {
final DraftData draft = (DraftData) result;
if (draft == null) {
mListener.onReadDraftDataFailed((ReadDraftDataAction) action, data);
} else {
mListener.onReadDraftDataSucceeded((ReadDraftDataAction) action, data,
draft.message, draft.conversation);
}
}
@Override
public void onActionFailed(final ActionMonitor monitor,
final Action action, final Object data, final Object result) {
Assert.fail("Reading draft should not fail");
}
}
private ReadDraftDataAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ReadDraftDataAction> CREATOR
= new Parcelable.Creator<ReadDraftDataAction>() {
@Override
public ReadDraftDataAction createFromParcel(final Parcel in) {
return new ReadDraftDataAction(in);
}
@Override
public ReadDraftDataAction[] newArray(final int size) {
return new ReadDraftDataAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,197 +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.datamodel.action;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DataModelException;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.mmslib.pdu.PduHeaders;
import com.android.messaging.sms.DatabaseMessages;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.util.LogUtil;
import java.util.List;
/**
* Action used to "receive" an incoming message
*/
public class ReceiveMmsMessageAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String KEY_SUB_ID = "sub_id";
private static final String KEY_PUSH_DATA = "push_data";
private static final String KEY_TRANSACTION_ID = "transaction_id";
private static final String KEY_CONTENT_LOCATION = "content_location";
/**
* Create a message received from a particular number in a particular conversation
*/
public ReceiveMmsMessageAction(final int subId, final byte[] pushData) {
actionParameters.putInt(KEY_SUB_ID, subId);
actionParameters.putByteArray(KEY_PUSH_DATA, pushData);
}
@Override
protected Object executeAction() {
final Context context = Factory.get().getApplicationContext();
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
final byte[] pushData = actionParameters.getByteArray(KEY_PUSH_DATA);
final DatabaseWrapper db = DataModel.get().getDatabase();
// Write received message to telephony DB
MessageData message = null;
final ParticipantData self = BugleDatabaseOperations.getOrCreateSelf(db, subId);
final long received = System.currentTimeMillis();
// Inform sync that message has been added at local received timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(received);
// TODO: Should use local time to set received time in MMS message
final DatabaseMessages.MmsMessage mms = MmsUtils.processReceivedPdu(
context, pushData, self.getSubId(), self.getNormalizedDestination());
if (mms != null) {
final List<String> recipients = MmsUtils.getRecipientsByThread(mms.mThreadId);
String from = MmsUtils.getMmsSender(recipients, mms.getUri());
if (from == null) {
LogUtil.w(TAG, "Received an MMS without sender address; using unknown sender.");
from = ParticipantData.getUnknownSenderDestination();
}
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
from, subId);
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
db, rawSender.getNormalizedDestination());
final boolean autoDownload = (!blocked && MmsUtils.allowMmsAutoRetrieve(subId));
final String conversationId =
BugleDatabaseOperations.getOrCreateConversationFromThreadId(db, mms.mThreadId,
blocked, subId);
final boolean messageInFocusedConversation =
DataModel.get().isFocusedConversation(conversationId);
final boolean messageInObservableConversation =
DataModel.get().isNewMessageObservable(conversationId);
// TODO: Also write these values to the telephony provider
mms.mRead = messageInFocusedConversation;
mms.mSeen = messageInObservableConversation || blocked;
// Write received placeholder message to our DB
db.beginTransaction();
try {
final String participantId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
final String selfId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
message = MmsUtils.createMmsMessage(mms, conversationId, participantId, selfId,
(autoDownload ? MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD :
MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD));
// Write the message
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
if (!autoDownload) {
BugleDatabaseOperations.updateConversationMetadataInTransaction(db,
conversationId, message.getMessageId(), message.getReceivedTimeStamp(),
blocked, true /* shouldAutoSwitchSelfId */);
final ParticipantData sender = ParticipantData .getFromId(
db, participantId);
BugleActionToasts.onMessageReceived(conversationId, sender, message);
}
// else update the conversation once we have downloaded final message (or failed)
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
// Update conversation if not immediately initiating a download
if (!autoDownload) {
MessagingContentProvider.notifyMessagesChanged(message.getConversationId());
MessagingContentProvider.notifyPartsChanged();
// Show a notification to let the user know a new message has arrived
BugleNotifications.update(false/*silent*/, conversationId,
BugleNotifications.UPDATE_ALL);
// Send the NotifyRespInd with DEFERRED status since no auto download
actionParameters.putString(KEY_TRANSACTION_ID, mms.mTransactionId);
actionParameters.putString(KEY_CONTENT_LOCATION, mms.mContentLocation);
requestBackgroundWork();
}
LogUtil.i(TAG, "ReceiveMmsMessageAction: Received MMS message " + message.getMessageId()
+ " in conversation " + message.getConversationId()
+ ", uri = " + message.getSmsMessageUri());
} else {
LogUtil.e(TAG, "ReceiveMmsMessageAction: Skipping processing of incoming PDU");
}
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
return message;
}
@Override
protected Bundle doBackgroundWork() throws DataModelException {
final Context context = Factory.get().getApplicationContext();
final int subId = actionParameters.getInt(KEY_SUB_ID, ParticipantData.DEFAULT_SELF_SUB_ID);
final String transactionId = actionParameters.getString(KEY_TRANSACTION_ID);
final String contentLocation = actionParameters.getString(KEY_CONTENT_LOCATION);
MmsUtils.sendNotifyResponseForMmsDownload(
context,
subId,
MmsUtils.stringToBytes(transactionId, "UTF-8"),
contentLocation,
PduHeaders.STATUS_DEFERRED);
// We don't need to return anything.
return null;
}
private ReceiveMmsMessageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ReceiveMmsMessageAction> CREATOR
= new Parcelable.Creator<ReceiveMmsMessageAction>() {
@Override
public ReceiveMmsMessageAction createFromParcel(final Parcel in) {
return new ReceiveMmsMessageAction(in);
}
@Override
public ReceiveMmsMessageAction[] newArray(final int size) {
return new ReceiveMmsMessageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}
@@ -1,198 +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.datamodel.action;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Telephony.Sms;
import android.text.TextUtils;
import com.android.messaging.Factory;
import com.android.messaging.datamodel.BugleDatabaseOperations;
import com.android.messaging.datamodel.BugleNotifications;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.SyncManager;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsSmsUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
/**
* Action used to "receive" an incoming message
*/
public class ReceiveSmsMessageAction extends Action implements Parcelable {
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
private static final String KEY_MESSAGE_VALUES = "message_values";
/**
* Create a message received from a particular number in a particular conversation
*/
public ReceiveSmsMessageAction(final ContentValues messageValues) {
actionParameters.putParcelable(KEY_MESSAGE_VALUES, messageValues);
}
@Override
protected Object executeAction() {
final Context context = Factory.get().getApplicationContext();
final ContentValues messageValues = actionParameters.getParcelable(KEY_MESSAGE_VALUES);
final DatabaseWrapper db = DataModel.get().getDatabase();
// Get the SIM subscription ID
Integer subId = messageValues.getAsInteger(Sms.SUBSCRIPTION_ID);
if (subId == null) {
subId = ParticipantData.DEFAULT_SELF_SUB_ID;
}
// Make sure we have a sender address
String address = messageValues.getAsString(Sms.ADDRESS);
if (TextUtils.isEmpty(address)) {
LogUtil.w(TAG, "Received an SMS without an address; using unknown sender.");
address = ParticipantData.getUnknownSenderDestination();
messageValues.put(Sms.ADDRESS, address);
}
final ParticipantData rawSender = ParticipantData.getFromRawPhoneBySimLocale(
address, subId);
// TODO: Should use local timestamp for this?
final long received = messageValues.getAsLong(Sms.DATE);
// Inform sync that message has been added at local received timestamp
final SyncManager syncManager = DataModel.get().getSyncManager();
syncManager.onNewMessageInserted(received);
// Make sure we've got a thread id
final long threadId = MmsSmsUtils.Threads.getOrCreateThreadId(context, address);
messageValues.put(Sms.THREAD_ID, threadId);
final boolean blocked = BugleDatabaseOperations.isBlockedDestination(
db, rawSender.getNormalizedDestination());
final String conversationId = BugleDatabaseOperations.
getOrCreateConversationFromRecipient(db, threadId, blocked, rawSender);
final boolean messageInFocusedConversation =
DataModel.get().isFocusedConversation(conversationId);
final boolean messageInObservableConversation =
DataModel.get().isNewMessageObservable(conversationId);
MessageData message = null;
// Only the primary user gets to insert the message into the telephony db and into bugle's
// db. The secondary user goes through this path, but skips doing the actual insert. It
// goes through this path because it needs to compute messageInFocusedConversation in order
// to calculate whether to skip the notification and play a soft sound if the user is
// already in the conversation.
if (!OsUtil.isSecondaryUser()) {
final boolean read = messageValues.getAsBoolean(Sms.Inbox.READ)
|| messageInFocusedConversation;
// If you have read it you have seen it
final boolean seen = read || messageInObservableConversation || blocked;
messageValues.put(Sms.Inbox.READ, read ? Integer.valueOf(1) : Integer.valueOf(0));
// incoming messages are marked as seen in the telephony db
messageValues.put(Sms.Inbox.SEEN, 1);
// Insert into telephony
final Uri messageUri = context.getContentResolver().insert(Sms.Inbox.CONTENT_URI,
messageValues);
if (messageUri != null) {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ReceiveSmsMessageAction: Inserted SMS message into telephony, "
+ "uri = " + messageUri);
}
} else {
LogUtil.e(TAG, "ReceiveSmsMessageAction: Failed to insert SMS into telephony!");
}
final String text = messageValues.getAsString(Sms.BODY);
final String subject = messageValues.getAsString(Sms.SUBJECT);
final long sent = messageValues.getAsLong(Sms.DATE_SENT);
final ParticipantData self = ParticipantData.getSelfParticipant(subId);
final Integer pathPresent = messageValues.getAsInteger(Sms.REPLY_PATH_PRESENT);
final String smsServiceCenter = messageValues.getAsString(Sms.SERVICE_CENTER);
String conversationServiceCenter = null;
// Only set service center if message REPLY_PATH_PRESENT = 1
if (pathPresent != null && pathPresent == 1 && !TextUtils.isEmpty(smsServiceCenter)) {
conversationServiceCenter = smsServiceCenter;
}
db.beginTransaction();
try {
final String participantId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, rawSender);
final String selfId =
BugleDatabaseOperations.getOrCreateParticipantInTransaction(db, self);
message = MessageData.createReceivedSmsMessage(messageUri, conversationId,
participantId, selfId, text, subject, sent, received, seen, read);
BugleDatabaseOperations.insertNewMessageInTransaction(db, message);
BugleDatabaseOperations.updateConversationMetadataInTransaction(db, conversationId,
message.getMessageId(), message.getReceivedTimeStamp(), blocked,
conversationServiceCenter, true /* shouldAutoSwitchSelfId */);
final ParticipantData sender = ParticipantData.getFromId(db, participantId);
BugleActionToasts.onMessageReceived(conversationId, sender, message);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
LogUtil.i(TAG, "ReceiveSmsMessageAction: Received SMS message " + message.getMessageId()
+ " in conversation " + message.getConversationId()
+ ", uri = " + messageUri);
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
} else {
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
LogUtil.d(TAG, "ReceiveSmsMessageAction: Not inserting received SMS message for "
+ "secondary user.");
}
}
// Show a notification to let the user know a new message has arrived
BugleNotifications.update(false/*silent*/, conversationId, BugleNotifications.UPDATE_ALL);
MessagingContentProvider.notifyMessagesChanged(conversationId);
MessagingContentProvider.notifyPartsChanged();
return message;
}
private ReceiveSmsMessageAction(final Parcel in) {
super(in);
}
public static final Parcelable.Creator<ReceiveSmsMessageAction> CREATOR
= new Parcelable.Creator<ReceiveSmsMessageAction>() {
@Override
public ReceiveSmsMessageAction createFromParcel(final Parcel in) {
return new ReceiveSmsMessageAction(in);
}
@Override
public ReceiveSmsMessageAction[] newArray(final int size) {
return new ReceiveSmsMessageAction[size];
}
};
@Override
public void writeToParcel(final Parcel parcel, final int flags) {
writeActionToParcel(parcel, flags);
}
}

Some files were not shown because too many files have changed in this diff Show More