Revert "Initial checkin of AOSP Messaging app."
This reverts commit 461a34b466.
Change-Id: Iac4ca77eeaa94989e91dead49a7959c905bd3078
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) {}
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user