diff options
234 files changed, 88505 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..2487766 --- /dev/null +++ b/Android.mk @@ -0,0 +1,34 @@ +# Copyright (C) 2011 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + src/java/com/android/internal/telephony/ISms.aidl \ + src/java/com/android/internal/telephony/IIccPhoneBook.aidl \ + src/java/com/android/internal/telephony/EventLogTags.logtags \ + +LOCAL_SRC_FILES += $(call all-java-files-under, src/java) + +LOCAL_MODULE_TAGS := optional +LOCAL_MODULE := telephony-common + +include $(BUILD_JAVA_LIBRARY) + +# Include subdirectory makefiles +# ============================================================ +include $(call all-makefiles-under,$(LOCAL_PATH)) + diff --git a/CleanSpec.mk b/CleanSpec.mk new file mode 100644 index 0000000..9a63705 --- /dev/null +++ b/CleanSpec.mk @@ -0,0 +1,45 @@ +# Copyright (C) 2011 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. +# + +# If you don't need to do a full clean build but would like to touch +# a file or delete some intermediate files, add a clean step to the end +# of the list. These steps will only be run once, if they haven't been +# run before. +# +# E.g.: +# $(call add-clean-step, touch -c external/sqlite/sqlite3.h) +# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates) +# +# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with +# files that are missing or have been moved. +# +# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory. +# Use $(OUT_DIR) to refer to the "out" directory. +# +# If you need to re-do something that's already mentioned, just copy +# the command and add it to the bottom of the list. E.g., if a change +# that you made last week required touching a file and a change you +# made today requires touching the same file, just copy the old +# touch step and add it to the end of the list. +# +# ************************************************ +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST +# ************************************************ + +$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/telephony-common_intermediates) + +# ************************************************ +# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST +# ************************************************ diff --git a/mockril/Android.mk b/mockril/Android.mk new file mode 100644 index 0000000..95ae84c --- /dev/null +++ b/mockril/Android.mk @@ -0,0 +1,30 @@ +# +# Copyright (C) 2010 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. +# +# + +LOCAL_PATH:=$(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_JAVA_LIBRARIES := core framework + +LOCAL_STATIC_JAVA_LIBRARIES := librilproto-java + +LOCAL_MODULE := mockrilcontroller + +include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/mockril/src/com/android/internal/telephony/mockril/MockRilController.java b/mockril/src/com/android/internal/telephony/mockril/MockRilController.java new file mode 100644 index 0000000..0e75c72 --- /dev/null +++ b/mockril/src/com/android/internal/telephony/mockril/MockRilController.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2010, 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.internal.telephony.mockril; + +import android.os.Bundle; +import android.util.Log; + +import com.android.internal.communication.MsgHeader; +import com.android.internal.communication.Msg; +import com.android.internal.telephony.RilChannel; +import com.android.internal.telephony.ril_proto.RilCtrlCmds; +import com.android.internal.telephony.ril_proto.RilCmds; +import com.google.protobuf.micro.MessageMicro; + +import java.io.IOException; + +/** + * Contain a list of commands to control Mock RIL. Before using these commands the devices + * needs to be set with Mock RIL. Refer to hardware/ril/mockril/README.txt for details. + * + */ +public class MockRilController { + private static final String TAG = "MockRILController"; + private RilChannel mRilChannel = null; + private Msg mMessage = null; + + public MockRilController() throws IOException { + mRilChannel = RilChannel.makeRilChannel(); + } + + /** + * Close the channel after the communication is done. + * This method has to be called after the test is finished. + */ + public void closeChannel() { + mRilChannel.close(); + } + + /** + * Send commands and return true on success + * @param cmd for MsgHeader + * @param token for MsgHeader + * @param status for MsgHeader + * @param pbData for Msg data + * @return true if command is sent successfully, false if it fails + */ + private boolean sendCtrlCommand(int cmd, long token, int status, MessageMicro pbData) { + try { + Msg.send(mRilChannel, cmd, token, status, pbData); + } catch (IOException e) { + Log.v(TAG, "send command : %d failed: " + e.getStackTrace()); + return false; + } + return true; + } + + /** + * Get control response + * @return Msg if response is received, else return null. + */ + private Msg getCtrlResponse() { + Msg response = null; + try { + response = Msg.recv(mRilChannel); + } catch (IOException e) { + Log.v(TAG, "receive response for getRadioState() error: " + e.getStackTrace()); + return null; + } + return response; + } + + /** + * @return the radio state if it is valid, otherwise return -1 + */ + public int getRadioState() { + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_GET_RADIO_STATE, 0, 0, null)) { + return -1; + } + Msg response = getCtrlResponse(); + if (response == null) { + Log.v(TAG, "failed to get response"); + return -1; + } + response.printHeader(TAG); + RilCtrlCmds.CtrlRspRadioState resp = + response.getDataAs(RilCtrlCmds.CtrlRspRadioState.class); + int state = resp.getState(); + if ((state >= RilCmds.RADIOSTATE_OFF) && (state <= RilCmds.RADIOSTATE_NV_READY)) + return state; + else + return -1; + } + + /** + * Set the radio state of mock ril to the given state + * @param state for given radio state + * @return true if the state is set successful, false if it fails + */ + public boolean setRadioState(int state) { + RilCtrlCmds.CtrlReqRadioState req = new RilCtrlCmds.CtrlReqRadioState(); + if (state < 0 || state > RilCmds.RADIOSTATE_NV_READY) { + Log.v(TAG, "the give radio state is not valid."); + return false; + } + req.setState(state); + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_SET_RADIO_STATE, 0, 0, req)) { + Log.v(TAG, "send set radio state request failed."); + return false; + } + Msg response = getCtrlResponse(); + if (response == null) { + Log.v(TAG, "failed to get response for setRadioState"); + return false; + } + response.printHeader(TAG); + RilCtrlCmds.CtrlRspRadioState resp = + response.getDataAs(RilCtrlCmds.CtrlRspRadioState.class); + int curstate = resp.getState(); + return curstate == state; + } + + /** + * Start an incoming call for the given phone number + * + * @param phoneNumber is the number to show as incoming call + * @return true if the incoming call is started successfully, false if it fails. + */ + public boolean startIncomingCall(String phoneNumber) { + RilCtrlCmds.CtrlReqSetMTCall req = new RilCtrlCmds.CtrlReqSetMTCall(); + + req.setPhoneNumber(phoneNumber); + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_SET_MT_CALL, 0, 0, req)) { + Log.v(TAG, "send CMD_SET_MT_CALL request failed"); + return false; + } + return true; + } + + /** + * Hang up a connection remotelly for the given call fail cause + * + * @param connectionID is the connection to be hung up + * @param failCause is the call fail cause defined in ril.h + * @return true if the hangup is successful, false if it fails + */ + public boolean hangupRemote(int connectionId, int failCause) { + RilCtrlCmds.CtrlHangupConnRemote req = new RilCtrlCmds.CtrlHangupConnRemote(); + req.setConnectionId(connectionId); + req.setCallFailCause(failCause); + + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_HANGUP_CONN_REMOTE, 0, 0, req)) { + Log.v(TAG, "send CTRL_CMD_HANGUP_CONN_REMOTE request failed"); + return false; + } + return true; + } + + /** + * Set call transition flag to the Mock Ril + * + * @param flag is a boolean value for the call transiton flag + * true: call transition: dialing->alert, alert->active is controlled + * false: call transition is automatically handled by Mock Ril + * @return true if the request is successful, false if it failed to set the flag + */ + public boolean setCallTransitionFlag(boolean flag) { + RilCtrlCmds.CtrlSetCallTransitionFlag req = new RilCtrlCmds.CtrlSetCallTransitionFlag(); + + req.setFlag(flag); + + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_SET_CALL_TRANSITION_FLAG, 0, 0, req)) { + Log.v(TAG, "send CTRL_CMD_SET_CALL_TRANSITION_FLAG request failed"); + return false; + } + return true; + } + + /** + * Set the dialing call to alert if the call transition flag is true + * + * @return true if the call transition is successful, false if it fails + */ + public boolean setDialCallToAlert() { + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_SET_CALL_ALERT, 0, 0, null)) { + Log.v(TAG, "send CTRL_CMD_SET_CALL_ALERT request failed"); + return false; + } + return true; + } + + /** + * Set the alert call to active if the call transition flag is true + * + * @return true if the call transition is successful, false if it fails + */ + public boolean setAlertCallToActive() { + if (!sendCtrlCommand(RilCtrlCmds.CTRL_CMD_SET_CALL_ACTIVE, 0, 0, null)) { + Log.v(TAG, "send CTRL_CMD_SET_CALL_ACTIVE request failed"); + return false; + } + return true; + } +} diff --git a/src/java/android/provider/Telephony.java b/src/java/android/provider/Telephony.java new file mode 100644 index 0000000..dd8be66 --- /dev/null +++ b/src/java/android/provider/Telephony.java @@ -0,0 +1,1993 @@ +/* + * Copyright (C) 2006 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.provider; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SqliteWrapper; +import android.net.Uri; +import android.os.Environment; +import android.telephony.SmsMessage; +import android.text.TextUtils; +import android.util.Log; +import android.util.Patterns; + + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The Telephony provider contains data related to phone operation. + * + * @hide + */ +public final class Telephony { + private static final String TAG = "Telephony"; + private static final boolean DEBUG = true; + private static final boolean LOCAL_LOGV = false; + + // Constructor + public Telephony() { + } + + /** + * Base columns for tables that contain text based SMSs. + */ + public interface TextBasedSmsColumns { + /** + * The type of the message + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + public static final int MESSAGE_TYPE_ALL = 0; + public static final int MESSAGE_TYPE_INBOX = 1; + public static final int MESSAGE_TYPE_SENT = 2; + public static final int MESSAGE_TYPE_DRAFT = 3; + public static final int MESSAGE_TYPE_OUTBOX = 4; + public static final int MESSAGE_TYPE_FAILED = 5; // for failed outgoing messages + public static final int MESSAGE_TYPE_QUEUED = 6; // for messages to send later + + + /** + * The thread ID of the message + * <P>Type: INTEGER</P> + */ + public static final String THREAD_ID = "thread_id"; + + /** + * The address of the other party + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + + /** + * The person ID of the sender + * <P>Type: INTEGER (long)</P> + */ + public static final String PERSON_ID = "person"; + + /** + * The date the message was received + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The date the message was sent + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** + * Has the message been read + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * Indicates whether this message has been seen by the user. The "seen" flag will be + * used to figure out whether we need to throw up a statusbar notification or not. + */ + public static final String SEEN = "seen"; + + /** + * The TP-Status value for the message, or -1 if no status has + * been received + */ + public static final String STATUS = "status"; + + public static final int STATUS_NONE = -1; + public static final int STATUS_COMPLETE = 0; + public static final int STATUS_PENDING = 32; + public static final int STATUS_FAILED = 64; + + /** + * The subject of the message, if present + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "subject"; + + /** + * The body of the message + * <P>Type: TEXT</P> + */ + public static final String BODY = "body"; + + /** + * The id of the sender of the conversation, if present + * <P>Type: INTEGER (reference to item in content://contacts/people)</P> + */ + public static final String PERSON = "person"; + + /** + * The protocol identifier code + * <P>Type: INTEGER</P> + */ + public static final String PROTOCOL = "protocol"; + + /** + * Whether the <code>TP-Reply-Path</code> bit was set on this message + * <P>Type: BOOLEAN</P> + */ + public static final String REPLY_PATH_PRESENT = "reply_path_present"; + + /** + * The service center (SC) through which to send the message, if present + * <P>Type: TEXT</P> + */ + public static final String SERVICE_CENTER = "service_center"; + + /** + * Has the message been locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; + + /** + * Error code associated with sending or receiving this message + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "error_code"; + + /** + * Meta data used externally. + * <P>Type: TEXT</P> + */ + public static final String META_DATA = "meta_data"; + } + + /** + * Contains all text based SMS messages. + */ + public static final class Sms implements BaseColumns, TextBasedSmsColumns { + public static final Cursor query(ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query(ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, where, + null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the given URI. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @return the URI for the new message + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport) { + return addMessageToUri(resolver, uri, address, body, subject, + date, read, deliveryReport, -1L); + } + + /** + * Add an SMS to the given URI with thread_id specified. + * + * @param resolver the content resolver to use + * @param uri the URI to add the message to + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @param deliveryReport true if a delivery report was requested, false if not + * @param threadId the thread_id of the message + * @return the URI for the new message + */ + public static Uri addMessageToUri(ContentResolver resolver, + Uri uri, String address, String body, String subject, + Long date, boolean read, boolean deliveryReport, long threadId) { + ContentValues values = new ContentValues(7); + + values.put(ADDRESS, address); + if (date != null) { + values.put(DATE, date); + } + values.put(READ, read ? Integer.valueOf(1) : Integer.valueOf(0)); + values.put(SUBJECT, subject); + values.put(BODY, body); + if (deliveryReport) { + values.put(STATUS, STATUS_PENDING); + } + if (threadId != -1L) { + values.put(THREAD_ID, threadId); + } + return resolver.insert(uri, values); + } + + /** + * Move a message to the given folder. + * + * @param context the context to use + * @param uri the message to move + * @param folder the folder to move to + * @return true if the operation succeeded + */ + public static boolean moveMessageToFolder(Context context, + Uri uri, int folder, int error) { + if (uri == null) { + return false; + } + + boolean markAsUnread = false; + boolean markAsRead = false; + switch(folder) { + case MESSAGE_TYPE_INBOX: + case MESSAGE_TYPE_DRAFT: + break; + case MESSAGE_TYPE_OUTBOX: + case MESSAGE_TYPE_SENT: + markAsRead = true; + break; + case MESSAGE_TYPE_FAILED: + case MESSAGE_TYPE_QUEUED: + markAsUnread = true; + break; + default: + return false; + } + + ContentValues values = new ContentValues(3); + + values.put(TYPE, folder); + if (markAsUnread) { + values.put(READ, Integer.valueOf(0)); + } else if (markAsRead) { + values.put(READ, Integer.valueOf(1)); + } + values.put(ERROR_CODE, error); + + return 1 == SqliteWrapper.update(context, context.getContentResolver(), + uri, values, null, null); + } + + /** + * Returns true iff the folder (message type) identifies an + * outgoing message. + */ + public static boolean isOutgoingFolder(int messageType) { + return (messageType == MESSAGE_TYPE_FAILED) + || (messageType == MESSAGE_TYPE_OUTBOX) + || (messageType == MESSAGE_TYPE_SENT) + || (messageType == MESSAGE_TYPE_QUEUED); + } + + /** + * Contains all text based SMS messages in the SMS app's inbox. + */ + public static final class Inbox implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/inbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param read true if the message has been read, false if not + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean read) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, read, false); + } + } + + /** + * Contains all sent text based SMS messages in the SMS app's. + */ + public static final class Sent implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/sent"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + } + + /** + * Contains all sent text based SMS messages in the SMS app's. + */ + public static final class Draft implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/draft"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Draft box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, false); + } + + /** + * Save over an existing draft message. + * + * @param resolver the content resolver to use + * @param uri of existing message + * @param body the new body for the draft message + * @return true is successful, false otherwise + */ + public static boolean saveMessage(ContentResolver resolver, + Uri uri, String body) { + ContentValues values = new ContentValues(2); + values.put(BODY, body); + values.put(DATE, System.currentTimeMillis()); + return resolver.update(uri, values, null, null) == 1; + } + } + + /** + * Contains all pending outgoing text based SMS messages. + */ + public static final class Outbox implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/outbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * Add an SMS to the Out box. + * + * @param resolver the content resolver to use + * @param address the address of the sender + * @param body the body of the message + * @param subject the psuedo-subject of the message + * @param date the timestamp for the message + * @param deliveryReport whether a delivery report was requested for the message + * @return the URI for the new message + */ + public static Uri addMessage(ContentResolver resolver, + String address, String body, String subject, Long date, + boolean deliveryReport, long threadId) { + return addMessageToUri(resolver, CONTENT_URI, address, body, + subject, date, true, deliveryReport, threadId); + } + } + + /** + * Contains all sent text-based SMS messages in the SMS app's. + */ + public static final class Conversations + implements BaseColumns, TextBasedSmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sms/conversations"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * The first 45 characters of the body of the message + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + + /** + * The number of messages in the conversation + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "msg_count"; + } + + /** + * Contains info about SMS related Intents that are broadcast. + */ + public static final class Intents { + /** + * Set by BroadcastReceiver. Indicates the message was handled + * successfully. + */ + public static final int RESULT_SMS_HANDLED = 1; + + /** + * Set by BroadcastReceiver. Indicates a generic error while + * processing the message. + */ + public static final int RESULT_SMS_GENERIC_ERROR = 2; + + /** + * Set by BroadcastReceiver. Indicates insufficient memory to store + * the message. + */ + public static final int RESULT_SMS_OUT_OF_MEMORY = 3; + + /** + * Set by BroadcastReceiver. Indicates the message, while + * possibly valid, is of a format or encoding that is not + * supported. + */ + public static final int RESULT_SMS_UNSUPPORTED = 4; + + /** + * Broadcast Action: A new text based SMS message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_RECEIVED_ACTION = + "android.provider.Telephony.SMS_RECEIVED"; + + /** + * Broadcast Action: A new data based SMS message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>pdus</em> - An Object[] of byte[]s containing the PDUs + * that make up the message.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String DATA_SMS_RECEIVED_ACTION = + "android.intent.action.DATA_SMS_RECEIVED"; + + /** + * Broadcast Action: A new WAP PUSH message has been received by the + * device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>transactionId (Integer)</em> - The WAP transaction ID</li> + * <li><em>pduType (Integer)</em> - The WAP PDU type</li> + * <li><em>header (byte[])</em> - The header of the message</li> + * <li><em>data (byte[])</em> - The data payload of the message</li> + * <li><em>contentTypeParameters (HashMap<String,String>)</em> + * - Any parameters associated with the content type + * (decoded from the WSP Content-Type header)</li> + * </ul> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + * + * <p>The contentTypeParameters extra value is map of content parameters keyed by + * their names.</p> + * + * <p>If any unassigned well-known parameters are encountered, the key of the map will + * be 'unassigned/0x...', where '...' is the hex value of the unassigned parameter. If + * a parameter has No-Value the value in the map will be null.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String WAP_PUSH_RECEIVED_ACTION = + "android.provider.Telephony.WAP_PUSH_RECEIVED"; + + /** + * Broadcast Action: A new Cell Broadcast message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>message</em> - An SmsCbMessage object containing the broadcast message + * data. This is not an emergency alert, so ETWS and CMAS data will be null.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_CB_RECEIVED_ACTION = + "android.provider.Telephony.SMS_CB_RECEIVED"; + + /** + * Broadcast Action: A new Emergency Broadcast message has been received + * by the device. The intent will have the following extra + * values:</p> + * + * <ul> + * <li><em>message</em> - An SmsCbMessage object containing the broadcast message + * data, including ETWS or CMAS warning notification info if present.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_EMERGENCY_CB_RECEIVED_ACTION = + "android.provider.Telephony.SMS_EMERGENCY_CB_RECEIVED"; + + /** + * Broadcast Action: A new CDMA SMS has been received containing Service Category + * Program Data (updates the list of enabled broadcast channels). The intent will + * have the following extra values:</p> + * + * <ul> + * <li><em>operations</em> - An array of CdmaSmsCbProgramData objects containing + * the service category operations (add/delete/clear) to perform.</li> + * </ul> + * + * <p>The extra values can be extracted using + * {@link #getMessagesFromIntent(Intent)}.</p> + * + * <p>If a BroadcastReceiver encounters an error while processing + * this intent it should set the result code appropriately.</p> + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION = + "android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED"; + + /** + * Broadcast Action: The SIM storage for SMS messages is full. If + * space is not freed, messages targeted for the SIM (class 2) may + * not be saved. + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SIM_FULL_ACTION = + "android.provider.Telephony.SIM_FULL"; + + /** + * Broadcast Action: An incoming SMS has been rejected by the + * telephony framework. This intent is sent in lieu of any + * of the RECEIVED_ACTION intents. The intent will have the + * following extra value:</p> + * + * <ul> + * <li><em>result</em> - An int result code, eg, + * <code>{@link #RESULT_SMS_OUT_OF_MEMORY}</code>, + * indicating the error returned to the network.</li> + * </ul> + + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String SMS_REJECTED_ACTION = + "android.provider.Telephony.SMS_REJECTED"; + + /** + * Read the PDUs out of an {@link #SMS_RECEIVED_ACTION} or a + * {@link #DATA_SMS_RECEIVED_ACTION} intent. + * + * @param intent the intent to read from + * @return an array of SmsMessages for the PDUs + */ + public static SmsMessage[] getMessagesFromIntent( + Intent intent) { + Object[] messages = (Object[]) intent.getSerializableExtra("pdus"); + String format = intent.getStringExtra("format"); + byte[][] pduObjs = new byte[messages.length][]; + + for (int i = 0; i < messages.length; i++) { + pduObjs[i] = (byte[]) messages[i]; + } + byte[][] pdus = new byte[pduObjs.length][]; + int pduCount = pdus.length; + SmsMessage[] msgs = new SmsMessage[pduCount]; + for (int i = 0; i < pduCount; i++) { + pdus[i] = pduObjs[i]; + msgs[i] = SmsMessage.createFromPdu(pdus[i], format); + } + return msgs; + } + } + } + + /** + * Base columns for tables that contain MMSs. + */ + public interface BaseMmsColumns extends BaseColumns { + + public static final int MESSAGE_BOX_ALL = 0; + public static final int MESSAGE_BOX_INBOX = 1; + public static final int MESSAGE_BOX_SENT = 2; + public static final int MESSAGE_BOX_DRAFTS = 3; + public static final int MESSAGE_BOX_OUTBOX = 4; + + /** + * The date the message was received. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * The date the message was sent. + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE_SENT = "date_sent"; + + /** + * The box which the message belong to, for example, MESSAGE_BOX_INBOX. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_BOX = "msg_box"; + + /** + * Has the message been read. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String READ = "read"; + + /** + * Indicates whether this message has been seen by the user. The "seen" flag will be + * used to figure out whether we need to throw up a statusbar notification or not. + */ + public static final String SEEN = "seen"; + + /** + * The Message-ID of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_ID = "m_id"; + + /** + * The subject of the message, if present. + * <P>Type: TEXT</P> + */ + public static final String SUBJECT = "sub"; + + /** + * The character set of the subject, if present. + * <P>Type: INTEGER</P> + */ + public static final String SUBJECT_CHARSET = "sub_cs"; + + /** + * The Content-Type of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct_t"; + + /** + * The Content-Location of the message. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_LOCATION = "ct_l"; + + /** + * The address of the sender. + * <P>Type: TEXT</P> + */ + public static final String FROM = "from"; + + /** + * The address of the recipients. + * <P>Type: TEXT</P> + */ + public static final String TO = "to"; + + /** + * The address of the cc. recipients. + * <P>Type: TEXT</P> + */ + public static final String CC = "cc"; + + /** + * The address of the bcc. recipients. + * <P>Type: TEXT</P> + */ + public static final String BCC = "bcc"; + + /** + * The expiry time of the message. + * <P>Type: INTEGER</P> + */ + public static final String EXPIRY = "exp"; + + /** + * The class of the message. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_CLASS = "m_cls"; + + /** + * The type of the message defined by MMS spec. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_TYPE = "m_type"; + + /** + * The version of specification that this message conform. + * <P>Type: INTEGER</P> + */ + public static final String MMS_VERSION = "v"; + + /** + * The size of the message. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_SIZE = "m_size"; + + /** + * The priority of the message. + * <P>Type: TEXT</P> + */ + public static final String PRIORITY = "pri"; + + /** + * The read-report of the message. + * <P>Type: TEXT</P> + */ + public static final String READ_REPORT = "rr"; + + /** + * Whether the report is allowed. + * <P>Type: TEXT</P> + */ + public static final String REPORT_ALLOWED = "rpt_a"; + + /** + * The response-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String RESPONSE_STATUS = "resp_st"; + + /** + * The status of the message. + * <P>Type: INTEGER</P> + */ + public static final String STATUS = "st"; + + /** + * The transaction-id of the message. + * <P>Type: TEXT</P> + */ + public static final String TRANSACTION_ID = "tr_id"; + + /** + * The retrieve-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRIEVE_STATUS = "retr_st"; + + /** + * The retrieve-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RETRIEVE_TEXT = "retr_txt"; + + /** + * The character set of the retrieve-text. + * <P>Type: TEXT</P> + */ + public static final String RETRIEVE_TEXT_CHARSET = "retr_txt_cs"; + + /** + * The read-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String READ_STATUS = "read_status"; + + /** + * The content-class of the message. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_CLASS = "ct_cls"; + + /** + * The delivery-report of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_REPORT = "d_rpt"; + + /** + * The delivery-time-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_TIME_TOKEN = "d_tm_tok"; + + /** + * The delivery-time of the message. + * <P>Type: INTEGER</P> + */ + public static final String DELIVERY_TIME = "d_tm"; + + /** + * The response-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RESPONSE_TEXT = "resp_txt"; + + /** + * The sender-visibility of the message. + * <P>Type: TEXT</P> + */ + public static final String SENDER_VISIBILITY = "s_vis"; + + /** + * The reply-charging of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING = "r_chg"; + + /** + * The reply-charging-deadline-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_DEADLINE_TOKEN = "r_chg_dl_tok"; + + /** + * The reply-charging-deadline of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_DEADLINE = "r_chg_dl"; + + /** + * The reply-charging-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLY_CHARGING_ID = "r_chg_id"; + + /** + * The reply-charging-size of the message. + * <P>Type: INTEGER</P> + */ + public static final String REPLY_CHARGING_SIZE = "r_chg_sz"; + + /** + * The previously-sent-by of the message. + * <P>Type: TEXT</P> + */ + public static final String PREVIOUSLY_SENT_BY = "p_s_by"; + + /** + * The previously-sent-date of the message. + * <P>Type: INTEGER</P> + */ + public static final String PREVIOUSLY_SENT_DATE = "p_s_d"; + + /** + * The store of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE = "store"; + + /** + * The mm-state of the message. + * <P>Type: INTEGER</P> + */ + public static final String MM_STATE = "mm_st"; + + /** + * The mm-flags-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MM_FLAGS_TOKEN = "mm_flg_tok"; + + /** + * The mm-flags of the message. + * <P>Type: TEXT</P> + */ + public static final String MM_FLAGS = "mm_flg"; + + /** + * The store-status of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE_STATUS = "store_st"; + + /** + * The store-status-text of the message. + * <P>Type: TEXT</P> + */ + public static final String STORE_STATUS_TEXT = "store_st_txt"; + + /** + * The stored of the message. + * <P>Type: TEXT</P> + */ + public static final String STORED = "stored"; + + /** + * The totals of the message. + * <P>Type: TEXT</P> + */ + public static final String TOTALS = "totals"; + + /** + * The mbox-totals of the message. + * <P>Type: TEXT</P> + */ + public static final String MBOX_TOTALS = "mb_t"; + + /** + * The mbox-totals-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MBOX_TOTALS_TOKEN = "mb_t_tok"; + + /** + * The quotas of the message. + * <P>Type: TEXT</P> + */ + public static final String QUOTAS = "qt"; + + /** + * The mbox-quotas of the message. + * <P>Type: TEXT</P> + */ + public static final String MBOX_QUOTAS = "mb_qt"; + + /** + * The mbox-quotas-token of the message. + * <P>Type: INTEGER</P> + */ + public static final String MBOX_QUOTAS_TOKEN = "mb_qt_tok"; + + /** + * The message-count of the message. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "m_cnt"; + + /** + * The start of the message. + * <P>Type: INTEGER</P> + */ + public static final String START = "start"; + + /** + * The distribution-indicator of the message. + * <P>Type: TEXT</P> + */ + public static final String DISTRIBUTION_INDICATOR = "d_ind"; + + /** + * The element-descriptor of the message. + * <P>Type: TEXT</P> + */ + public static final String ELEMENT_DESCRIPTOR = "e_des"; + + /** + * The limit of the message. + * <P>Type: INTEGER</P> + */ + public static final String LIMIT = "limit"; + + /** + * The recommended-retrieval-mode of the message. + * <P>Type: INTEGER</P> + */ + public static final String RECOMMENDED_RETRIEVAL_MODE = "r_r_mod"; + + /** + * The recommended-retrieval-mode-text of the message. + * <P>Type: TEXT</P> + */ + public static final String RECOMMENDED_RETRIEVAL_MODE_TEXT = "r_r_mod_txt"; + + /** + * The status-text of the message. + * <P>Type: TEXT</P> + */ + public static final String STATUS_TEXT = "st_txt"; + + /** + * The applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String APPLIC_ID = "apl_id"; + + /** + * The reply-applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLY_APPLIC_ID = "r_apl_id"; + + /** + * The aux-applic-id of the message. + * <P>Type: TEXT</P> + */ + public static final String AUX_APPLIC_ID = "aux_apl_id"; + + /** + * The drm-content of the message. + * <P>Type: TEXT</P> + */ + public static final String DRM_CONTENT = "drm_c"; + + /** + * The adaptation-allowed of the message. + * <P>Type: TEXT</P> + */ + public static final String ADAPTATION_ALLOWED = "adp_a"; + + /** + * The replace-id of the message. + * <P>Type: TEXT</P> + */ + public static final String REPLACE_ID = "repl_id"; + + /** + * The cancel-id of the message. + * <P>Type: TEXT</P> + */ + public static final String CANCEL_ID = "cl_id"; + + /** + * The cancel-status of the message. + * <P>Type: INTEGER</P> + */ + public static final String CANCEL_STATUS = "cl_st"; + + /** + * The thread ID of the message + * <P>Type: INTEGER</P> + */ + public static final String THREAD_ID = "thread_id"; + + /** + * Has the message been locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; + + /** + * Meta data used externally. + * <P>Type: TEXT</P> + */ + public static final String META_DATA = "meta_data"; + } + + /** + * Columns for the "canonical_addresses" table used by MMS and + * SMS." + */ + public interface CanonicalAddressesColumns extends BaseColumns { + /** + * An address used in MMS or SMS. Email addresses are + * converted to lower case and are compared by string + * equality. Other addresses are compared using + * PHONE_NUMBERS_EQUAL. + * <P>Type: TEXT</P> + */ + public static final String ADDRESS = "address"; + } + + /** + * Columns for the "threads" table used by MMS and SMS. + */ + public interface ThreadsColumns extends BaseColumns { + /** + * The date at which the thread was created. + * + * <P>Type: INTEGER (long)</P> + */ + public static final String DATE = "date"; + + /** + * A string encoding of the recipient IDs of the recipients of + * the message, in numerical order and separated by spaces. + * <P>Type: TEXT</P> + */ + public static final String RECIPIENT_IDS = "recipient_ids"; + + /** + * The message count of the thread. + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_COUNT = "message_count"; + /** + * Indicates whether all messages of the thread have been read. + * <P>Type: INTEGER</P> + */ + public static final String READ = "read"; + + /** + * The snippet of the latest message in the thread. + * <P>Type: TEXT</P> + */ + public static final String SNIPPET = "snippet"; + /** + * The charset of the snippet. + * <P>Type: INTEGER</P> + */ + public static final String SNIPPET_CHARSET = "snippet_cs"; + /** + * Type of the thread, either Threads.COMMON_THREAD or + * Threads.BROADCAST_THREAD. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + /** + * Indicates whether there is a transmission error in the thread. + * <P>Type: INTEGER</P> + */ + public static final String ERROR = "error"; + /** + * Indicates whether this thread contains any attachments. + * <P>Type: INTEGER</P> + */ + public static final String HAS_ATTACHMENT = "has_attachment"; + } + + /** + * Helper functions for the "threads" table used by MMS and SMS. + */ + public static final class Threads implements ThreadsColumns { + private static final String[] ID_PROJECTION = { BaseColumns._ID }; + private static final String STANDARD_ENCODING = "UTF-8"; + private static final Uri THREAD_ID_CONTENT_URI = Uri.parse( + "content://mms-sms/threadID"); + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "conversations"); + public static final Uri OBSOLETE_THREADS_URI = Uri.withAppendedPath( + CONTENT_URI, "obsolete"); + + public static final int COMMON_THREAD = 0; + public static final int BROADCAST_THREAD = 1; + + // No one should construct an instance of this class. + private Threads() { + } + + /** + * This is a single-recipient version of + * getOrCreateThreadId. It's convenient for use with SMS + * messages. + */ + public static long getOrCreateThreadId(Context context, String recipient) { + Set<String> recipients = new HashSet<String>(); + + recipients.add(recipient); + return getOrCreateThreadId(context, recipients); + } + + /** + * Given the recipients list and subject of an unsaved message, + * return its thread ID. If the message starts a new thread, + * allocate a new thread ID. Otherwise, use the appropriate + * existing thread ID. + * + * Find the thread ID of the same set of recipients (in + * any order, without any additions). If one + * is found, return it. Otherwise, return a unique thread ID. + */ + public static long getOrCreateThreadId( + Context context, Set<String> recipients) { + Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon(); + + for (String recipient : recipients) { + if (Mms.isEmailAddress(recipient)) { + recipient = Mms.extractAddrSpec(recipient); + } + + uriBuilder.appendQueryParameter("recipient", recipient); + } + + Uri uri = uriBuilder.build(); + //if (DEBUG) Log.v(TAG, "getOrCreateThreadId uri: " + uri); + + Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(), + uri, ID_PROJECTION, null, null, null); + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + Log.e(TAG, "getOrCreateThreadId returned no rows!"); + } + } finally { + cursor.close(); + } + } + + Log.e(TAG, "getOrCreateThreadId failed with uri " + uri.toString()); + throw new IllegalArgumentException("Unable to find or allocate a thread ID."); + } + } + + /** + * Contains all MMS messages. + */ + public static final class Mms implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://mms"); + + public static final Uri REPORT_REQUEST_URI = Uri.withAppendedPath( + CONTENT_URI, "report-request"); + + public static final Uri REPORT_STATUS_URI = Uri.withAppendedPath( + CONTENT_URI, "report-status"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + + /** + * mailbox = name-addr + * name-addr = [display-name] angle-addr + * angle-addr = [CFWS] "<" addr-spec ">" [CFWS] + */ + public static final Pattern NAME_ADDR_EMAIL_PATTERN = + Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); + + /** + * quoted-string = [CFWS] + * DQUOTE *([FWS] qcontent) [FWS] DQUOTE + * [CFWS] + */ + public static final Pattern QUOTED_STRING_PATTERN = + Pattern.compile("\\s*\"([^\"]*)\"\\s*"); + + public static final Cursor query( + ContentResolver cr, String[] projection) { + return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); + } + + public static final Cursor query( + ContentResolver cr, String[] projection, + String where, String orderBy) { + return cr.query(CONTENT_URI, projection, + where, null, orderBy == null ? DEFAULT_SORT_ORDER : orderBy); + } + + public static final String getMessageBoxName(int msgBox) { + switch (msgBox) { + case MESSAGE_BOX_ALL: + return "all"; + case MESSAGE_BOX_INBOX: + return "inbox"; + case MESSAGE_BOX_SENT: + return "sent"; + case MESSAGE_BOX_DRAFTS: + return "drafts"; + case MESSAGE_BOX_OUTBOX: + return "outbox"; + default: + throw new IllegalArgumentException("Invalid message box: " + msgBox); + } + } + + public static String extractAddrSpec(String address) { + Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address); + + if (match.matches()) { + return match.group(2); + } + return address; + } + + /** + * Returns true if the address is an email address + * + * @param address the input address to be tested + * @return true if address is an email address + */ + public static boolean isEmailAddress(String address) { + if (TextUtils.isEmpty(address)) { + return false; + } + + String s = extractAddrSpec(address); + Matcher match = Patterns.EMAIL_ADDRESS.matcher(s); + return match.matches(); + } + + /** + * Returns true if the number is a Phone number + * + * @param number the input number to be tested + * @return true if number is a Phone number + */ + public static boolean isPhoneNumber(String number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Matcher match = Patterns.PHONE.matcher(number); + return match.matches(); + } + + /** + * Contains all MMS messages in the MMS app's inbox. + */ + public static final class Inbox implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/inbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's sent box. + */ + public static final class Sent implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/sent"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's drafts box. + */ + public static final class Draft implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/drafts"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + /** + * Contains all MMS messages in the MMS app's outbox. + */ + public static final class Outbox implements BaseMmsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri + CONTENT_URI = Uri.parse("content://mms/outbox"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "date DESC"; + } + + public static final class Addr implements BaseColumns { + /** + * The ID of MM which this address entry belongs to. + */ + public static final String MSG_ID = "msg_id"; + + /** + * The ID of contact entry in Phone Book. + */ + public static final String CONTACT_ID = "contact_id"; + + /** + * The address text. + */ + public static final String ADDRESS = "address"; + + /** + * Type of address, must be one of PduHeaders.BCC, + * PduHeaders.CC, PduHeaders.FROM, PduHeaders.TO. + */ + public static final String TYPE = "type"; + + /** + * Character set of this entry. + */ + public static final String CHARSET = "charset"; + } + + public static final class Part implements BaseColumns { + /** + * The identifier of the message which this part belongs to. + * <P>Type: INTEGER</P> + */ + public static final String MSG_ID = "mid"; + + /** + * The order of the part. + * <P>Type: INTEGER</P> + */ + public static final String SEQ = "seq"; + + /** + * The content type of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_TYPE = "ct"; + + /** + * The name of the part. + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + + /** + * The charset of the part. + * <P>Type: TEXT</P> + */ + public static final String CHARSET = "chset"; + + /** + * The file name of the part. + * <P>Type: TEXT</P> + */ + public static final String FILENAME = "fn"; + + /** + * The content disposition of the part. + * <P>Type: TEXT</P> + */ + public static final String CONTENT_DISPOSITION = "cd"; + + /** + * The content ID of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_ID = "cid"; + + /** + * The content location of the part. + * <P>Type: INTEGER</P> + */ + public static final String CONTENT_LOCATION = "cl"; + + /** + * The start of content-type of the message. + * <P>Type: INTEGER</P> + */ + public static final String CT_START = "ctt_s"; + + /** + * The type of content-type of the message. + * <P>Type: TEXT</P> + */ + public static final String CT_TYPE = "ctt_t"; + + /** + * The location(on filesystem) of the binary data of the part. + * <P>Type: INTEGER</P> + */ + public static final String _DATA = "_data"; + + public static final String TEXT = "text"; + + } + + public static final class Rate { + public static final Uri CONTENT_URI = Uri.withAppendedPath( + Mms.CONTENT_URI, "rate"); + /** + * When a message was successfully sent. + * <P>Type: INTEGER</P> + */ + public static final String SENT_TIME = "sent_time"; + } + + public static final class Intents { + private Intents() { + // Non-instantiatable. + } + + /** + * The extra field to store the contents of the Intent, + * which should be an array of Uri. + */ + public static final String EXTRA_CONTENTS = "contents"; + /** + * The extra field to store the type of the contents, + * which should be an array of String. + */ + public static final String EXTRA_TYPES = "types"; + /** + * The extra field to store the 'Cc' addresses. + */ + public static final String EXTRA_CC = "cc"; + /** + * The extra field to store the 'Bcc' addresses; + */ + public static final String EXTRA_BCC = "bcc"; + /** + * The extra field to store the 'Subject'. + */ + public static final String EXTRA_SUBJECT = "subject"; + /** + * Indicates that the contents of specified URIs were changed. + * The application which is showing or caching these contents + * should be updated. + */ + public static final String + CONTENT_CHANGED_ACTION = "android.intent.action.CONTENT_CHANGED"; + /** + * An extra field which stores the URI of deleted contents. + */ + public static final String DELETED_CONTENTS = "deleted_contents"; + } + } + + /** + * Contains all MMS and SMS messages. + */ + public static final class MmsSms implements BaseColumns { + /** + * The column to distinguish SMS & MMS messages in query results. + */ + public static final String TYPE_DISCRIMINATOR_COLUMN = + "transport_type"; + + public static final Uri CONTENT_URI = Uri.parse("content://mms-sms/"); + + public static final Uri CONTENT_CONVERSATIONS_URI = Uri.parse( + "content://mms-sms/conversations"); + + public static final Uri CONTENT_FILTER_BYPHONE_URI = Uri.parse( + "content://mms-sms/messages/byphone"); + + public static final Uri CONTENT_UNDELIVERED_URI = Uri.parse( + "content://mms-sms/undelivered"); + + public static final Uri CONTENT_DRAFT_URI = Uri.parse( + "content://mms-sms/draft"); + + public static final Uri CONTENT_LOCKED_URI = Uri.parse( + "content://mms-sms/locked"); + + /*** + * Pass in a query parameter called "pattern" which is the text + * to search for. + * The sort order is fixed to be thread_id ASC,date DESC. + */ + public static final Uri SEARCH_URI = Uri.parse( + "content://mms-sms/search"); + + // Constants for message protocol types. + public static final int SMS_PROTO = 0; + public static final int MMS_PROTO = 1; + + // Constants for error types of pending messages. + public static final int NO_ERROR = 0; + public static final int ERR_TYPE_GENERIC = 1; + public static final int ERR_TYPE_SMS_PROTO_TRANSIENT = 2; + public static final int ERR_TYPE_MMS_PROTO_TRANSIENT = 3; + public static final int ERR_TYPE_TRANSPORT_FAILURE = 4; + public static final int ERR_TYPE_GENERIC_PERMANENT = 10; + public static final int ERR_TYPE_SMS_PROTO_PERMANENT = 11; + public static final int ERR_TYPE_MMS_PROTO_PERMANENT = 12; + + public static final class PendingMessages implements BaseColumns { + public static final Uri CONTENT_URI = Uri.withAppendedPath( + MmsSms.CONTENT_URI, "pending"); + /** + * The type of transport protocol(MMS or SMS). + * <P>Type: INTEGER</P> + */ + public static final String PROTO_TYPE = "proto_type"; + /** + * The ID of the message to be sent or downloaded. + * <P>Type: INTEGER</P> + */ + public static final String MSG_ID = "msg_id"; + /** + * The type of the message to be sent or downloaded. + * This field is only valid for MM. For SM, its value is always + * set to 0. + */ + public static final String MSG_TYPE = "msg_type"; + /** + * The type of the error code. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_TYPE = "err_type"; + /** + * The error code of sending/retrieving process. + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "err_code"; + /** + * How many times we tried to send or download the message. + * <P>Type: INTEGER</P> + */ + public static final String RETRY_INDEX = "retry_index"; + /** + * The time to do next retry. + */ + public static final String DUE_TIME = "due_time"; + /** + * The time we last tried to send or download the message. + */ + public static final String LAST_TRY = "last_try"; + } + + public static final class WordsTable { + public static final String ID = "_id"; + public static final String SOURCE_ROW_ID = "source_id"; + public static final String TABLE_ID = "table_to_use"; + public static final String INDEXED_TEXT = "index_text"; + } + } + + public static final class Carriers implements BaseColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://telephony/carriers"); + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "name ASC"; + + public static final String NAME = "name"; + + public static final String APN = "apn"; + + public static final String PROXY = "proxy"; + + public static final String PORT = "port"; + + public static final String MMSPROXY = "mmsproxy"; + + public static final String MMSPORT = "mmsport"; + + public static final String SERVER = "server"; + + public static final String USER = "user"; + + public static final String PASSWORD = "password"; + + public static final String MMSC = "mmsc"; + + public static final String MCC = "mcc"; + + public static final String MNC = "mnc"; + + public static final String NUMERIC = "numeric"; + + public static final String AUTH_TYPE = "authtype"; + + public static final String TYPE = "type"; + + public static final String INACTIVE_TIMER = "inactivetimer"; + + // Only if enabled try Data Connection. + public static final String ENABLED = "enabled"; + + // Rules apply based on class. + public static final String CLASS = "class"; + + /** + * The protocol to be used to connect to this APN. + * + * One of the PDP_type values in TS 27.007 section 10.1.1. + * For example, "IP", "IPV6", "IPV4V6", or "PPP". + */ + public static final String PROTOCOL = "protocol"; + + /** + * The protocol to be used to connect to this APN when roaming. + * + * The syntax is the same as protocol. + */ + public static final String ROAMING_PROTOCOL = "roaming_protocol"; + + public static final String CURRENT = "current"; + + /** + * Current status of APN + * true : enabled APN, false : disabled APN. + */ + public static final String CARRIER_ENABLED = "carrier_enabled"; + + /** + * Radio Access Technology info + * To check what values can hold, refer to ServiceState.java. + * This should be spread to other technologies, + * but currently only used for LTE(14) and EHRPD(13). + */ + public static final String BEARER = "bearer"; + } + + /** + * Contains received SMS cell broadcast messages. + */ + public static final class CellBroadcasts implements BaseColumns { + + /** Not instantiable. */ + private CellBroadcasts() {} + + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + Uri.parse("content://cellbroadcasts"); + + /** + * Message geographical scope. + * <P>Type: INTEGER</P> + */ + public static final String GEOGRAPHICAL_SCOPE = "geo_scope"; + + /** + * Message serial number. + * <P>Type: INTEGER</P> + */ + public static final String SERIAL_NUMBER = "serial_number"; + + /** + * PLMN of broadcast sender. (SERIAL_NUMBER + PLMN + LAC + CID) uniquely identifies a + * broadcast for duplicate detection purposes. + * <P>Type: TEXT</P> + */ + public static final String PLMN = "plmn"; + + /** + * Location Area (GSM) or Service Area (UMTS) of broadcast sender. Unused for CDMA. + * Only included if Geographical Scope of message is not PLMN wide (01). + * <P>Type: INTEGER</P> + */ + public static final String LAC = "lac"; + + /** + * Cell ID of message sender (GSM/UMTS). Unused for CDMA. Only included when the + * Geographical Scope of message is cell wide (00 or 11). + * <P>Type: INTEGER</P> + */ + public static final String CID = "cid"; + + /** + * Message code (OBSOLETE: merged into SERIAL_NUMBER). + * <P>Type: INTEGER</P> + */ + public static final String V1_MESSAGE_CODE = "message_code"; + + /** + * Message identifier (OBSOLETE: renamed to SERVICE_CATEGORY). + * <P>Type: INTEGER</P> + */ + public static final String V1_MESSAGE_IDENTIFIER = "message_id"; + + /** + * Service category (GSM/UMTS message identifier, CDMA service category). + * <P>Type: INTEGER</P> + */ + public static final String SERVICE_CATEGORY = "service_category"; + + /** + * Message language code. + * <P>Type: TEXT</P> + */ + public static final String LANGUAGE_CODE = "language"; + + /** + * Message body. + * <P>Type: TEXT</P> + */ + public static final String MESSAGE_BODY = "body"; + + /** + * Message delivery time. + * <P>Type: INTEGER (long)</P> + */ + public static final String DELIVERY_TIME = "date"; + + /** + * Has the message been viewed? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String MESSAGE_READ = "read"; + + /** + * Message format (3GPP or 3GPP2). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_FORMAT = "format"; + + /** + * Message priority (including emergency). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_PRIORITY = "priority"; + + /** + * ETWS warning type (ETWS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String ETWS_WARNING_TYPE = "etws_warning_type"; + + /** + * CMAS message class (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_MESSAGE_CLASS = "cmas_message_class"; + + /** + * CMAS category (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CATEGORY = "cmas_category"; + + /** + * CMAS response type (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_RESPONSE_TYPE = "cmas_response_type"; + + /** + * CMAS severity (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_SEVERITY = "cmas_severity"; + + /** + * CMAS urgency (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_URGENCY = "cmas_urgency"; + + /** + * CMAS certainty (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CERTAINTY = "cmas_certainty"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = DELIVERY_TIME + " DESC"; + + /** + * Query columns for instantiating {@link android.telephony.CellBroadcastMessage} objects. + */ + public static final String[] QUERY_COLUMNS = { + _ID, + GEOGRAPHICAL_SCOPE, + PLMN, + LAC, + CID, + SERIAL_NUMBER, + SERVICE_CATEGORY, + LANGUAGE_CODE, + MESSAGE_BODY, + DELIVERY_TIME, + MESSAGE_READ, + MESSAGE_FORMAT, + MESSAGE_PRIORITY, + ETWS_WARNING_TYPE, + CMAS_MESSAGE_CLASS, + CMAS_CATEGORY, + CMAS_RESPONSE_TYPE, + CMAS_SEVERITY, + CMAS_URGENCY, + CMAS_CERTAINTY + }; + } +} diff --git a/src/java/android/telephony/CellBroadcastMessage.java b/src/java/android/telephony/CellBroadcastMessage.java new file mode 100644 index 0000000..36c238d --- /dev/null +++ b/src/java/android/telephony/CellBroadcastMessage.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2011 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.telephony; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Typeface; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.Telephony; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.format.DateUtils; +import android.text.style.StyleSpan; + +/** + * Application wrapper for {@link SmsCbMessage}. This is Parcelable so that + * decoded broadcast message objects can be passed between running Services. + * New broadcasts are received by the CellBroadcastReceiver app, which exports + * the database of previously received broadcasts at "content://cellbroadcasts/". + * The "android.permission.READ_CELL_BROADCASTS" permission is required to read + * from the ContentProvider, and writes to the database are not allowed.<p> + * + * Use {@link #createFromCursor} to create CellBroadcastMessage objects from rows + * in the database cursor returned by the ContentProvider. + * + * {@hide} + */ +public class CellBroadcastMessage implements Parcelable { + + /** Identifier for getExtra() when adding this object to an Intent. */ + public static final String SMS_CB_MESSAGE_EXTRA = + "com.android.cellbroadcastreceiver.SMS_CB_MESSAGE"; + + /** SmsCbMessage. */ + private final SmsCbMessage mSmsCbMessage; + + private final long mDeliveryTime; + private boolean mIsRead; + + public CellBroadcastMessage(SmsCbMessage message) { + mSmsCbMessage = message; + mDeliveryTime = System.currentTimeMillis(); + mIsRead = false; + } + + private CellBroadcastMessage(SmsCbMessage message, long deliveryTime, boolean isRead) { + mSmsCbMessage = message; + mDeliveryTime = deliveryTime; + mIsRead = isRead; + } + + private CellBroadcastMessage(Parcel in) { + mSmsCbMessage = new SmsCbMessage(in); + mDeliveryTime = in.readLong(); + mIsRead = (in.readInt() != 0); + } + + /** Parcelable: no special flags. */ + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + mSmsCbMessage.writeToParcel(out, flags); + out.writeLong(mDeliveryTime); + out.writeInt(mIsRead ? 1 : 0); + } + + public static final Parcelable.Creator<CellBroadcastMessage> CREATOR + = new Parcelable.Creator<CellBroadcastMessage>() { + public CellBroadcastMessage createFromParcel(Parcel in) { + return new CellBroadcastMessage(in); + } + + public CellBroadcastMessage[] newArray(int size) { + return new CellBroadcastMessage[size]; + } + }; + + /** + * Create a CellBroadcastMessage from a row in the database. + * @param cursor an open SQLite cursor pointing to the row to read + * @return the new CellBroadcastMessage + * @throws IllegalArgumentException if one of the required columns is missing + */ + public static CellBroadcastMessage createFromCursor(Cursor cursor) { + int geoScope = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE)); + int serialNum = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.SERIAL_NUMBER)); + int category = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.SERVICE_CATEGORY)); + String language = cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.LANGUAGE_CODE)); + String body = cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_BODY)); + int format = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_FORMAT)); + int priority = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_PRIORITY)); + + String plmn; + int plmnColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.PLMN); + if (plmnColumn != -1 && !cursor.isNull(plmnColumn)) { + plmn = cursor.getString(plmnColumn); + } else { + plmn = null; + } + + int lac; + int lacColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.LAC); + if (lacColumn != -1 && !cursor.isNull(lacColumn)) { + lac = cursor.getInt(lacColumn); + } else { + lac = -1; + } + + int cid; + int cidColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.CID); + if (cidColumn != -1 && !cursor.isNull(cidColumn)) { + cid = cursor.getInt(cidColumn); + } else { + cid = -1; + } + + SmsCbLocation location = new SmsCbLocation(plmn, lac, cid); + + SmsCbEtwsInfo etwsInfo; + int etwsWarningTypeColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.ETWS_WARNING_TYPE); + if (etwsWarningTypeColumn != -1 && !cursor.isNull(etwsWarningTypeColumn)) { + int warningType = cursor.getInt(etwsWarningTypeColumn); + etwsInfo = new SmsCbEtwsInfo(warningType, false, false, null); + } else { + etwsInfo = null; + } + + SmsCbCmasInfo cmasInfo; + int cmasMessageClassColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS); + if (cmasMessageClassColumn != -1 && !cursor.isNull(cmasMessageClassColumn)) { + int messageClass = cursor.getInt(cmasMessageClassColumn); + + int cmasCategory; + int cmasCategoryColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_CATEGORY); + if (cmasCategoryColumn != -1 && !cursor.isNull(cmasCategoryColumn)) { + cmasCategory = cursor.getInt(cmasCategoryColumn); + } else { + cmasCategory = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + } + + int responseType; + int cmasResponseTypeColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE); + if (cmasResponseTypeColumn != -1 && !cursor.isNull(cmasResponseTypeColumn)) { + responseType = cursor.getInt(cmasResponseTypeColumn); + } else { + responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + } + + int severity; + int cmasSeverityColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_SEVERITY); + if (cmasSeverityColumn != -1 && !cursor.isNull(cmasSeverityColumn)) { + severity = cursor.getInt(cmasSeverityColumn); + } else { + severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + } + + int urgency; + int cmasUrgencyColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_URGENCY); + if (cmasUrgencyColumn != -1 && !cursor.isNull(cmasUrgencyColumn)) { + urgency = cursor.getInt(cmasUrgencyColumn); + } else { + urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + } + + int certainty; + int cmasCertaintyColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_CERTAINTY); + if (cmasCertaintyColumn != -1 && !cursor.isNull(cmasCertaintyColumn)) { + certainty = cursor.getInt(cmasCertaintyColumn); + } else { + certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + } + + cmasInfo = new SmsCbCmasInfo(messageClass, cmasCategory, responseType, severity, + urgency, certainty); + } else { + cmasInfo = null; + } + + SmsCbMessage msg = new SmsCbMessage(format, geoScope, serialNum, location, category, + language, body, priority, etwsInfo, cmasInfo); + + long deliveryTime = cursor.getLong(cursor.getColumnIndexOrThrow( + Telephony.CellBroadcasts.DELIVERY_TIME)); + boolean isRead = (cursor.getInt(cursor.getColumnIndexOrThrow( + Telephony.CellBroadcasts.MESSAGE_READ)) != 0); + + return new CellBroadcastMessage(msg, deliveryTime, isRead); + } + + /** + * Return a ContentValues object for insertion into the database. + * @return a new ContentValues object containing this object's data + */ + public ContentValues getContentValues() { + ContentValues cv = new ContentValues(16); + SmsCbMessage msg = mSmsCbMessage; + cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, msg.getGeographicalScope()); + SmsCbLocation location = msg.getLocation(); + if (location.getPlmn() != null) { + cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn()); + } + if (location.getLac() != -1) { + cv.put(Telephony.CellBroadcasts.LAC, location.getLac()); + } + if (location.getCid() != -1) { + cv.put(Telephony.CellBroadcasts.CID, location.getCid()); + } + cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, msg.getSerialNumber()); + cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, msg.getServiceCategory()); + cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, msg.getLanguageCode()); + cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, msg.getMessageBody()); + cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, mDeliveryTime); + cv.put(Telephony.CellBroadcasts.MESSAGE_READ, mIsRead); + cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, msg.getMessageFormat()); + cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, msg.getMessagePriority()); + + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + if (etwsInfo != null) { + cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); + } + + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null) { + cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); + cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory()); + cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); + cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity()); + cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency()); + cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty()); + } + + return cv; + } + + /** + * Set or clear the "read message" flag. + * @param isRead true if the message has been read; false if not + */ + public void setIsRead(boolean isRead) { + mIsRead = isRead; + } + + public String getLanguageCode() { + return mSmsCbMessage.getLanguageCode(); + } + + public int getServiceCategory() { + return mSmsCbMessage.getServiceCategory(); + } + + public long getDeliveryTime() { + return mDeliveryTime; + } + + public String getMessageBody() { + return mSmsCbMessage.getMessageBody(); + } + + public boolean isRead() { + return mIsRead; + } + + public int getSerialNumber() { + return mSmsCbMessage.getSerialNumber(); + } + + public SmsCbCmasInfo getCmasWarningInfo() { + return mSmsCbMessage.getCmasWarningInfo(); + } + + public SmsCbEtwsInfo getEtwsWarningInfo() { + return mSmsCbMessage.getEtwsWarningInfo(); + } + + /** + * Return whether the broadcast is an emergency (PWS) message type. + * This includes lower priority test messages and Amber alerts. + * + * All public alerts show the flashing warning icon in the dialog, + * but only emergency alerts play the alert sound and speak the message. + * + * @return true if the message is PWS type; false otherwise + */ + public boolean isPublicAlertMessage() { + return mSmsCbMessage.isEmergencyMessage(); + } + + /** + * Returns whether the broadcast is an emergency (PWS) message type, + * including test messages, but excluding lower priority Amber alert broadcasts. + * + * @return true if the message is PWS type, excluding Amber alerts + */ + public boolean isEmergencyAlertMessage() { + if (!mSmsCbMessage.isEmergencyMessage()) { + return false; + } + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null && + cmasInfo.getMessageClass() == SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY) { + return false; + } + return true; + } + + /** + * Return whether the broadcast is an ETWS emergency message type. + * @return true if the message is ETWS emergency type; false otherwise + */ + public boolean isEtwsMessage() { + return mSmsCbMessage.isEtwsMessage(); + } + + /** + * Return whether the broadcast is a CMAS emergency message type. + * @return true if the message is CMAS emergency type; false otherwise + */ + public boolean isCmasMessage() { + return mSmsCbMessage.isCmasMessage(); + } + + /** + * Return the CMAS message class. + * @return the CMAS message class, e.g. {@link SmsCbCmasInfo#CMAS_CLASS_SEVERE_THREAT}, or + * {@link SmsCbCmasInfo#CMAS_CLASS_UNKNOWN} if this is not a CMAS alert + */ + public int getCmasMessageClass() { + if (mSmsCbMessage.isCmasMessage()) { + return mSmsCbMessage.getCmasWarningInfo().getMessageClass(); + } else { + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Return whether the broadcast is an ETWS popup alert. + * This method checks the message ID and the message code. + * @return true if the message indicates an ETWS popup alert + */ + public boolean isEtwsPopupAlert() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isPopupAlert(); + } + + /** + * Return whether the broadcast is an ETWS emergency user alert. + * This method checks the message ID and the message code. + * @return true if the message indicates an ETWS emergency user alert + */ + public boolean isEtwsEmergencyUserAlert() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isEmergencyUserAlert(); + } + + /** + * Return whether the broadcast is an ETWS test message. + * @return true if the message is an ETWS test message; false otherwise + */ + public boolean isEtwsTestMessage() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && + etwsInfo.getWarningType() == SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; + } + + /** + * Return the abbreviated date string for the message delivery time. + * @param context the context object + * @return a String to use in the broadcast list UI + */ + public String getDateString(Context context) { + int flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_CAP_AMPM; + return DateUtils.formatDateTime(context, mDeliveryTime, flags); + } + + /** + * Return the date string for the message delivery time, suitable for text-to-speech. + * @param context the context object + * @return a String for populating the list item AccessibilityEvent for TTS + */ + public String getSpokenDateString(Context context) { + int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE; + return DateUtils.formatDateTime(context, mDeliveryTime, flags); + } +} diff --git a/src/java/android/telephony/SmsCbCmasInfo.java b/src/java/android/telephony/SmsCbCmasInfo.java new file mode 100644 index 0000000..7a89d94 --- /dev/null +++ b/src/java/android/telephony/SmsCbCmasInfo.java @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Contains CMAS warning notification Type 1 elements for a {@link SmsCbMessage}. + * Supported values for each element are defined in TIA-1149-0-1 (CMAS over CDMA) and + * 3GPP TS 23.041 (for GSM/UMTS). + * + * {@hide} + */ +public class SmsCbCmasInfo implements Parcelable { + + // CMAS message class (in GSM/UMTS message identifier or CDMA service category). + + /** Presidential-level alert (Korean Public Alert System Class 0 message). */ + public static final int CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT = 0x00; + + /** Extreme threat to life and property (Korean Public Alert System Class 1 message). */ + public static final int CMAS_CLASS_EXTREME_THREAT = 0x01; + + /** Severe threat to life and property (Korean Public Alert System Class 1 message). */ + public static final int CMAS_CLASS_SEVERE_THREAT = 0x02; + + /** Child abduction emergency (AMBER Alert). */ + public static final int CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY = 0x03; + + /** CMAS test message. */ + public static final int CMAS_CLASS_REQUIRED_MONTHLY_TEST = 0x04; + + /** CMAS exercise. */ + public static final int CMAS_CLASS_CMAS_EXERCISE = 0x05; + + /** CMAS category for operator defined use. */ + public static final int CMAS_CLASS_OPERATOR_DEFINED_USE = 0x06; + + /** CMAS category for warning types that are reserved for future extension. */ + public static final int CMAS_CLASS_UNKNOWN = -1; + + // CMAS alert category (in CDMA type 1 elements record). + + /** CMAS alert category: Geophysical including landslide. */ + public static final int CMAS_CATEGORY_GEO = 0x00; + + /** CMAS alert category: Meteorological including flood. */ + public static final int CMAS_CATEGORY_MET = 0x01; + + /** CMAS alert category: General emergency and public safety. */ + public static final int CMAS_CATEGORY_SAFETY = 0x02; + + /** CMAS alert category: Law enforcement, military, homeland/local/private security. */ + public static final int CMAS_CATEGORY_SECURITY = 0x03; + + /** CMAS alert category: Rescue and recovery. */ + public static final int CMAS_CATEGORY_RESCUE = 0x04; + + /** CMAS alert category: Fire suppression and rescue. */ + public static final int CMAS_CATEGORY_FIRE = 0x05; + + /** CMAS alert category: Medical and public health. */ + public static final int CMAS_CATEGORY_HEALTH = 0x06; + + /** CMAS alert category: Pollution and other environmental. */ + public static final int CMAS_CATEGORY_ENV = 0x07; + + /** CMAS alert category: Public and private transportation. */ + public static final int CMAS_CATEGORY_TRANSPORT = 0x08; + + /** CMAS alert category: Utility, telecom, other non-transport infrastructure. */ + public static final int CMAS_CATEGORY_INFRA = 0x09; + + /** CMAS alert category: Chem, bio, radiological, nuclear, high explosive threat or attack. */ + public static final int CMAS_CATEGORY_CBRNE = 0x0a; + + /** CMAS alert category: Other events. */ + public static final int CMAS_CATEGORY_OTHER = 0x0b; + + /** + * CMAS alert category is unknown. The category is only available for CDMA broadcasts + * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown. + */ + public static final int CMAS_CATEGORY_UNKNOWN = -1; + + // CMAS response type (in CDMA type 1 elements record). + + /** CMAS response type: Take shelter in place. */ + public static final int CMAS_RESPONSE_TYPE_SHELTER = 0x00; + + /** CMAS response type: Evacuate (Relocate). */ + public static final int CMAS_RESPONSE_TYPE_EVACUATE = 0x01; + + /** CMAS response type: Make preparations. */ + public static final int CMAS_RESPONSE_TYPE_PREPARE = 0x02; + + /** CMAS response type: Execute a pre-planned activity. */ + public static final int CMAS_RESPONSE_TYPE_EXECUTE = 0x03; + + /** CMAS response type: Attend to information sources. */ + public static final int CMAS_RESPONSE_TYPE_MONITOR = 0x04; + + /** CMAS response type: Avoid hazard. */ + public static final int CMAS_RESPONSE_TYPE_AVOID = 0x05; + + /** CMAS response type: Evaluate the information in this message (not for public warnings). */ + public static final int CMAS_RESPONSE_TYPE_ASSESS = 0x06; + + /** CMAS response type: No action recommended. */ + public static final int CMAS_RESPONSE_TYPE_NONE = 0x07; + + /** + * CMAS response type is unknown. The response type is only available for CDMA broadcasts + * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown. + */ + public static final int CMAS_RESPONSE_TYPE_UNKNOWN = -1; + + // 4-bit CMAS severity (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS severity type: Extraordinary threat to life or property. */ + public static final int CMAS_SEVERITY_EXTREME = 0x0; + + /** CMAS severity type: Significant threat to life or property. */ + public static final int CMAS_SEVERITY_SEVERE = 0x1; + + /** + * CMAS alert severity is unknown. The severity is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_SEVERITY_UNKNOWN = -1; + + // CMAS urgency (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS urgency type: Responsive action should be taken immediately. */ + public static final int CMAS_URGENCY_IMMEDIATE = 0x0; + + /** CMAS urgency type: Responsive action should be taken within the next hour. */ + public static final int CMAS_URGENCY_EXPECTED = 0x1; + + /** + * CMAS alert urgency is unknown. The urgency is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_URGENCY_UNKNOWN = -1; + + // CMAS certainty (in GSM/UMTS message identifier or CDMA type 1 elements record). + + /** CMAS certainty type: Determined to have occurred or to be ongoing. */ + public static final int CMAS_CERTAINTY_OBSERVED = 0x0; + + /** CMAS certainty type: Likely (probability > ~50%). */ + public static final int CMAS_CERTAINTY_LIKELY = 0x1; + + /** + * CMAS alert certainty is unknown. The certainty is available for CDMA warning alerts + * containing a type 1 elements record and for all GSM and UMTS alerts except for the + * Presidential-level alert class (Korean Public Alert System Class 0). + */ + public static final int CMAS_CERTAINTY_UNKNOWN = -1; + + /** CMAS message class. */ + private final int mMessageClass; + + /** CMAS category. */ + private final int mCategory; + + /** CMAS response type. */ + private final int mResponseType; + + /** CMAS severity. */ + private final int mSeverity; + + /** CMAS urgency. */ + private final int mUrgency; + + /** CMAS certainty. */ + private final int mCertainty; + + /** Create a new SmsCbCmasInfo object with the specified values. */ + public SmsCbCmasInfo(int messageClass, int category, int responseType, int severity, + int urgency, int certainty) { + mMessageClass = messageClass; + mCategory = category; + mResponseType = responseType; + mSeverity = severity; + mUrgency = urgency; + mCertainty = certainty; + } + + /** Create a new SmsCbCmasInfo object from a Parcel. */ + SmsCbCmasInfo(Parcel in) { + mMessageClass = in.readInt(); + mCategory = in.readInt(); + mResponseType = in.readInt(); + mSeverity = in.readInt(); + mUrgency = in.readInt(); + mCertainty = in.readInt(); + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mMessageClass); + dest.writeInt(mCategory); + dest.writeInt(mResponseType); + dest.writeInt(mSeverity); + dest.writeInt(mUrgency); + dest.writeInt(mCertainty); + } + + /** + * Returns the CMAS message class, e.g. {@link #CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT}. + * @return one of the {@code CMAS_CLASS} values + */ + public int getMessageClass() { + return mMessageClass; + } + + /** + * Returns the CMAS category, e.g. {@link #CMAS_CATEGORY_GEO}. + * @return one of the {@code CMAS_CATEGORY} values + */ + public int getCategory() { + return mCategory; + } + + /** + * Returns the CMAS response type, e.g. {@link #CMAS_RESPONSE_TYPE_SHELTER}. + * @return one of the {@code CMAS_RESPONSE_TYPE} values + */ + public int getResponseType() { + return mResponseType; + } + + /** + * Returns the CMAS severity, e.g. {@link #CMAS_SEVERITY_EXTREME}. + * @return one of the {@code CMAS_SEVERITY} values + */ + public int getSeverity() { + return mSeverity; + } + + /** + * Returns the CMAS urgency, e.g. {@link #CMAS_URGENCY_IMMEDIATE}. + * @return one of the {@code CMAS_URGENCY} values + */ + public int getUrgency() { + return mUrgency; + } + + /** + * Returns the CMAS certainty, e.g. {@link #CMAS_CERTAINTY_OBSERVED}. + * @return one of the {@code CMAS_CERTAINTY} values + */ + public int getCertainty() { + return mCertainty; + } + + @Override + public String toString() { + return "SmsCbCmasInfo{messageClass=" + mMessageClass + ", category=" + mCategory + + ", responseType=" + mResponseType + ", severity=" + mSeverity + + ", urgency=" + mUrgency + ", certainty=" + mCertainty + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } + + /** Creator for unparcelling objects. */ + public static final Parcelable.Creator<SmsCbCmasInfo> + CREATOR = new Parcelable.Creator<SmsCbCmasInfo>() { + public SmsCbCmasInfo createFromParcel(Parcel in) { + return new SmsCbCmasInfo(in); + } + + public SmsCbCmasInfo[] newArray(int size) { + return new SmsCbCmasInfo[size]; + } + }; +} diff --git a/src/java/android/telephony/SmsCbEtwsInfo.java b/src/java/android/telephony/SmsCbEtwsInfo.java new file mode 100644 index 0000000..0890d52 --- /dev/null +++ b/src/java/android/telephony/SmsCbEtwsInfo.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.format.Time; + +import com.android.internal.telephony.IccUtils; + +import java.util.Arrays; + +/** + * Contains information elements for a GSM or UMTS ETWS warning notification. + * Supported values for each element are defined in 3GPP TS 23.041. + * + * {@hide} + */ +public class SmsCbEtwsInfo implements Parcelable { + + /** ETWS warning type for earthquake. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE = 0x00; + + /** ETWS warning type for tsunami. */ + public static final int ETWS_WARNING_TYPE_TSUNAMI = 0x01; + + /** ETWS warning type for earthquake and tsunami. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI = 0x02; + + /** ETWS warning type for test messages. */ + public static final int ETWS_WARNING_TYPE_TEST_MESSAGE = 0x03; + + /** ETWS warning type for other emergency types. */ + public static final int ETWS_WARNING_TYPE_OTHER_EMERGENCY = 0x04; + + /** Unknown ETWS warning type. */ + public static final int ETWS_WARNING_TYPE_UNKNOWN = -1; + + /** One of the ETWS warning type constants defined in this class. */ + private final int mWarningType; + + /** Whether or not to activate the emergency user alert tone and vibration. */ + private final boolean mEmergencyUserAlert; + + /** Whether or not to activate a popup alert. */ + private final boolean mActivatePopup; + + /** + * 50-byte security information (ETWS primary notification for GSM only). As of Release 10, + * 3GPP TS 23.041 states that the UE shall ignore the ETWS primary notification timestamp + * and digital signature if received. Therefore it is treated as a raw byte array and + * parceled with the broadcast intent if present, but the timestamp is only computed if an + * application asks for the individual components. + */ + private final byte[] mWarningSecurityInformation; + + /** Create a new SmsCbEtwsInfo object with the specified values. */ + public SmsCbEtwsInfo(int warningType, boolean emergencyUserAlert, boolean activatePopup, + byte[] warningSecurityInformation) { + mWarningType = warningType; + mEmergencyUserAlert = emergencyUserAlert; + mActivatePopup = activatePopup; + mWarningSecurityInformation = warningSecurityInformation; + } + + /** Create a new SmsCbEtwsInfo object from a Parcel. */ + SmsCbEtwsInfo(Parcel in) { + mWarningType = in.readInt(); + mEmergencyUserAlert = (in.readInt() != 0); + mActivatePopup = (in.readInt() != 0); + mWarningSecurityInformation = in.createByteArray(); + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mWarningType); + dest.writeInt(mEmergencyUserAlert ? 1 : 0); + dest.writeInt(mActivatePopup ? 1 : 0); + dest.writeByteArray(mWarningSecurityInformation); + } + + /** + * Returns the ETWS warning type. + * @return a warning type such as {@link #ETWS_WARNING_TYPE_EARTHQUAKE} + */ + public int getWarningType() { + return mWarningType; + } + + /** + * Returns the ETWS emergency user alert flag. + * @return true to notify terminal to activate emergency user alert; false otherwise + */ + public boolean isEmergencyUserAlert() { + return mEmergencyUserAlert; + } + + /** + * Returns the ETWS activate popup flag. + * @return true to notify terminal to activate display popup; false otherwise + */ + public boolean isPopupAlert() { + return mActivatePopup; + } + + /** + * Returns the Warning-Security-Information timestamp (GSM primary notifications only). + * As of Release 10, 3GPP TS 23.041 states that the UE shall ignore this value if received. + * @return a UTC timestamp in System.currentTimeMillis() format, or 0 if not present + */ + public long getPrimaryNotificationTimestamp() { + if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 7) { + return 0; + } + + int year = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[0]); + int month = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[1]); + int day = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[2]); + int hour = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[3]); + int minute = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[4]); + int second = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[5]); + + // For the timezone, the most significant bit of the + // least significant nibble is the sign byte + // (meaning the max range of this field is 79 quarter-hours, + // which is more than enough) + + byte tzByte = mWarningSecurityInformation[6]; + + // Mask out sign bit. + int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); + + timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; + + Time time = new Time(Time.TIMEZONE_UTC); + + // We only need to support years above 2000. + time.year = year + 2000; + time.month = month - 1; + time.monthDay = day; + time.hour = hour; + time.minute = minute; + time.second = second; + + // Timezone offset is in quarter hours. + return time.toMillis(true) - (long) (timezoneOffset * 15 * 60 * 1000); + } + + /** + * Returns the digital signature (GSM primary notifications only). As of Release 10, + * 3GPP TS 23.041 states that the UE shall ignore this value if received. + * @return a byte array containing a copy of the primary notification digital signature + */ + public byte[] getPrimaryNotificationSignature() { + if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 50) { + return null; + } + return Arrays.copyOfRange(mWarningSecurityInformation, 7, 50); + } + + @Override + public String toString() { + return "SmsCbEtwsInfo{warningType=" + mWarningType + ", emergencyUserAlert=" + + mEmergencyUserAlert + ", activatePopup=" + mActivatePopup + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } + + /** Creator for unparcelling objects. */ + public static final Creator<SmsCbEtwsInfo> CREATOR = new Creator<SmsCbEtwsInfo>() { + public SmsCbEtwsInfo createFromParcel(Parcel in) { + return new SmsCbEtwsInfo(in); + } + + public SmsCbEtwsInfo[] newArray(int size) { + return new SmsCbEtwsInfo[size]; + } + }; +} diff --git a/src/java/android/telephony/SmsCbLocation.java b/src/java/android/telephony/SmsCbLocation.java new file mode 100644 index 0000000..7b5bd0d --- /dev/null +++ b/src/java/android/telephony/SmsCbLocation.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2012 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.telephony; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.gsm.GsmCellLocation; + +/** + * Represents the location and geographical scope of a cell broadcast message. + * For GSM/UMTS, the Location Area and Cell ID are set when the broadcast + * geographical scope is cell wide or Location Area wide. For CDMA, the + * broadcast geographical scope is always PLMN wide. + * + * @hide + */ +public class SmsCbLocation implements Parcelable { + + /** The PLMN. Note that this field may be an empty string, but isn't allowed to be null. */ + private final String mPlmn; + + private final int mLac; + private final int mCid; + + /** + * Construct an empty location object. This is used for some test cases, and for + * cell broadcasts saved in older versions of the database without location info. + */ + public SmsCbLocation() { + mPlmn = ""; + mLac = -1; + mCid = -1; + } + + /** + * Construct a location object for the PLMN. This class is immutable, so + * the same object can be reused for multiple broadcasts. + */ + public SmsCbLocation(String plmn) { + mPlmn = plmn; + mLac = -1; + mCid = -1; + } + + /** + * Construct a location object for the PLMN, LAC, and Cell ID. This class is immutable, so + * the same object can be reused for multiple broadcasts. + */ + public SmsCbLocation(String plmn, int lac, int cid) { + mPlmn = plmn; + mLac = lac; + mCid = cid; + } + + /** + * Initialize the object from a Parcel. + */ + public SmsCbLocation(Parcel in) { + mPlmn = in.readString(); + mLac = in.readInt(); + mCid = in.readInt(); + } + + /** + * Returns the MCC/MNC of the network as a String. + * @return the PLMN identifier (MCC+MNC) as a String + */ + public String getPlmn() { + return mPlmn; + } + + /** + * Returns the GSM location area code, or UMTS service area code. + * @return location area code, -1 if unknown, 0xffff max legal value + */ + public int getLac() { + return mLac; + } + + /** + * Returns the GSM or UMTS cell ID. + * @return gsm cell id, -1 if unknown, 0xffff max legal value + */ + public int getCid() { + return mCid; + } + + @Override + public int hashCode() { + int hash = mPlmn.hashCode(); + hash = hash * 31 + mLac; + hash = hash * 31 + mCid; + return hash; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || !(o instanceof SmsCbLocation)) { + return false; + } + SmsCbLocation other = (SmsCbLocation) o; + return mPlmn.equals(other.mPlmn) && mLac == other.mLac && mCid == other.mCid; + } + + @Override + public String toString() { + return '[' + mPlmn + ',' + mLac + ',' + mCid + ']'; + } + + /** + * Test whether this location is within the location area of the specified object. + * + * @param area the location area to compare with this location + * @return true if this location is contained within the specified location area + */ + public boolean isInLocationArea(SmsCbLocation area) { + if (mCid != -1 && mCid != area.mCid) { + return false; + } + if (mLac != -1 && mLac != area.mLac) { + return false; + } + return mPlmn.equals(area.mPlmn); + } + + /** + * Test whether this location is within the location area of the CellLocation. + * + * @param plmn the PLMN to use for comparison + * @param lac the Location Area (GSM) or Service Area (UMTS) to compare with + * @param cid the Cell ID to compare with + * @return true if this location is contained within the specified PLMN, LAC, and Cell ID + */ + public boolean isInLocationArea(String plmn, int lac, int cid) { + if (!mPlmn.equals(plmn)) { + return false; + } + + if (mLac != -1 && mLac != lac) { + return false; + } + + if (mCid != -1 && mCid != cid) { + return false; + } + + return true; + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPlmn); + dest.writeInt(mLac); + dest.writeInt(mCid); + } + + public static final Parcelable.Creator<SmsCbLocation> CREATOR + = new Parcelable.Creator<SmsCbLocation>() { + @Override + public SmsCbLocation createFromParcel(Parcel in) { + return new SmsCbLocation(in); + } + + @Override + public SmsCbLocation[] newArray(int size) { + return new SmsCbLocation[size]; + } + }; + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/src/java/android/telephony/SmsCbMessage.java b/src/java/android/telephony/SmsCbMessage.java new file mode 100644 index 0000000..046bf8c --- /dev/null +++ b/src/java/android/telephony/SmsCbMessage.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2010 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.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parcelable object containing a received cell broadcast message. There are four different types + * of Cell Broadcast messages: + * + * <ul> + * <li>opt-in informational broadcasts, e.g. news, weather, stock quotes, sports scores</li> + * <li>cell information messages, broadcast on channel 50, indicating the current cell name for + * roaming purposes (required to display on the idle screen in Brazil)</li> + * <li>emergency broadcasts for the Japanese Earthquake and Tsunami Warning System (ETWS)</li> + * <li>emergency broadcasts for the American Commercial Mobile Alert Service (CMAS)</li> + * </ul> + * + * <p>There are also four different CB message formats: GSM, ETWS Primary Notification (GSM only), + * UMTS, and CDMA. Some fields are only applicable for some message formats. Other fields were + * unified under a common name, avoiding some names, such as "Message Identifier", that refer to + * two completely different concepts in 3GPP and CDMA. + * + * <p>The GSM/UMTS Message Identifier field is available via {@link #getServiceCategory}, the name + * of the equivalent field in CDMA. In both cases the service category is a 16-bit value, but 3GPP + * and 3GPP2 have completely different meanings for the respective values. For ETWS and CMAS, the + * application should + * + * <p>The CDMA Message Identifier field is available via {@link #getSerialNumber}, which is used + * to detect the receipt of a duplicate message to be discarded. In CDMA, the message ID is + * unique to the current PLMN. In GSM/UMTS, there is a 16-bit serial number containing a 2-bit + * Geographical Scope field which indicates whether the 10-bit message code and 4-bit update number + * are considered unique to the PLMN, to the current cell, or to the current Location Area (or + * Service Area in UMTS). The relevant values are concatenated into a single String which will be + * unique if the messages are not duplicates. + * + * <p>The SMS dispatcher does not detect duplicate messages. However, it does concatenate the + * pages of a GSM multi-page cell broadcast into a single SmsCbMessage object. + * + * <p>Interested applications with {@code RECEIVE_SMS_PERMISSION} can register to receive + * {@code SMS_CB_RECEIVED_ACTION} broadcast intents for incoming non-emergency broadcasts. + * Only system applications such as the CellBroadcastReceiver may receive notifications for + * emergency broadcasts (ETWS and CMAS). This is intended to prevent any potential for delays or + * interference with the immediate display of the alert message and playing of the alert sound and + * vibration pattern, which could be caused by poorly written or malicious non-system code. + * + * @hide + */ +public class SmsCbMessage implements Parcelable { + + protected static final String LOG_TAG = "SMSCB"; + + /** Cell wide geographical scope with immediate display (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0; + + /** PLMN wide geographical scope (GSM/UMTS and all CDMA broadcasts). */ + public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1; + + /** Location / service area wide geographical scope (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2; + + /** Cell wide geographical scope (GSM/UMTS only). */ + public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3; + + /** GSM or UMTS format cell broadcast. */ + public static final int MESSAGE_FORMAT_3GPP = 1; + + /** CDMA format cell broadcast. */ + public static final int MESSAGE_FORMAT_3GPP2 = 2; + + /** Normal message priority. */ + public static final int MESSAGE_PRIORITY_NORMAL = 0; + + /** Interactive message priority. */ + public static final int MESSAGE_PRIORITY_INTERACTIVE = 1; + + /** Urgent message priority. */ + public static final int MESSAGE_PRIORITY_URGENT = 2; + + /** Emergency message priority. */ + public static final int MESSAGE_PRIORITY_EMERGENCY = 3; + + /** Format of this message (for interpretation of service category values). */ + private final int mMessageFormat; + + /** Geographical scope of broadcast. */ + private final int mGeographicalScope; + + /** + * Serial number of broadcast (message identifier for CDMA, geographical scope + message code + + * update number for GSM/UMTS). The serial number plus the location code uniquely identify + * a cell broadcast for duplicate detection. + */ + private final int mSerialNumber; + + /** + * Location identifier for this message. It consists of the current operator MCC/MNC as a + * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the + * message is not binary 01, the Location Area is included for comparison. If the GS is + * 00 or 11, the Cell ID is also included. LAC and Cell ID are -1 if not specified. + */ + private final SmsCbLocation mLocation; + + /** + * 16-bit CDMA service category or GSM/UMTS message identifier. For ETWS and CMAS warnings, + * the information provided by the category is also available via {@link #getEtwsWarningInfo()} + * or {@link #getCmasWarningInfo()}. + */ + private final int mServiceCategory; + + /** Message language, as a two-character string, e.g. "en". */ + private final String mLanguage; + + /** Message body, as a String. */ + private final String mBody; + + /** Message priority (including emergency priority). */ + private final int mPriority; + + /** ETWS warning notification information (ETWS warnings only). */ + private final SmsCbEtwsInfo mEtwsWarningInfo; + + /** CMAS warning notification information (CMAS warnings only). */ + private final SmsCbCmasInfo mCmasWarningInfo; + + /** + * Create a new SmsCbMessage with the specified data. + */ + public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber, + SmsCbLocation location, int serviceCategory, String language, String body, + int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo) { + mMessageFormat = messageFormat; + mGeographicalScope = geographicalScope; + mSerialNumber = serialNumber; + mLocation = location; + mServiceCategory = serviceCategory; + mLanguage = language; + mBody = body; + mPriority = priority; + mEtwsWarningInfo = etwsWarningInfo; + mCmasWarningInfo = cmasWarningInfo; + } + + /** Create a new SmsCbMessage object from a Parcel. */ + public SmsCbMessage(Parcel in) { + mMessageFormat = in.readInt(); + mGeographicalScope = in.readInt(); + mSerialNumber = in.readInt(); + mLocation = new SmsCbLocation(in); + mServiceCategory = in.readInt(); + mLanguage = in.readString(); + mBody = in.readString(); + mPriority = in.readInt(); + int type = in.readInt(); + switch (type) { + case 'E': + // unparcel ETWS warning information + mEtwsWarningInfo = new SmsCbEtwsInfo(in); + mCmasWarningInfo = null; + break; + + case 'C': + // unparcel CMAS warning information + mEtwsWarningInfo = null; + mCmasWarningInfo = new SmsCbCmasInfo(in); + break; + + default: + mEtwsWarningInfo = null; + mCmasWarningInfo = null; + } + } + + /** + * Flatten this object into a Parcel. + * + * @param dest The Parcel in which the object should be written. + * @param flags Additional flags about how the object should be written (ignored). + */ + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mMessageFormat); + dest.writeInt(mGeographicalScope); + dest.writeInt(mSerialNumber); + mLocation.writeToParcel(dest, flags); + dest.writeInt(mServiceCategory); + dest.writeString(mLanguage); + dest.writeString(mBody); + dest.writeInt(mPriority); + if (mEtwsWarningInfo != null) { + // parcel ETWS warning information + dest.writeInt('E'); + mEtwsWarningInfo.writeToParcel(dest, flags); + } else if (mCmasWarningInfo != null) { + // parcel CMAS warning information + dest.writeInt('C'); + mCmasWarningInfo.writeToParcel(dest, flags); + } else { + // no ETWS or CMAS warning information + dest.writeInt('0'); + } + } + + public static final Parcelable.Creator<SmsCbMessage> CREATOR + = new Parcelable.Creator<SmsCbMessage>() { + @Override + public SmsCbMessage createFromParcel(Parcel in) { + return new SmsCbMessage(in); + } + + @Override + public SmsCbMessage[] newArray(int size) { + return new SmsCbMessage[size]; + } + }; + + /** + * Return the geographical scope of this message (GSM/UMTS only). + * + * @return Geographical scope + */ + public int getGeographicalScope() { + return mGeographicalScope; + } + + /** + * Return the broadcast serial number of broadcast (message identifier for CDMA, or + * geographical scope + message code + update number for GSM/UMTS). The serial number plus + * the location code uniquely identify a cell broadcast for duplicate detection. + * + * @return the 16-bit CDMA message identifier or GSM/UMTS serial number + */ + public int getSerialNumber() { + return mSerialNumber; + } + + /** + * Return the location identifier for this message, consisting of the MCC/MNC as a + * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the + * message is not binary 01, the Location Area is included. If the GS is 00 or 11, the + * cell ID is also included. The {@link SmsCbLocation} object includes a method to test + * if the location is included within another location area or within a PLMN and CellLocation. + * + * @return the geographical location code for duplicate message detection + */ + public SmsCbLocation getLocation() { + return mLocation; + } + + /** + * Return the 16-bit CDMA service category or GSM/UMTS message identifier. The interpretation + * of the category is radio technology specific. For ETWS and CMAS warnings, the information + * provided by the category is available via {@link #getEtwsWarningInfo()} or + * {@link #getCmasWarningInfo()} in a radio technology independent format. + * + * @return the radio technology specific service category + */ + public int getServiceCategory() { + return mServiceCategory; + } + + /** + * Get the ISO-639-1 language code for this message, or null if unspecified + * + * @return Language code + */ + public String getLanguageCode() { + return mLanguage; + } + + /** + * Get the body of this message, or null if no body available + * + * @return Body, or null + */ + public String getMessageBody() { + return mBody; + } + + /** + * Get the message format ({@link #MESSAGE_FORMAT_3GPP} or {@link #MESSAGE_FORMAT_3GPP2}). + * @return an integer representing 3GPP or 3GPP2 message format + */ + public int getMessageFormat() { + return mMessageFormat; + } + + /** + * Get the message priority. Normal broadcasts return {@link #MESSAGE_PRIORITY_NORMAL} + * and emergency broadcasts return {@link #MESSAGE_PRIORITY_EMERGENCY}. CDMA also may return + * {@link #MESSAGE_PRIORITY_INTERACTIVE} or {@link #MESSAGE_PRIORITY_URGENT}. + * @return an integer representing the message priority + */ + public int getMessagePriority() { + return mPriority; + } + + /** + * If this is an ETWS warning notification then this method will return an object containing + * the ETWS warning type, the emergency user alert flag, and the popup flag. If this is an + * ETWS primary notification (GSM only), there will also be a 7-byte timestamp and 43-byte + * digital signature. As of Release 10, 3GPP TS 23.041 states that the UE shall ignore the + * ETWS primary notification timestamp and digital signature if received. + * + * @return an SmsCbEtwsInfo object, or null if this is not an ETWS warning notification + */ + public SmsCbEtwsInfo getEtwsWarningInfo() { + return mEtwsWarningInfo; + } + + /** + * If this is a CMAS warning notification then this method will return an object containing + * the CMAS message class, category, response type, severity, urgency and certainty. + * The message class is always present. Severity, urgency and certainty are present for CDMA + * warning notifications containing a type 1 elements record and for GSM and UMTS warnings + * except for the Presidential-level alert category. Category and response type are only + * available for CDMA notifications containing a type 1 elements record. + * + * @return an SmsCbCmasInfo object, or null if this is not a CMAS warning notification + */ + public SmsCbCmasInfo getCmasWarningInfo() { + return mCmasWarningInfo; + } + + /** + * Return whether this message is an emergency (PWS) message type. + * @return true if the message is a public warning notification; false otherwise + */ + public boolean isEmergencyMessage() { + return mPriority == MESSAGE_PRIORITY_EMERGENCY; + } + + /** + * Return whether this message is an ETWS warning alert. + * @return true if the message is an ETWS warning notification; false otherwise + */ + public boolean isEtwsMessage() { + return mEtwsWarningInfo != null; + } + + /** + * Return whether this message is a CMAS warning alert. + * @return true if the message is a CMAS warning notification; false otherwise + */ + public boolean isCmasMessage() { + return mCmasWarningInfo != null; + } + + @Override + public String toString() { + return "SmsCbMessage{geographicalScope=" + mGeographicalScope + ", serialNumber=" + + mSerialNumber + ", location=" + mLocation + ", serviceCategory=" + + mServiceCategory + ", language=" + mLanguage + ", body=" + mBody + + ", priority=" + mPriority + + (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "") + + (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "") + '}'; + } + + /** + * Describe the kinds of special objects contained in the marshalled representation. + * @return a bitmask indicating this Parcelable contains no special objects + */ + @Override + public int describeContents() { + return 0; + } +} diff --git a/src/java/android/telephony/SmsManager.java b/src/java/android/telephony/SmsManager.java new file mode 100644 index 0000000..44bdaeb --- /dev/null +++ b/src/java/android/telephony/SmsManager.java @@ -0,0 +1,522 @@ +/* + * Copyright (C) 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.telephony; + +import android.app.PendingIntent; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; + +import com.android.internal.telephony.ISms; +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.SmsRawData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/* + * TODO(code review): Curious question... Why are a lot of these + * methods not declared as static, since they do not seem to require + * any local object state? Presumably this cannot be changed without + * interfering with the API... + */ + +/** + * Manages SMS operations such as sending data, text, and pdu SMS messages. + * Get this object by calling the static method SmsManager.getDefault(). + */ +public final class SmsManager { + /** Singleton object constructed during class initialization. */ + private static final SmsManager sInstance = new SmsManager(); + + /** + * Send a text based SMS. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or text are empty + */ + public void sendTextMessage( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (TextUtils.isEmpty(text)) { + throw new IllegalArgumentException("Invalid message body"); + } + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + iccISms.sendText(destinationAddress, scAddress, text, sentIntent, deliveryIntent); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Divide a message text into several fragments, none bigger than + * the maximum SMS message size. + * + * @param text the original message. Must not be null. + * @return an <code>ArrayList</code> of strings that, in order, + * comprise the original message + */ + public ArrayList<String> divideMessage(String text) { + return SmsMessage.fragmentText(text); + } + + /** + * Send a multi-part text based SMS. The callee should have already + * divided the message into correctly sized parts by calling + * <code>divideMessage</code>. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> each sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + */ + public void sendMultipartTextMessage( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + if (parts == null || parts.size() < 1) { + throw new IllegalArgumentException("Invalid message body"); + } + + if (parts.size() > 1) { + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + iccISms.sendMultipartText(destinationAddress, scAddress, parts, + sentIntents, deliveryIntents); + } + } catch (RemoteException ex) { + // ignore it + } + } else { + PendingIntent sentIntent = null; + PendingIntent deliveryIntent = null; + if (sentIntents != null && sentIntents.size() > 0) { + sentIntent = sentIntents.get(0); + } + if (deliveryIntents != null && deliveryIntents.size() > 0) { + deliveryIntent = deliveryIntents.get(0); + } + sendTextMessage(destinationAddress, scAddress, parts.get(0), + sentIntent, deliveryIntent); + } + } + + /** + * Send a data based SMS to a specific application port. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param destinationPort the port to deliver the message to + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK</code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + */ + public void sendDataMessage( + String destinationAddress, String scAddress, short destinationPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + if (TextUtils.isEmpty(destinationAddress)) { + throw new IllegalArgumentException("Invalid destinationAddress"); + } + + if (data == null || data.length == 0) { + throw new IllegalArgumentException("Invalid message data"); + } + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + iccISms.sendData(destinationAddress, scAddress, destinationPort & 0xFFFF, + data, sentIntent, deliveryIntent); + } + } catch (RemoteException ex) { + // ignore it + } + } + + /** + * Get the default instance of the SmsManager + * + * @return the default instance of the SmsManager + */ + public static SmsManager getDefault() { + return sInstance; + } + + private SmsManager() { + //nothing + } + + /** + * Copy a raw SMS PDU to the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param smsc the SMSC for this message, or NULL for the default SMSC + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD, + * STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT) + * @return true for success + * + * {@hide} + */ + public boolean copyMessageToIcc(byte[] smsc, byte[] pdu, int status) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.copyMessageToIccEf(status, pdu, smsc); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Delete the specified message from the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param messageIndex is the record index of the message on ICC + * @return true for success + * + * {@hide} + */ + public boolean + deleteMessageFromIcc(int messageIndex) { + boolean success = false; + byte[] pdu = new byte[IccConstants.SMS_RECORD_LENGTH-1]; + Arrays.fill(pdu, (byte)0xff); + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.updateMessageOnIccEf(messageIndex, STATUS_ON_ICC_FREE, pdu); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Update the specified message on the ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @param messageIndex record index of message to update + * @param newStatus new message status (STATUS_ON_ICC_READ, + * STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT, + * STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE) + * @param pdu the raw PDU to store + * @return true for success + * + * {@hide} + */ + public boolean updateMessageOnIcc(int messageIndex, int newStatus, byte[] pdu) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.updateMessageOnIccEf(messageIndex, newStatus, pdu); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Retrieves all messages currently stored on ICC. + * ICC (Integrated Circuit Card) is the card of the device. + * For example, this can be the SIM or USIM for GSM. + * + * @return <code>ArrayList</code> of <code>SmsMessage</code> objects + * + * {@hide} + */ + public static ArrayList<SmsMessage> getAllMessagesFromIcc() { + List<SmsRawData> records = null; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + records = iccISms.getAllMessagesFromIccEf(); + } + } catch (RemoteException ex) { + // ignore it + } + + return createMessageListFromRawRecords(records); + } + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. All received messages will be broadcast in an + * intent with the action "android.provider.Telephony.SMS_CB_RECEIVED". + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * @see #disableCellBroadcast(int) + * + * {@hide} + */ + public boolean enableCellBroadcast(int messageIdentifier) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.enableCellBroadcast(messageIdentifier); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #enableCellBroadcast(int) + * + * {@hide} + */ + public boolean disableCellBroadcast(int messageIdentifier) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.disableCellBroadcast(messageIdentifier); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. All received messages will be broadcast in an + * intent with the action "android.provider.Telephony.SMS_CB_RECEIVED". + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param startMessageId first message identifier as specified in TS 23.041 + * @param endMessageId last message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * @see #disableCellBroadcastRange(int, int) + * + * {@hide} + */ + public boolean enableCellBroadcastRange(int startMessageId, int endMessageId) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.enableCellBroadcastRange(startMessageId, endMessageId); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. + * Note: This call is blocking, callers may want to avoid calling it from + * the main thread of an application. + * + * @param startMessageId first message identifier as specified in TS 23.041 + * @param endMessageId last message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #enableCellBroadcastRange(int, int) + * + * {@hide} + */ + public boolean disableCellBroadcastRange(int startMessageId, int endMessageId) { + boolean success = false; + + try { + ISms iccISms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + if (iccISms != null) { + success = iccISms.disableCellBroadcastRange(startMessageId, endMessageId); + } + } catch (RemoteException ex) { + // ignore it + } + + return success; + } + + /** + * Create a list of <code>SmsMessage</code>s from a list of RawSmsData + * records returned by <code>getAllMessagesFromIcc()</code> + * + * @param records SMS EF records, returned by + * <code>getAllMessagesFromIcc</code> + * @return <code>ArrayList</code> of <code>SmsMessage</code> objects. + */ + private static ArrayList<SmsMessage> createMessageListFromRawRecords(List<SmsRawData> records) { + ArrayList<SmsMessage> messages = new ArrayList<SmsMessage>(); + if (records != null) { + int count = records.size(); + for (int i = 0; i < count; i++) { + SmsRawData data = records.get(i); + // List contains all records, including "free" records (null) + if (data != null) { + SmsMessage sms = SmsMessage.createFromEfRecord(i+1, data.getBytes()); + if (sms != null) { + messages.add(sms); + } + } + } + } + return messages; + } + + // see SmsMessage.getStatusOnIcc + + /** Free space (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_FREE = 0; + + /** Received and read (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_READ = 1; + + /** Received and unread (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_UNREAD = 3; + + /** Stored and sent (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_SENT = 5; + + /** Stored and unsent (TS 51.011 10.5.3 / 3GPP2 C.S0023 3.4.27). */ + static public final int STATUS_ON_ICC_UNSENT = 7; + + // SMS send failure result codes + + /** Generic failure cause */ + static public final int RESULT_ERROR_GENERIC_FAILURE = 1; + /** Failed because radio was explicitly turned off */ + static public final int RESULT_ERROR_RADIO_OFF = 2; + /** Failed because no pdu provided */ + static public final int RESULT_ERROR_NULL_PDU = 3; + /** Failed because service is currently unavailable */ + static public final int RESULT_ERROR_NO_SERVICE = 4; + /** Failed because we reached the sending queue limit. {@hide} */ + static public final int RESULT_ERROR_LIMIT_EXCEEDED = 5; + /** Failed because FDN is enabled. {@hide} */ + static public final int RESULT_ERROR_FDN_CHECK_FAILURE = 6; +} diff --git a/src/java/android/telephony/SmsMessage.java b/src/java/android/telephony/SmsMessage.java new file mode 100644 index 0000000..b94609e --- /dev/null +++ b/src/java/android/telephony/SmsMessage.java @@ -0,0 +1,688 @@ +/* + * Copyright (C) 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.telephony; + +import android.os.Parcel; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsMessageBase.SubmitPduBase; + +import java.lang.Math; +import java.util.ArrayList; +import java.util.Arrays; + +import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; + + +/** + * A Short Message Service message. + */ +public class SmsMessage { + private static final String LOG_TAG = "SMS"; + + /** + * SMS Class enumeration. + * See TS 23.038. + * + */ + public enum MessageClass{ + UNKNOWN, CLASS_0, CLASS_1, CLASS_2, CLASS_3; + } + + /** User data text encoding code unit size */ + public static final int ENCODING_UNKNOWN = 0; + public static final int ENCODING_7BIT = 1; + public static final int ENCODING_8BIT = 2; + public static final int ENCODING_16BIT = 3; + /** + * @hide This value is not defined in global standard. Only in Korea, this is used. + */ + public static final int ENCODING_KSC5601 = 4; + + /** The maximum number of payload bytes per message */ + public static final int MAX_USER_DATA_BYTES = 140; + + /** + * The maximum number of payload bytes per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + */ + public static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134; + + /** The maximum number of payload septets per message */ + public static final int MAX_USER_DATA_SEPTETS = 160; + + /** + * The maximum number of payload septets per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + */ + public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153; + + /** + * Indicates a 3GPP format SMS message. + * @hide pending API council approval + */ + public static final String FORMAT_3GPP = "3gpp"; + + /** + * Indicates a 3GPP2 format SMS message. + * @hide pending API council approval + */ + public static final String FORMAT_3GPP2 = "3gpp2"; + + /** Contains actual SmsMessage. Only public for debugging and for framework layer. + * + * @hide + */ + public SmsMessageBase mWrappedSmsMessage; + + public static class SubmitPdu { + + public byte[] encodedScAddress; // Null if not applicable. + public byte[] encodedMessage; + + public String toString() { + return "SubmitPdu: encodedScAddress = " + + Arrays.toString(encodedScAddress) + + ", encodedMessage = " + + Arrays.toString(encodedMessage); + } + + /** + * @hide + */ + protected SubmitPdu(SubmitPduBase spb) { + this.encodedMessage = spb.encodedMessage; + this.encodedScAddress = spb.encodedScAddress; + } + + } + + private SmsMessage(SmsMessageBase smb) { + mWrappedSmsMessage = smb; + } + + /** + * Create an SmsMessage from a raw PDU. + * + * <p><b>This method will soon be deprecated</b> and all applications which handle + * incoming SMS messages by processing the {@code SMS_RECEIVED_ACTION} broadcast + * intent <b>must</b> now pass the new {@code format} String extra from the intent + * into the new method {@code createFromPdu(byte[], String)} which takes an + * extra format parameter. This is required in order to correctly decode the PDU on + * devices that require support for both 3GPP and 3GPP2 formats at the same time, + * such as dual-mode GSM/CDMA and CDMA/LTE phones. + */ + public static SmsMessage createFromPdu(byte[] pdu) { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + String format = (PHONE_TYPE_CDMA == activePhone) ? + SmsConstants.FORMAT_3GPP2 : SmsConstants.FORMAT_3GPP; + return createFromPdu(pdu, format); + } + + /** + * Create an SmsMessage from a raw PDU with the specified message format. The + * message format is passed in the {@code SMS_RECEIVED_ACTION} as the {@code format} + * String extra, and will be either "3gpp" for GSM/UMTS/LTE messages in 3GPP format + * or "3gpp2" for CDMA/LTE messages in 3GPP2 format. + * + * @param pdu the message PDU from the SMS_RECEIVED_ACTION intent + * @param format the format extra from the SMS_RECEIVED_ACTION intent + * @hide pending API council approval + */ + public static SmsMessage createFromPdu(byte[] pdu, String format) { + SmsMessageBase wrappedMessage; + + if (SmsConstants.FORMAT_3GPP2.equals(format)) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromPdu(pdu); + } else if (SmsConstants.FORMAT_3GPP.equals(format)) { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromPdu(pdu); + } else { + Log.e(LOG_TAG, "createFromPdu(): unsupported message format " + format); + return null; + } + + return new SmsMessage(wrappedMessage); + } + + /** + * TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the + * +CMT unsolicited response (PDU mode, of course) + * +CMT: [<alpha>],<length><CR><LF><pdu> + * + * Only public for debugging and for RIL + * + * {@hide} + */ + public static SmsMessage newFromCMT(String[] lines) { + // received SMS in 3GPP format + SmsMessageBase wrappedMessage = + com.android.internal.telephony.gsm.SmsMessage.newFromCMT(lines); + + return new SmsMessage(wrappedMessage); + } + + /** @hide */ + public static SmsMessage newFromParcel(Parcel p) { + // received SMS in 3GPP2 format + SmsMessageBase wrappedMessage = + com.android.internal.telephony.cdma.SmsMessage.newFromParcel(p); + + return new SmsMessage(wrappedMessage); + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by SmsManager.getAllMessagesFromSim + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + SmsMessageBase wrappedMessage; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord( + index, data); + } else { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromEfRecord( + index, data); + } + + return wrappedMessage != null ? new SmsMessage(wrappedMessage) : null; + } + + /** + * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the + * length in bytes (not hex chars) less the SMSC header + * + * FIXME: This method is only used by a CTS test case that isn't run on CDMA devices. + * We should probably deprecate it and remove the obsolete test case. + */ + public static int getTPLayerLengthForPDU(String pdu) { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + return com.android.internal.telephony.cdma.SmsMessage.getTPLayerLengthForPDU(pdu); + } else { + return com.android.internal.telephony.gsm.SmsMessage.getTPLayerLengthForPDU(pdu); + } + } + + /* + * TODO(cleanup): It would make some sense if the result of + * preprocessing a message to determine the proper encoding (i.e. + * the resulting data structure from calculateLength) could be + * passed as an argument to the actual final encoding function. + * This would better ensure that the logic behind size calculation + * actually matched the encoding. + */ + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message. + * + * @param msgBody the message to encode + * @param use7bitOnly if true, characters that are not part of the + * radio-specific 7-bit encoding are counted as single + * space chars. If false, and if the messageBody contains + * non-7-bit encodable characters, length is calculated + * using a 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's + * required, int[1] the number of code units used, and + * int[2] is the number of code units remaining until the + * next message. int[3] is an indicator of the encoding + * code unit size (see the ENCODING_* definitions in SmsConstants) + */ + public static int[] calculateLength(CharSequence msgBody, boolean use7bitOnly) { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + TextEncodingDetails ted = (PHONE_TYPE_CDMA == activePhone) ? + com.android.internal.telephony.cdma.SmsMessage.calculateLength(msgBody, use7bitOnly) : + com.android.internal.telephony.gsm.SmsMessage.calculateLength(msgBody, use7bitOnly); + int ret[] = new int[4]; + ret[0] = ted.msgCount; + ret[1] = ted.codeUnitCount; + ret[2] = ted.codeUnitsRemaining; + ret[3] = ted.codeUnitSize; + return ret; + } + + /** + * Divide a message text into several fragments, none bigger than + * the maximum SMS message text size. + * + * @param text text, must not be null. + * @return an <code>ArrayList</code> of strings that, in order, + * comprise the original msg text + * + * @hide + */ + public static ArrayList<String> fragmentText(String text) { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + TextEncodingDetails ted = (PHONE_TYPE_CDMA == activePhone) ? + com.android.internal.telephony.cdma.SmsMessage.calculateLength(text, false) : + com.android.internal.telephony.gsm.SmsMessage.calculateLength(text, false); + + // TODO(cleanup): The code here could be rolled into the logic + // below cleanly if these MAX_* constants were defined more + // flexibly... + + int limit; + if (ted.codeUnitSize == SmsConstants.ENCODING_7BIT) { + int udhLength; + if (ted.languageTable != 0 && ted.languageShiftTable != 0) { + udhLength = GsmAlphabet.UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } else if (ted.languageTable != 0 || ted.languageShiftTable != 0) { + udhLength = GsmAlphabet.UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength = 0; + } + + if (ted.msgCount > 1) { + udhLength += GsmAlphabet.UDH_SEPTET_COST_CONCATENATED_MESSAGE; + } + + if (udhLength != 0) { + udhLength += GsmAlphabet.UDH_SEPTET_COST_LENGTH; + } + + limit = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength; + } else { + if (ted.msgCount > 1) { + limit = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + } else { + limit = SmsConstants.MAX_USER_DATA_BYTES; + } + } + + int pos = 0; // Index in code units. + int textLen = text.length(); + ArrayList<String> result = new ArrayList<String>(ted.msgCount); + while (pos < textLen) { + int nextPos = 0; // Counts code units. + if (ted.codeUnitSize == SmsConstants.ENCODING_7BIT) { + if (activePhone == PHONE_TYPE_CDMA && ted.msgCount == 1) { + // For a singleton CDMA message, the encoding must be ASCII... + nextPos = pos + Math.min(limit, textLen - pos); + } else { + // For multi-segment messages, CDMA 7bit equals GSM 7bit encoding (EMS mode). + nextPos = GsmAlphabet.findGsmSeptetLimitIndex(text, pos, limit, + ted.languageTable, ted.languageShiftTable); + } + } else { // Assume unicode. + nextPos = pos + Math.min(limit / 2, textLen - pos); + } + if ((nextPos <= pos) || (nextPos > textLen)) { + Log.e(LOG_TAG, "fragmentText failed (" + pos + " >= " + nextPos + " or " + + nextPos + " >= " + textLen + ")"); + break; + } + result.add(text.substring(pos, nextPos)); + pos = nextPos; + } + return result; + } + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message, given the + * current encoding. + * + * @param messageBody the message to encode + * @param use7bitOnly if true, characters that are not part of the radio + * specific (GSM / CDMA) alphabet encoding are converted to as a + * single space characters. If false, a messageBody containing + * non-GSM or non-CDMA alphabet characters are encoded using + * 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's required, int[1] + * the number of code units used, and int[2] is the number of code + * units remaining until the next message. int[3] is the encoding + * type that should be used for the message. + */ + public static int[] calculateLength(String messageBody, boolean use7bitOnly) { + return calculateLength((CharSequence)messageBody, use7bitOnly); + } + + /* + * TODO(cleanup): It looks like there is now no useful reason why + * apps should generate pdus themselves using these routines, + * instead of handing the raw data to SMSDispatcher (and thereby + * have the phone process do the encoding). Moreover, CDMA now + * has shared state (in the form of the msgId system property) + * which can only be modified by the phone process, and hence + * makes the output of these routines incorrect. Since they now + * serve no purpose, they should probably just return null + * directly, and be deprecated. Going further in that direction, + * the above parsers of serialized pdu data should probably also + * be gotten rid of, hiding all but the necessarily visible + * structured data from client apps. A possible concern with + * doing this is that apps may be using these routines to generate + * pdus that are then sent elsewhere, some network server, for + * example, and that always returning null would thereby break + * otherwise useful apps. + */ + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message. + * This method will not attempt to use any GSM national language 7 bit encodings. + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, boolean statusReportRequested) { + SubmitPduBase spb; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested, null); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port. + * This method will not attempt to use any GSM national language 7 bit encodings. + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param destinationPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, short destinationPort, byte[] data, + boolean statusReportRequested) { + SubmitPduBase spb; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Returns the address of the SMS service center that relayed this message + * or null if there is none. + */ + public String getServiceCenterAddress() { + return mWrappedSmsMessage.getServiceCenterAddress(); + } + + /** + * Returns the originating address (sender) of this SMS message in String + * form or null if unavailable + */ + public String getOriginatingAddress() { + return mWrappedSmsMessage.getOriginatingAddress(); + } + + /** + * Returns the originating address, or email from address if this message + * was from an email gateway. Returns null if originating address + * unavailable. + */ + public String getDisplayOriginatingAddress() { + return mWrappedSmsMessage.getDisplayOriginatingAddress(); + } + + /** + * Returns the message body as a String, if it exists and is text based. + * @return message body is there is one, otherwise null + */ + public String getMessageBody() { + return mWrappedSmsMessage.getMessageBody(); + } + + /** + * Returns the class of this message. + */ + public MessageClass getMessageClass() { + switch(mWrappedSmsMessage.getMessageClass()) { + case CLASS_0: return MessageClass.CLASS_0; + case CLASS_1: return MessageClass.CLASS_1; + case CLASS_2: return MessageClass.CLASS_2; + case CLASS_3: return MessageClass.CLASS_3; + default: return MessageClass.UNKNOWN; + + } + } + + /** + * Returns the message body, or email message body if this message was from + * an email gateway. Returns null if message body unavailable. + */ + public String getDisplayMessageBody() { + return mWrappedSmsMessage.getDisplayMessageBody(); + } + + /** + * Unofficial convention of a subject line enclosed in parens empty string + * if not present + */ + public String getPseudoSubject() { + return mWrappedSmsMessage.getPseudoSubject(); + } + + /** + * Returns the service centre timestamp in currentTimeMillis() format + */ + public long getTimestampMillis() { + return mWrappedSmsMessage.getTimestampMillis(); + } + + /** + * Returns true if message is an email. + * + * @return true if this message came through an email gateway and email + * sender / subject / parsed body are available + */ + public boolean isEmail() { + return mWrappedSmsMessage.isEmail(); + } + + /** + * @return if isEmail() is true, body of the email sent through the gateway. + * null otherwise + */ + public String getEmailBody() { + return mWrappedSmsMessage.getEmailBody(); + } + + /** + * @return if isEmail() is true, email from address of email sent through + * the gateway. null otherwise + */ + public String getEmailFrom() { + return mWrappedSmsMessage.getEmailFrom(); + } + + /** + * Get protocol identifier. + */ + public int getProtocolIdentifier() { + return mWrappedSmsMessage.getProtocolIdentifier(); + } + + /** + * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" + * SMS + */ + public boolean isReplace() { + return mWrappedSmsMessage.isReplace(); + } + + /** + * Returns true for CPHS MWI toggle message. + * + * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section + * B.4.2 + */ + public boolean isCphsMwiMessage() { + return mWrappedSmsMessage.isCphsMwiMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) clear message + */ + public boolean isMWIClearMessage() { + return mWrappedSmsMessage.isMWIClearMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) set message + */ + public boolean isMWISetMessage() { + return mWrappedSmsMessage.isMWISetMessage(); + } + + /** + * returns true if this message is a "Message Waiting Indication Group: + * Discard Message" notification and should not be stored. + */ + public boolean isMwiDontStore() { + return mWrappedSmsMessage.isMwiDontStore(); + } + + /** + * returns the user data section minus the user data header if one was + * present. + */ + public byte[] getUserData() { + return mWrappedSmsMessage.getUserData(); + } + + /** + * Returns the raw PDU for the message. + * + * @return the raw PDU for the message. + */ + public byte[] getPdu() { + return mWrappedSmsMessage.getPdu(); + } + + /** + * Returns the status of the message on the SIM (read, unread, sent, unsent). + * + * @return the status of the message on the SIM. These are: + * SmsManager.STATUS_ON_SIM_FREE + * SmsManager.STATUS_ON_SIM_READ + * SmsManager.STATUS_ON_SIM_UNREAD + * SmsManager.STATUS_ON_SIM_SEND + * SmsManager.STATUS_ON_SIM_UNSENT + * @deprecated Use getStatusOnIcc instead. + */ + @Deprecated public int getStatusOnSim() { + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the status of the message on the ICC (read, unread, sent, unsent). + * + * @return the status of the message on the ICC. These are: + * SmsManager.STATUS_ON_ICC_FREE + * SmsManager.STATUS_ON_ICC_READ + * SmsManager.STATUS_ON_ICC_UNREAD + * SmsManager.STATUS_ON_ICC_SEND + * SmsManager.STATUS_ON_ICC_UNSENT + */ + public int getStatusOnIcc() { + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the record index of the message on the SIM (1-based index). + * @return the record index of the message on the SIM, or -1 if this + * SmsMessage was not created from a SIM SMS EF record. + * @deprecated Use getIndexOnIcc instead. + */ + @Deprecated public int getIndexOnSim() { + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * Returns the record index of the message on the ICC (1-based index). + * @return the record index of the message on the ICC, or -1 if this + * SmsMessage was not created from a ICC SMS EF record. + */ + public int getIndexOnIcc() { + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * GSM: + * For an SMS-STATUS-REPORT message, this returns the status field from + * the status report. This field indicates the status of a previously + * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a + * description of values. + * CDMA: + * For not interfering with status codes from GSM, the value is + * shifted to the bits 31-16. + * The value is composed of an error class (bits 25-24) and a status code (bits 23-16). + * Possible codes are described in C.S0015-B, v2.0, 4.5.21. + * + * @return 0 indicates the previously sent message was received. + * See TS 23.040, 9.9.2.3.15 and C.S0015-B, v2.0, 4.5.21 + * for a description of other possible values. + */ + public int getStatus() { + return mWrappedSmsMessage.getStatus(); + } + + /** + * Return true iff the message is a SMS-STATUS-REPORT message. + */ + public boolean isStatusReportMessage() { + return mWrappedSmsMessage.isStatusReportMessage(); + } + + /** + * Returns true iff the <code>TP-Reply-Path</code> bit is set in + * this message. + */ + public boolean isReplyPathPresent() { + return mWrappedSmsMessage.isReplyPathPresent(); + } +} diff --git a/src/java/android/telephony/gsm/SmsManager.java b/src/java/android/telephony/gsm/SmsManager.java new file mode 100644 index 0000000..3b75298 --- /dev/null +++ b/src/java/android/telephony/gsm/SmsManager.java @@ -0,0 +1,261 @@ +/* + * 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.telephony.gsm; + +import android.app.PendingIntent; + +import java.util.ArrayList; + + +/** + * Manages SMS operations such as sending data, text, and pdu SMS messages. + * Get this object by calling the static method SmsManager.getDefault(). + * @deprecated Replaced by android.telephony.SmsManager that supports both GSM and CDMA. + */ +@Deprecated public final class SmsManager { + private static SmsManager sInstance; + private android.telephony.SmsManager mSmsMgrProxy; + + /** Get the default instance of the SmsManager + * + * @return the default instance of the SmsManager + * @deprecated Use android.telephony.SmsManager. + */ + @Deprecated + public static final SmsManager getDefault() { + if (sInstance == null) { + sInstance = new SmsManager(); + } + return sInstance; + } + + @Deprecated + private SmsManager() { + mSmsMgrProxy = android.telephony.SmsManager.getDefault(); + } + + /** + * Send a text based SMS. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or text are empty + * @deprecated Use android.telephony.SmsManager. + */ + @Deprecated + public final void sendTextMessage( + String destinationAddress, String scAddress, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + mSmsMgrProxy.sendTextMessage(destinationAddress, scAddress, text, + sentIntent, deliveryIntent); + } + + /** + * Divide a text message into several messages, none bigger than + * the maximum SMS message size. + * + * @param text the original message. Must not be null. + * @return an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @deprecated Use android.telephony.SmsManager. + */ + @Deprecated + public final ArrayList<String> divideMessage(String text) { + return mSmsMgrProxy.divideMessage(text); + } + + /** + * Send a multi-part text based SMS. The callee should have already + * divided the message into correctly sized parts by calling + * <code>divideMessage</code>. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applicaitons, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + * @deprecated Use android.telephony.SmsManager. + */ + @Deprecated + public final void sendMultipartTextMessage( + String destinationAddress, String scAddress, ArrayList<String> parts, + ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { + mSmsMgrProxy.sendMultipartTextMessage(destinationAddress, scAddress, parts, + sentIntents, deliveryIntents); + } + + /** + * Send a data based SMS to a specific application port. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param destinationPort the port to deliver the message to + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is sucessfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applicaitons, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * + * @throws IllegalArgumentException if destinationAddress or data are empty + * @deprecated Use android.telephony.SmsManager. + */ + @Deprecated + public final void sendDataMessage( + String destinationAddress, String scAddress, short destinationPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + mSmsMgrProxy.sendDataMessage(destinationAddress, scAddress, destinationPort, + data, sentIntent, deliveryIntent); + } + + /** + * Copy a raw SMS PDU to the SIM. + * + * @param smsc the SMSC for this message, or NULL for the default SMSC + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_SIM_READ, STATUS_ON_SIM_UNREAD, + * STATUS_ON_SIM_SENT, STATUS_ON_SIM_UNSENT) + * @return true for success + * @deprecated Use android.telephony.SmsManager. + * {@hide} + */ + @Deprecated + public final boolean copyMessageToSim(byte[] smsc, byte[] pdu, int status) { + return mSmsMgrProxy.copyMessageToIcc(smsc, pdu, status); + } + + /** + * Delete the specified message from the SIM. + * + * @param messageIndex is the record index of the message on SIM + * @return true for success + * @deprecated Use android.telephony.SmsManager. + * {@hide} + */ + @Deprecated + public final boolean deleteMessageFromSim(int messageIndex) { + return mSmsMgrProxy.deleteMessageFromIcc(messageIndex); + } + + /** + * Update the specified message on the SIM. + * + * @param messageIndex record index of message to update + * @param newStatus new message status (STATUS_ON_SIM_READ, + * STATUS_ON_SIM_UNREAD, STATUS_ON_SIM_SENT, + * STATUS_ON_SIM_UNSENT, STATUS_ON_SIM_FREE) + * @param pdu the raw PDU to store + * @return true for success + * @deprecated Use android.telephony.SmsManager. + * {@hide} + */ + @Deprecated + public final boolean updateMessageOnSim(int messageIndex, int newStatus, byte[] pdu) { + return mSmsMgrProxy.updateMessageOnIcc(messageIndex, newStatus, pdu); + } + + /** + * Retrieves all messages currently stored on SIM. + * @return <code>ArrayList</code> of <code>SmsMessage</code> objects + * @deprecated Use android.telephony.SmsManager. + * {@hide} + */ + @Deprecated + public final ArrayList<android.telephony.SmsMessage> getAllMessagesFromSim() { + return mSmsMgrProxy.getAllMessagesFromIcc(); + } + + /** Free space (TS 51.011 10.5.3). + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int STATUS_ON_SIM_FREE = 0; + + /** Received and read (TS 51.011 10.5.3). + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int STATUS_ON_SIM_READ = 1; + + /** Received and unread (TS 51.011 10.5.3). + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int STATUS_ON_SIM_UNREAD = 3; + + /** Stored and sent (TS 51.011 10.5.3). + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int STATUS_ON_SIM_SENT = 5; + + /** Stored and unsent (TS 51.011 10.5.3). + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int STATUS_ON_SIM_UNSENT = 7; + + /** Generic failure cause + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int RESULT_ERROR_GENERIC_FAILURE = 1; + + /** Failed because radio was explicitly turned off + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int RESULT_ERROR_RADIO_OFF = 2; + + /** Failed because no pdu provided + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int RESULT_ERROR_NULL_PDU = 3; + + /** Failed because service is currently unavailable + * @deprecated Use android.telephony.SmsManager. */ + @Deprecated static public final int RESULT_ERROR_NO_SERVICE = 4; + +} diff --git a/src/java/android/telephony/gsm/SmsMessage.java b/src/java/android/telephony/gsm/SmsMessage.java new file mode 100644 index 0000000..7a814c3 --- /dev/null +++ b/src/java/android/telephony/gsm/SmsMessage.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 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.telephony.gsm; + +import android.os.Parcel; +import android.telephony.TelephonyManager; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.EncodeException; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsMessageBase.SubmitPduBase; + +import java.util.Arrays; + +import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; + + +/** + * A Short Message Service message. + * @deprecated Replaced by android.telephony.SmsMessage that supports both GSM and CDMA. + */ +@Deprecated +public class SmsMessage { + private static final boolean LOCAL_DEBUG = true; + private static final String LOG_TAG = "SMS"; + + /** + * SMS Class enumeration. + * See TS 23.038. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public enum MessageClass{ + UNKNOWN, CLASS_0, CLASS_1, CLASS_2, CLASS_3; + } + + /** Unknown encoding scheme (see TS 23.038) + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int ENCODING_UNKNOWN = 0; + + /** 7-bit encoding scheme (see TS 23.038) + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int ENCODING_7BIT = 1; + + /** 8-bit encoding scheme (see TS 23.038) + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int ENCODING_8BIT = 2; + + /** 16-bit encoding scheme (see TS 23.038) + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int ENCODING_16BIT = 3; + + /** The maximum number of payload bytes per message + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int MAX_USER_DATA_BYTES = 140; + + /** + * The maximum number of payload bytes per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + * + * @deprecated Use android.telephony.SmsMessage. + * @hide pending API Council approval to extend the public API + */ + @Deprecated public static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134; + + /** The maximum number of payload septets per message + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int MAX_USER_DATA_SEPTETS = 160; + + /** + * The maximum number of payload septets per message if a user data header + * is present. This assumes the header only contains the + * CONCATENATED_8_BIT_REFERENCE element. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153; + + /** Contains actual SmsMessage. Only public for debugging and for framework layer. + * @deprecated Use android.telephony.SmsMessage. + * {@hide} + */ + @Deprecated public SmsMessageBase mWrappedSmsMessage; + + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated + public static class SubmitPdu { + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated public byte[] encodedScAddress; // Null if not applicable. + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated public byte[] encodedMessage; + + //Constructor + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated + public SubmitPdu() { + } + + /** @deprecated Use android.telephony.SmsMessage. + * {@hide} + */ + @Deprecated + protected SubmitPdu(SubmitPduBase spb) { + this.encodedMessage = spb.encodedMessage; + this.encodedScAddress = spb.encodedScAddress; + } + + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated + public String toString() { + return "SubmitPdu: encodedScAddress = " + + Arrays.toString(encodedScAddress) + + ", encodedMessage = " + + Arrays.toString(encodedMessage); + } + } + + // Constructor + /** @deprecated Use android.telephony.SmsMessage. */ + @Deprecated + public SmsMessage() { + this(getSmsFacility()); + } + + private SmsMessage(SmsMessageBase smb) { + mWrappedSmsMessage = smb; + } + + /** + * Create an SmsMessage from a raw PDU. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static SmsMessage createFromPdu(byte[] pdu) { + SmsMessageBase wrappedMessage; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + wrappedMessage = com.android.internal.telephony.cdma.SmsMessage.createFromPdu(pdu); + } else { + wrappedMessage = com.android.internal.telephony.gsm.SmsMessage.createFromPdu(pdu); + } + + return new SmsMessage(wrappedMessage); + } + + /** + * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the + * length in bytes (not hex chars) less the SMSC header + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static int getTPLayerLengthForPDU(String pdu) { + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + return com.android.internal.telephony.cdma.SmsMessage.getTPLayerLengthForPDU(pdu); + } else { + return com.android.internal.telephony.gsm.SmsMessage.getTPLayerLengthForPDU(pdu); + } + } + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message, given the + * current encoding. + * + * @param messageBody the message to encode + * @param use7bitOnly if true, characters that are not part of the GSM + * alphabet are counted as a single space char. If false, a + * messageBody containing non-GSM alphabet characters is calculated + * for 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's required, int[1] + * the number of code units used, and int[2] is the number of code + * units remaining until the next message. int[3] is the encoding + * type that should be used for the message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static int[] calculateLength(CharSequence messageBody, boolean use7bitOnly) { + GsmAlphabet.TextEncodingDetails ted = + com.android.internal.telephony.gsm.SmsMessage + .calculateLength(messageBody, use7bitOnly); + int ret[] = new int[4]; + ret[0] = ted.msgCount; + ret[1] = ted.codeUnitCount; + ret[2] = ted.codeUnitsRemaining; + ret[3] = ted.codeUnitSize; + return ret; + } + + /** + * Calculates the number of SMS's required to encode the message body and + * the number of characters remaining until the next message, given the + * current encoding. + * + * @param messageBody the message to encode + * @param use7bitOnly if true, characters that are not part of the GSM + * alphabet are counted as a single space char. If false, a + * messageBody containing non-GSM alphabet characters is calculated + * for 16-bit encoding. + * @return an int[4] with int[0] being the number of SMS's required, int[1] + * the number of code units used, and int[2] is the number of code + * units remaining until the next message. int[3] is the encoding + * type that should be used for the message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static int[] calculateLength(String messageBody, boolean use7bitOnly) { + return calculateLength((CharSequence)messageBody, use7bitOnly); + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @deprecated Use android.telephony.SmsMessage. + * @hide + */ + @Deprecated + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested, byte[] header) { + SubmitPduBase spb; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested, + SmsHeader.fromByteArray(header)); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested, header); + } + + return new SubmitPdu(spb); + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, boolean statusReportRequested) { + SubmitPduBase spb; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested, null); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, message, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param destinationPort the port to deliver the message to at the + * destination + * @param data the dat for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, short destinationPort, byte[] data, + boolean statusReportRequested) { + SubmitPduBase spb; + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + + if (PHONE_TYPE_CDMA == activePhone) { + spb = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } else { + spb = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress, + destinationAddress, destinationPort, data, statusReportRequested); + } + + return new SubmitPdu(spb); + } + + /** + * Returns the address of the SMS service center that relayed this message + * or null if there is none. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getServiceCenterAddress() { + return mWrappedSmsMessage.getServiceCenterAddress(); + } + + /** + * Returns the originating address (sender) of this SMS message in String + * form or null if unavailable + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getOriginatingAddress() { + return mWrappedSmsMessage.getOriginatingAddress(); + } + + /** + * Returns the originating address, or email from address if this message + * was from an email gateway. Returns null if originating address + * unavailable. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getDisplayOriginatingAddress() { + return mWrappedSmsMessage.getDisplayOriginatingAddress(); + } + + /** + * Returns the message body as a String, if it exists and is text based. + * @return message body is there is one, otherwise null + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getMessageBody() { + return mWrappedSmsMessage.getMessageBody(); + } + + /** + * Returns the class of this message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public MessageClass getMessageClass() { + int index = mWrappedSmsMessage.getMessageClass().ordinal(); + + return MessageClass.values()[index]; + } + + /** + * Returns the message body, or email message body if this message was from + * an email gateway. Returns null if message body unavailable. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getDisplayMessageBody() { + return mWrappedSmsMessage.getDisplayMessageBody(); + } + + /** + * Unofficial convention of a subject line enclosed in parens empty string + * if not present + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getPseudoSubject() { + return mWrappedSmsMessage.getPseudoSubject(); + } + + /** + * Returns the service centre timestamp in currentTimeMillis() format + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public long getTimestampMillis() { + return mWrappedSmsMessage.getTimestampMillis(); + } + + /** + * Returns true if message is an email. + * + * @return true if this message came through an email gateway and email + * sender / subject / parsed body are available + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isEmail() { + return mWrappedSmsMessage.isEmail(); + } + + /** + * @return if isEmail() is true, body of the email sent through the gateway. + * null otherwise + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getEmailBody() { + return mWrappedSmsMessage.getEmailBody(); + } + + /** + * @return if isEmail() is true, email from address of email sent through + * the gateway. null otherwise + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public String getEmailFrom() { + return mWrappedSmsMessage.getEmailFrom(); + } + + /** + * Get protocol identifier. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public int getProtocolIdentifier() { + return mWrappedSmsMessage.getProtocolIdentifier(); + } + + /** + * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" SMS + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isReplace() { + return mWrappedSmsMessage.isReplace(); + } + + /** + * Returns true for CPHS MWI toggle message. + * + * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section B.4.2 + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isCphsMwiMessage() { + return mWrappedSmsMessage.isCphsMwiMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) clear message + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isMWIClearMessage() { + return mWrappedSmsMessage.isMWIClearMessage(); + } + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) set message + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isMWISetMessage() { + return mWrappedSmsMessage.isMWISetMessage(); + } + + /** + * returns true if this message is a "Message Waiting Indication Group: + * Discard Message" notification and should not be stored. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isMwiDontStore() { + return mWrappedSmsMessage.isMwiDontStore(); + } + + /** + * returns the user data section minus the user data header if one was present. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public byte[] getUserData() { + return mWrappedSmsMessage.getUserData(); + } + + /* Not part of the SDK interface and only needed by specific classes: + protected SmsHeader getUserDataHeader() + */ + + /** + * Returns the raw PDU for the message. + * + * @return the raw PDU for the message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public byte[] getPdu() { + return mWrappedSmsMessage.getPdu(); + } + + /** + * Returns the status of the message on the SIM (read, unread, sent, unsent). + * + * @return the status of the message on the SIM. These are: + * SmsManager.STATUS_ON_SIM_FREE + * SmsManager.STATUS_ON_SIM_READ + * SmsManager.STATUS_ON_SIM_UNREAD + * SmsManager.STATUS_ON_SIM_SEND + * SmsManager.STATUS_ON_SIM_UNSENT + * @deprecated Use android.telephony.SmsMessage and getStatusOnIcc instead. + */ + @Deprecated + public int getStatusOnSim() { + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the status of the message on the ICC (read, unread, sent, unsent). + * + * @return the status of the message on the ICC. These are: + * SmsManager.STATUS_ON_ICC_FREE + * SmsManager.STATUS_ON_ICC_READ + * SmsManager.STATUS_ON_ICC_UNREAD + * SmsManager.STATUS_ON_ICC_SEND + * SmsManager.STATUS_ON_ICC_UNSENT + * @deprecated Use android.telephony.SmsMessage. + * @hide + */ + @Deprecated + public int getStatusOnIcc() { + + return mWrappedSmsMessage.getStatusOnIcc(); + } + + /** + * Returns the record index of the message on the SIM (1-based index). + * @return the record index of the message on the SIM, or -1 if this + * SmsMessage was not created from a SIM SMS EF record. + * @deprecated Use android.telephony.SmsMessage and getIndexOnIcc instead. + */ + @Deprecated + public int getIndexOnSim() { + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * Returns the record index of the message on the ICC (1-based index). + * @return the record index of the message on the ICC, or -1 if this + * SmsMessage was not created from a ICC SMS EF record. + * @deprecated Use android.telephony.SmsMessage. + * @hide + */ + @Deprecated + public int getIndexOnIcc() { + + return mWrappedSmsMessage.getIndexOnIcc(); + } + + /** + * GSM: + * For an SMS-STATUS-REPORT message, this returns the status field from + * the status report. This field indicates the status of a previously + * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a + * description of values. + * CDMA: + * For not interfering with status codes from GSM, the value is + * shifted to the bits 31-16. + * The value is composed of an error class (bits 25-24) and a status code (bits 23-16). + * Possible codes are described in C.S0015-B, v2.0, 4.5.21. + * + * @return 0 indicates the previously sent message was received. + * See TS 23.040, 9.9.2.3.15 and C.S0015-B, v2.0, 4.5.21 + * for a description of other possible values. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public int getStatus() { + return mWrappedSmsMessage.getStatus(); + } + + /** + * Return true iff the message is a SMS-STATUS-REPORT message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isStatusReportMessage() { + return mWrappedSmsMessage.isStatusReportMessage(); + } + + /** + * Returns true iff the <code>TP-Reply-Path</code> bit is set in + * this message. + * @deprecated Use android.telephony.SmsMessage. + */ + @Deprecated + public boolean isReplyPathPresent() { + return mWrappedSmsMessage.isReplyPathPresent(); + } + + /** This method returns the reference to a specific + * SmsMessage object, which is used for accessing its static methods. + * @return Specific SmsMessage. + * @deprecated Use android.telephony.SmsMessage. + */ + private static final SmsMessageBase getSmsFacility(){ + int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); + if (PHONE_TYPE_CDMA == activePhone) { + return new com.android.internal.telephony.cdma.SmsMessage(); + } else { + return new com.android.internal.telephony.gsm.SmsMessage(); + } + } +} diff --git a/src/java/com/android/internal/telephony/ATParseEx.java b/src/java/com/android/internal/telephony/ATParseEx.java new file mode 100644 index 0000000..c93b875 --- /dev/null +++ b/src/java/com/android/internal/telephony/ATParseEx.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class ATParseEx extends RuntimeException +{ + public + ATParseEx() + { + super(); + } + + public + ATParseEx(String s) + { + super(s); + } +} diff --git a/src/java/com/android/internal/telephony/ATResponseParser.java b/src/java/com/android/internal/telephony/ATResponseParser.java new file mode 100644 index 0000000..fdb0526 --- /dev/null +++ b/src/java/com/android/internal/telephony/ATResponseParser.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class ATResponseParser +{ + /*************************** Instance Variables **************************/ + + private String line; + private int next = 0; + private int tokStart, tokEnd; + + /***************************** Class Methods *****************************/ + + public + ATResponseParser (String line) + { + this.line = line; + } + + public boolean + nextBoolean() + { + // "\s*(\d)(,|$)" + // \d is '0' or '1' + + nextTok(); + + if (tokEnd - tokStart > 1) { + throw new ATParseEx(); + } + char c = line.charAt(tokStart); + + if (c == '0') return false; + if (c == '1') return true; + throw new ATParseEx(); + } + + + /** positive int only */ + public int + nextInt() + { + // "\s*(\d+)(,|$)" + int ret = 0; + + nextTok(); + + for (int i = tokStart ; i < tokEnd ; i++) { + char c = line.charAt(i); + + // Yes, ASCII decimal digits only + if (c < '0' || c > '9') { + throw new ATParseEx(); + } + + ret *= 10; + ret += c - '0'; + } + + return ret; + } + + public String + nextString() + { + nextTok(); + + return line.substring(tokStart, tokEnd); + } + + public boolean + hasMore() + { + return next < line.length(); + } + + private void + nextTok() + { + int len = line.length(); + + if (next == 0) { + skipPrefix(); + } + + if (next >= len) { + throw new ATParseEx(); + } + + try { + // \s*("([^"]*)"|(.*)\s*)(,|$) + + char c = line.charAt(next++); + boolean hasQuote = false; + + c = skipWhiteSpace(c); + + if (c == '"') { + if (next >= len) { + throw new ATParseEx(); + } + c = line.charAt(next++); + tokStart = next - 1; + while (c != '"' && next < len) { + c = line.charAt(next++); + } + if (c != '"') { + throw new ATParseEx(); + } + tokEnd = next - 1; + if (next < len && line.charAt(next++) != ',') { + throw new ATParseEx(); + } + } else { + tokStart = next - 1; + tokEnd = tokStart; + while (c != ',') { + if (!Character.isWhitespace(c)) { + tokEnd = next; + } + if (next == len) { + break; + } + c = line.charAt(next++); + } + } + } catch (StringIndexOutOfBoundsException ex) { + throw new ATParseEx(); + } + } + + + /** Throws ATParseEx if whitespace extends to the end of string */ + private char + skipWhiteSpace (char c) + { + int len; + len = line.length(); + while (next < len && Character.isWhitespace(c)) { + c = line.charAt(next++); + } + + if (Character.isWhitespace(c)) { + throw new ATParseEx(); + } + return c; + } + + + private void + skipPrefix() + { + // consume "^[^:]:" + + next = 0; + int s = line.length(); + while (next < s){ + char c = line.charAt(next++); + + if (c == ':') { + return; + } + } + + throw new ATParseEx("missing prefix"); + } + +} diff --git a/src/java/com/android/internal/telephony/AdnRecord.java b/src/java/com/android/internal/telephony/AdnRecord.java new file mode 100644 index 0000000..1bf2d3c --- /dev/null +++ b/src/java/com/android/internal/telephony/AdnRecord.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Arrays; + + +/** + * + * Used to load or store ADNs (Abbreviated Dialing Numbers). + * + * {@hide} + * + */ +public class AdnRecord implements Parcelable { + static final String LOG_TAG = "GSM"; + + //***** Instance Variables + + String alphaTag = null; + String number = null; + String[] emails; + int extRecord = 0xff; + int efid; // or 0 if none + int recordNumber; // or 0 if none + + + //***** Constants + + // In an ADN record, everything but the alpha identifier + // is in a footer that's 14 bytes + static final int FOOTER_SIZE_BYTES = 14; + + // Maximum size of the un-extended number field + static final int MAX_NUMBER_SIZE_BYTES = 11; + + static final int EXT_RECORD_LENGTH_BYTES = 13; + static final int EXT_RECORD_TYPE_ADDITIONAL_DATA = 2; + static final int EXT_RECORD_TYPE_MASK = 3; + static final int MAX_EXT_CALLED_PARTY_LENGTH = 0xa; + + // ADN offset + static final int ADN_BCD_NUMBER_LENGTH = 0; + static final int ADN_TON_AND_NPI = 1; + static final int ADN_DIALING_NUMBER_START = 2; + static final int ADN_DIALING_NUMBER_END = 11; + static final int ADN_CAPABILITY_ID = 12; + static final int ADN_EXTENSION_ID = 13; + + //***** Static Methods + + public static final Parcelable.Creator<AdnRecord> CREATOR + = new Parcelable.Creator<AdnRecord>() { + public AdnRecord createFromParcel(Parcel source) { + int efid; + int recordNumber; + String alphaTag; + String number; + String[] emails; + + efid = source.readInt(); + recordNumber = source.readInt(); + alphaTag = source.readString(); + number = source.readString(); + emails = source.readStringArray(); + + return new AdnRecord(efid, recordNumber, alphaTag, number, emails); + } + + public AdnRecord[] newArray(int size) { + return new AdnRecord[size]; + } + }; + + + //***** Constructor + public AdnRecord (byte[] record) { + this(0, 0, record); + } + + public AdnRecord (int efid, int recordNumber, byte[] record) { + this.efid = efid; + this.recordNumber = recordNumber; + parseRecord(record); + } + + public AdnRecord (String alphaTag, String number) { + this(0, 0, alphaTag, number); + } + + public AdnRecord (String alphaTag, String number, String[] emails) { + this(0, 0, alphaTag, number, emails); + } + + public AdnRecord (int efid, int recordNumber, String alphaTag, String number, String[] emails) { + this.efid = efid; + this.recordNumber = recordNumber; + this.alphaTag = alphaTag; + this.number = number; + this.emails = emails; + } + + public AdnRecord(int efid, int recordNumber, String alphaTag, String number) { + this.efid = efid; + this.recordNumber = recordNumber; + this.alphaTag = alphaTag; + this.number = number; + this.emails = null; + } + + //***** Instance Methods + + public String getAlphaTag() { + return alphaTag; + } + + public String getNumber() { + return number; + } + + public String[] getEmails() { + return emails; + } + + public void setEmails(String[] emails) { + this.emails = emails; + } + + public String toString() { + return "ADN Record '" + alphaTag + "' '" + number + " " + emails + "'"; + } + + public boolean isEmpty() { + return TextUtils.isEmpty(alphaTag) && TextUtils.isEmpty(number) && emails == null; + } + + public boolean hasExtendedRecord() { + return extRecord != 0 && extRecord != 0xff; + } + + /** Helper function for {@link #isEqual}. */ + private static boolean stringCompareNullEqualsEmpty(String s1, String s2) { + if (s1 == s2) { + return true; + } + if (s1 == null) { + s1 = ""; + } + if (s2 == null) { + s2 = ""; + } + return (s1.equals(s2)); + } + + public boolean isEqual(AdnRecord adn) { + return ( stringCompareNullEqualsEmpty(alphaTag, adn.alphaTag) && + stringCompareNullEqualsEmpty(number, adn.number) && + Arrays.equals(emails, adn.emails)); + } + //***** Parcelable Implementation + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(efid); + dest.writeInt(recordNumber); + dest.writeString(alphaTag); + dest.writeString(number); + dest.writeStringArray(emails); + } + + /** + * Build adn hex byte array based on record size + * The format of byte array is defined in 51.011 10.5.1 + * + * @param recordSize is the size X of EF record + * @return hex byte[recordSize] to be written to EF record + * return null for wrong format of dialing number or tag + */ + public byte[] buildAdnString(int recordSize) { + byte[] bcdNumber; + byte[] byteTag; + byte[] adnString; + int footerOffset = recordSize - FOOTER_SIZE_BYTES; + + // create an empty record + adnString = new byte[recordSize]; + for (int i = 0; i < recordSize; i++) { + adnString[i] = (byte) 0xFF; + } + + if (TextUtils.isEmpty(number)) { + Log.w(LOG_TAG, "[buildAdnString] Empty dialing number"); + return adnString; // return the empty record (for delete) + } else if (number.length() + > (ADN_DIALING_NUMBER_END - ADN_DIALING_NUMBER_START + 1) * 2) { + Log.w(LOG_TAG, + "[buildAdnString] Max length of dialing number is 20"); + return null; + } else if (alphaTag != null && alphaTag.length() > footerOffset) { + Log.w(LOG_TAG, + "[buildAdnString] Max length of tag is " + footerOffset); + return null; + } else { + bcdNumber = PhoneNumberUtils.numberToCalledPartyBCD(number); + + System.arraycopy(bcdNumber, 0, adnString, + footerOffset + ADN_TON_AND_NPI, bcdNumber.length); + + adnString[footerOffset + ADN_BCD_NUMBER_LENGTH] + = (byte) (bcdNumber.length); + adnString[footerOffset + ADN_CAPABILITY_ID] + = (byte) 0xFF; // Capability Id + adnString[footerOffset + ADN_EXTENSION_ID] + = (byte) 0xFF; // Extension Record Id + + if (!TextUtils.isEmpty(alphaTag)) { + byteTag = GsmAlphabet.stringToGsm8BitPacked(alphaTag); + System.arraycopy(byteTag, 0, adnString, 0, byteTag.length); + } + + return adnString; + } + } + + /** + * See TS 51.011 10.5.10 + */ + public void + appendExtRecord (byte[] extRecord) { + try { + if (extRecord.length != EXT_RECORD_LENGTH_BYTES) { + return; + } + + if ((extRecord[0] & EXT_RECORD_TYPE_MASK) + != EXT_RECORD_TYPE_ADDITIONAL_DATA) { + return; + } + + if ((0xff & extRecord[1]) > MAX_EXT_CALLED_PARTY_LENGTH) { + // invalid or empty record + return; + } + + number += PhoneNumberUtils.calledPartyBCDFragmentToString( + extRecord, 2, 0xff & extRecord[1]); + + // We don't support ext record chaining. + + } catch (RuntimeException ex) { + Log.w(LOG_TAG, "Error parsing AdnRecord ext record", ex); + } + } + + //***** Private Methods + + /** + * alphaTag and number are set to null on invalid format + */ + private void + parseRecord(byte[] record) { + try { + alphaTag = IccUtils.adnStringFieldToString( + record, 0, record.length - FOOTER_SIZE_BYTES); + + int footerOffset = record.length - FOOTER_SIZE_BYTES; + + int numberLength = 0xff & record[footerOffset]; + + if (numberLength > MAX_NUMBER_SIZE_BYTES) { + // Invalid number length + number = ""; + return; + } + + // Please note 51.011 10.5.1: + // + // "If the Dialling Number/SSC String does not contain + // a dialling number, e.g. a control string deactivating + // a service, the TON/NPI byte shall be set to 'FF' by + // the ME (see note 2)." + + number = PhoneNumberUtils.calledPartyBCDToString( + record, footerOffset + 1, numberLength); + + + extRecord = 0xff & record[record.length - 1]; + + emails = null; + + } catch (RuntimeException ex) { + Log.w(LOG_TAG, "Error parsing AdnRecord", ex); + number = ""; + alphaTag = ""; + emails = null; + } + } +} diff --git a/src/java/com/android/internal/telephony/AdnRecordCache.java b/src/java/com/android/internal/telephony/AdnRecordCache.java new file mode 100644 index 0000000..db5f4da --- /dev/null +++ b/src/java/com/android/internal/telephony/AdnRecordCache.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.telephony.gsm.UsimPhoneBookManager; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * {@hide} + */ +public final class AdnRecordCache extends Handler implements IccConstants { + //***** Instance Variables + + private IccFileHandler mFh; + private UsimPhoneBookManager mUsimPhoneBookManager; + + // Indexed by EF ID + SparseArray<ArrayList<AdnRecord>> adnLikeFiles + = new SparseArray<ArrayList<AdnRecord>>(); + + // People waiting for ADN-like files to be loaded + SparseArray<ArrayList<Message>> adnLikeWaiters + = new SparseArray<ArrayList<Message>>(); + + // People waiting for adn record to be updated + SparseArray<Message> userWriteResponse = new SparseArray<Message>(); + + //***** Event Constants + + static final int EVENT_LOAD_ALL_ADN_LIKE_DONE = 1; + static final int EVENT_UPDATE_ADN_DONE = 2; + + //***** Constructor + + + + public AdnRecordCache(IccFileHandler fh) { + mFh = fh; + mUsimPhoneBookManager = new UsimPhoneBookManager(mFh, this); + } + + //***** Called from SIMRecords + + /** + * Called from SIMRecords.onRadioNotAvailable and SIMRecords.handleSimRefresh. + */ + public void reset() { + adnLikeFiles.clear(); + mUsimPhoneBookManager.reset(); + + clearWaiters(); + clearUserWriters(); + + } + + private void clearWaiters() { + int size = adnLikeWaiters.size(); + for (int i = 0; i < size; i++) { + ArrayList<Message> waiters = adnLikeWaiters.valueAt(i); + AsyncResult ar = new AsyncResult(null, null, new RuntimeException("AdnCache reset")); + notifyWaiters(waiters, ar); + } + adnLikeWaiters.clear(); + } + + private void clearUserWriters() { + int size = userWriteResponse.size(); + for (int i = 0; i < size; i++) { + sendErrorResponse(userWriteResponse.valueAt(i), "AdnCace reset"); + } + userWriteResponse.clear(); + } + + /** + * @return List of AdnRecords for efid if we've already loaded them this + * radio session, or null if we haven't + */ + public ArrayList<AdnRecord> + getRecordsIfLoaded(int efid) { + return adnLikeFiles.get(efid); + } + + /** + * Returns extension ef associated with ADN-like EF or -1 if + * we don't know. + * + * See 3GPP TS 51.011 for this mapping + */ + int extensionEfForEf(int efid) { + switch (efid) { + case EF_MBDN: return EF_EXT6; + case EF_ADN: return EF_EXT1; + case EF_SDN: return EF_EXT3; + case EF_FDN: return EF_EXT2; + case EF_MSISDN: return EF_EXT1; + case EF_PBR: return 0; // The EF PBR doesn't have an extension record + default: return -1; + } + } + + private void sendErrorResponse(Message response, String errString) { + if (response != null) { + Exception e = new RuntimeException(errString); + AsyncResult.forMessage(response).exception = e; + response.sendToTarget(); + } + } + + /** + * Update an ADN-like record in EF by record index + * + * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN + * @param adn is the new adn to be stored + * @param recordIndex is the 1-based adn record index + * @param pin2 is required to update EF_FDN, otherwise must be null + * @param response message to be posted when done + * response.exception hold the exception in error + */ + public void updateAdnByIndex(int efid, AdnRecord adn, int recordIndex, String pin2, + Message response) { + + int extensionEF = extensionEfForEf(efid); + if (extensionEF < 0) { + sendErrorResponse(response, "EF is not known ADN-like EF:" + efid); + return; + } + + Message pendingResponse = userWriteResponse.get(efid); + if (pendingResponse != null) { + sendErrorResponse(response, "Have pending update for EF:" + efid); + return; + } + + userWriteResponse.put(efid, response); + + new AdnRecordLoader(mFh).updateEF(adn, efid, extensionEF, + recordIndex, pin2, + obtainMessage(EVENT_UPDATE_ADN_DONE, efid, recordIndex, adn)); + } + + /** + * Replace oldAdn with newAdn in ADN-like record in EF + * + * The ADN-like records must be read through requestLoadAllAdnLike() before + * + * @param efid must be one of EF_ADN, EF_FDN, and EF_SDN + * @param oldAdn is the adn to be replaced + * If oldAdn.isEmpty() is ture, it insert the newAdn + * @param newAdn is the adn to be stored + * If newAdn.isEmpty() is true, it delete the oldAdn + * @param pin2 is required to update EF_FDN, otherwise must be null + * @param response message to be posted when done + * response.exception hold the exception in error + */ + public void updateAdnBySearch(int efid, AdnRecord oldAdn, AdnRecord newAdn, + String pin2, Message response) { + + int extensionEF; + extensionEF = extensionEfForEf(efid); + + if (extensionEF < 0) { + sendErrorResponse(response, "EF is not known ADN-like EF:" + efid); + return; + } + + ArrayList<AdnRecord> oldAdnList; + + if (efid == EF_PBR) { + oldAdnList = mUsimPhoneBookManager.loadEfFilesFromUsim(); + } else { + oldAdnList = getRecordsIfLoaded(efid); + } + + if (oldAdnList == null) { + sendErrorResponse(response, "Adn list not exist for EF:" + efid); + return; + } + + int index = -1; + int count = 1; + for (Iterator<AdnRecord> it = oldAdnList.iterator(); it.hasNext(); ) { + if (oldAdn.isEqual(it.next())) { + index = count; + break; + } + count++; + } + + if (index == -1) { + sendErrorResponse(response, "Adn record don't exist for " + oldAdn); + return; + } + + if (efid == EF_PBR) { + AdnRecord foundAdn = oldAdnList.get(index-1); + efid = foundAdn.efid; + extensionEF = foundAdn.extRecord; + index = foundAdn.recordNumber; + + newAdn.efid = efid; + newAdn.extRecord = extensionEF; + newAdn.recordNumber = index; + } + + Message pendingResponse = userWriteResponse.get(efid); + + if (pendingResponse != null) { + sendErrorResponse(response, "Have pending update for EF:" + efid); + return; + } + + userWriteResponse.put(efid, response); + + new AdnRecordLoader(mFh).updateEF(newAdn, efid, extensionEF, + index, pin2, + obtainMessage(EVENT_UPDATE_ADN_DONE, efid, index, newAdn)); + } + + + /** + * Responds with exception (in response) if efid is not a known ADN-like + * record + */ + public void + requestLoadAllAdnLike (int efid, int extensionEf, Message response) { + ArrayList<Message> waiters; + ArrayList<AdnRecord> result; + + if (efid == EF_PBR) { + result = mUsimPhoneBookManager.loadEfFilesFromUsim(); + } else { + result = getRecordsIfLoaded(efid); + } + + // Have we already loaded this efid? + if (result != null) { + if (response != null) { + AsyncResult.forMessage(response).result = result; + response.sendToTarget(); + } + + return; + } + + // Have we already *started* loading this efid? + + waiters = adnLikeWaiters.get(efid); + + if (waiters != null) { + // There's a pending request for this EF already + // just add ourselves to it + + waiters.add(response); + return; + } + + // Start loading efid + + waiters = new ArrayList<Message>(); + waiters.add(response); + + adnLikeWaiters.put(efid, waiters); + + + if (extensionEf < 0) { + // respond with error if not known ADN-like record + + if (response != null) { + AsyncResult.forMessage(response).exception + = new RuntimeException("EF is not known ADN-like EF:" + efid); + response.sendToTarget(); + } + + return; + } + + new AdnRecordLoader(mFh).loadAllFromEF(efid, extensionEf, + obtainMessage(EVENT_LOAD_ALL_ADN_LIKE_DONE, efid, 0)); + } + + //***** Private methods + + private void + notifyWaiters(ArrayList<Message> waiters, AsyncResult ar) { + + if (waiters == null) { + return; + } + + for (int i = 0, s = waiters.size() ; i < s ; i++) { + Message waiter = waiters.get(i); + + AsyncResult.forMessage(waiter, ar.result, ar.exception); + waiter.sendToTarget(); + } + } + + //***** Overridden from Handler + + public void + handleMessage(Message msg) { + AsyncResult ar; + int efid; + + switch(msg.what) { + case EVENT_LOAD_ALL_ADN_LIKE_DONE: + /* arg1 is efid, obj.result is ArrayList<AdnRecord>*/ + ar = (AsyncResult) msg.obj; + efid = msg.arg1; + ArrayList<Message> waiters; + + waiters = adnLikeWaiters.get(efid); + adnLikeWaiters.delete(efid); + + if (ar.exception == null) { + adnLikeFiles.put(efid, (ArrayList<AdnRecord>) ar.result); + } + notifyWaiters(waiters, ar); + break; + case EVENT_UPDATE_ADN_DONE: + ar = (AsyncResult)msg.obj; + efid = msg.arg1; + int index = msg.arg2; + AdnRecord adn = (AdnRecord) (ar.userObj); + + if (ar.exception == null) { + adnLikeFiles.get(efid).set(index - 1, adn); + mUsimPhoneBookManager.invalidateCache(); + } + + Message response = userWriteResponse.get(efid); + userWriteResponse.delete(efid); + + AsyncResult.forMessage(response, null, ar.exception); + response.sendToTarget(); + break; + } + + } + + +} diff --git a/src/java/com/android/internal/telephony/AdnRecordLoader.java b/src/java/com/android/internal/telephony/AdnRecordLoader.java new file mode 100644 index 0000000..084fae6 --- /dev/null +++ b/src/java/com/android/internal/telephony/AdnRecordLoader.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import java.util.ArrayList; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + + +public class AdnRecordLoader extends Handler { + final static String LOG_TAG = "RIL_AdnRecordLoader"; + + //***** Instance Variables + + private IccFileHandler mFh; + int ef; + int extensionEF; + int pendingExtLoads; + Message userResponse; + String pin2; + + // For "load one" + int recordNumber; + + // for "load all" + ArrayList<AdnRecord> adns; // only valid after EVENT_ADN_LOAD_ALL_DONE + + // Either an AdnRecord or a reference to adns depending + // if this is a load one or load all operation + Object result; + + //***** Event Constants + + static final int EVENT_ADN_LOAD_DONE = 1; + static final int EVENT_EXT_RECORD_LOAD_DONE = 2; + static final int EVENT_ADN_LOAD_ALL_DONE = 3; + static final int EVENT_EF_LINEAR_RECORD_SIZE_DONE = 4; + static final int EVENT_UPDATE_RECORD_DONE = 5; + + //***** Constructor + + public AdnRecordLoader(IccFileHandler fh) { + // The telephony unit-test cases may create AdnRecords + // in secondary threads + super(Looper.getMainLooper()); + mFh = fh; + } + + /** + * Resulting AdnRecord is placed in response.obj.result + * or response.obj.exception is set + */ + public void + loadFromEF(int ef, int extensionEF, int recordNumber, + Message response) { + this.ef = ef; + this.extensionEF = extensionEF; + this.recordNumber = recordNumber; + this.userResponse = response; + + mFh.loadEFLinearFixed( + ef, recordNumber, + obtainMessage(EVENT_ADN_LOAD_DONE)); + + } + + + /** + * Resulting ArrayList<adnRecord> is placed in response.obj.result + * or response.obj.exception is set + */ + public void + loadAllFromEF(int ef, int extensionEF, + Message response) { + this.ef = ef; + this.extensionEF = extensionEF; + this.userResponse = response; + + mFh.loadEFLinearFixedAll( + ef, + obtainMessage(EVENT_ADN_LOAD_ALL_DONE)); + + } + + /** + * Write adn to a EF SIM record + * It will get the record size of EF record and compose hex adn array + * then write the hex array to EF record + * + * @param adn is set with alphaTag and phone number + * @param ef EF fileid + * @param extensionEF extension EF fileid + * @param recordNumber 1-based record index + * @param pin2 for CHV2 operations, must be null if pin2 is not needed + * @param response will be sent to its handler when completed + */ + public void + updateEF(AdnRecord adn, int ef, int extensionEF, int recordNumber, + String pin2, Message response) { + this.ef = ef; + this.extensionEF = extensionEF; + this.recordNumber = recordNumber; + this.userResponse = response; + this.pin2 = pin2; + + mFh.getEFLinearRecordSize( ef, + obtainMessage(EVENT_EF_LINEAR_RECORD_SIZE_DONE, adn)); + } + + //***** Overridden from Handler + + public void + handleMessage(Message msg) { + AsyncResult ar; + byte data[]; + AdnRecord adn; + + try { + switch (msg.what) { + case EVENT_EF_LINEAR_RECORD_SIZE_DONE: + ar = (AsyncResult)(msg.obj); + adn = (AdnRecord)(ar.userObj); + + if (ar.exception != null) { + throw new RuntimeException("get EF record size failed", + ar.exception); + } + + int[] recordSize = (int[])ar.result; + // recordSize is int[3] array + // int[0] is the record length + // int[1] is the total length of the EF file + // int[2] is the number of records in the EF file + // So int[0] * int[2] = int[1] + if (recordSize.length != 3 || recordNumber > recordSize[2]) { + throw new RuntimeException("get wrong EF record size format", + ar.exception); + } + + data = adn.buildAdnString(recordSize[0]); + + if(data == null) { + throw new RuntimeException("wrong ADN format", + ar.exception); + } + + mFh.updateEFLinearFixed(ef, recordNumber, + data, pin2, obtainMessage(EVENT_UPDATE_RECORD_DONE)); + + pendingExtLoads = 1; + + break; + case EVENT_UPDATE_RECORD_DONE: + ar = (AsyncResult)(msg.obj); + if (ar.exception != null) { + throw new RuntimeException("update EF adn record failed", + ar.exception); + } + pendingExtLoads = 0; + result = null; + break; + case EVENT_ADN_LOAD_DONE: + ar = (AsyncResult)(msg.obj); + data = (byte[])(ar.result); + + if (ar.exception != null) { + throw new RuntimeException("load failed", ar.exception); + } + + if (false) { + Log.d(LOG_TAG,"ADN EF: 0x" + + Integer.toHexString(ef) + + ":" + recordNumber + + "\n" + IccUtils.bytesToHexString(data)); + } + + adn = new AdnRecord(ef, recordNumber, data); + result = adn; + + if (adn.hasExtendedRecord()) { + // If we have a valid value in the ext record field, + // we're not done yet: we need to read the corresponding + // ext record and append it + + pendingExtLoads = 1; + + mFh.loadEFLinearFixed( + extensionEF, adn.extRecord, + obtainMessage(EVENT_EXT_RECORD_LOAD_DONE, adn)); + } + break; + + case EVENT_EXT_RECORD_LOAD_DONE: + ar = (AsyncResult)(msg.obj); + data = (byte[])(ar.result); + adn = (AdnRecord)(ar.userObj); + + if (ar.exception != null) { + throw new RuntimeException("load failed", ar.exception); + } + + Log.d(LOG_TAG,"ADN extension EF: 0x" + + Integer.toHexString(extensionEF) + + ":" + adn.extRecord + + "\n" + IccUtils.bytesToHexString(data)); + + adn.appendExtRecord(data); + + pendingExtLoads--; + // result should have been set in + // EVENT_ADN_LOAD_DONE or EVENT_ADN_LOAD_ALL_DONE + break; + + case EVENT_ADN_LOAD_ALL_DONE: + ar = (AsyncResult)(msg.obj); + ArrayList<byte[]> datas = (ArrayList<byte[]>)(ar.result); + + if (ar.exception != null) { + throw new RuntimeException("load failed", ar.exception); + } + + adns = new ArrayList<AdnRecord>(datas.size()); + result = adns; + pendingExtLoads = 0; + + for(int i = 0, s = datas.size() ; i < s ; i++) { + adn = new AdnRecord(ef, 1 + i, datas.get(i)); + adns.add(adn); + + if (adn.hasExtendedRecord()) { + // If we have a valid value in the ext record field, + // we're not done yet: we need to read the corresponding + // ext record and append it + + pendingExtLoads++; + + mFh.loadEFLinearFixed( + extensionEF, adn.extRecord, + obtainMessage(EVENT_EXT_RECORD_LOAD_DONE, adn)); + } + } + break; + } + } catch (RuntimeException exc) { + if (userResponse != null) { + AsyncResult.forMessage(userResponse) + .exception = exc; + userResponse.sendToTarget(); + // Loading is all or nothing--either every load succeeds + // or we fail the whole thing. + userResponse = null; + } + return; + } + + if (userResponse != null && pendingExtLoads == 0) { + AsyncResult.forMessage(userResponse).result + = result; + + userResponse.sendToTarget(); + userResponse = null; + } + } + + +} diff --git a/src/java/com/android/internal/telephony/ApnContext.java b/src/java/com/android/internal/telephony/ApnContext.java new file mode 100644 index 0000000..4817a7b --- /dev/null +++ b/src/java/com/android/internal/telephony/ApnContext.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Maintain the Apn context + */ +public class ApnContext { + + public final String LOG_TAG; + + protected static final boolean DBG = true; + + private final String mApnType; + + private DctConstants.State mState; + + private ArrayList<ApnSetting> mWaitingApns = null; + + /** A zero indicates that all waiting APNs had a permanent error */ + private AtomicInteger mWaitingApnsPermanentFailureCountDown; + + private ApnSetting mApnSetting; + + DataConnection mDataConnection; + + DataConnectionAc mDataConnectionAc; + + String mReason; + + int mRetryCount; + + /** + * user/app requested connection on this APN + */ + AtomicBoolean mDataEnabled; + + /** + * carrier requirements met + */ + AtomicBoolean mDependencyMet; + + public ApnContext(String apnType, String logTag) { + mApnType = apnType; + mState = DctConstants.State.IDLE; + setReason(Phone.REASON_DATA_ENABLED); + setRetryCount(0); + mDataEnabled = new AtomicBoolean(false); + mDependencyMet = new AtomicBoolean(true); + mWaitingApnsPermanentFailureCountDown = new AtomicInteger(0); + LOG_TAG = logTag; + } + + public String getApnType() { + return mApnType; + } + + public synchronized DataConnection getDataConnection() { + return mDataConnection; + } + + public synchronized void setDataConnection(DataConnection dc) { + if (DBG) { + log("setDataConnection: old dc=" + mDataConnection + " new dc=" + dc + " this=" + this); + } + mDataConnection = dc; + } + + + public synchronized DataConnectionAc getDataConnectionAc() { + return mDataConnectionAc; + } + + public synchronized void setDataConnectionAc(DataConnectionAc dcac) { + if (DBG) { + log("setDataConnectionAc: old dcac=" + mDataConnectionAc + " new dcac=" + dcac); + } + if (dcac != null) { + dcac.addApnContextSync(this); + } else { + if (mDataConnectionAc != null) { + mDataConnectionAc.removeApnContextSync(this); + } + } + mDataConnectionAc = dcac; + } + + public synchronized ApnSetting getApnSetting() { + return mApnSetting; + } + + public synchronized void setApnSetting(ApnSetting apnSetting) { + mApnSetting = apnSetting; + } + + public synchronized void setWaitingApns(ArrayList<ApnSetting> waitingApns) { + mWaitingApns = waitingApns; + mWaitingApnsPermanentFailureCountDown.set(mWaitingApns.size()); + } + + public int getWaitingApnsPermFailCount() { + return mWaitingApnsPermanentFailureCountDown.get(); + } + + public void decWaitingApnsPermFailCount() { + mWaitingApnsPermanentFailureCountDown.decrementAndGet(); + } + + public synchronized ApnSetting getNextWaitingApn() { + ArrayList<ApnSetting> list = mWaitingApns; + ApnSetting apn = null; + + if (list != null) { + if (!list.isEmpty()) { + apn = list.get(0); + } + } + return apn; + } + + public synchronized void removeWaitingApn(ApnSetting apn) { + if (mWaitingApns != null) { + mWaitingApns.remove(apn); + } + } + + public synchronized ArrayList<ApnSetting> getWaitingApns() { + return mWaitingApns; + } + + public synchronized void setState(DctConstants.State s) { + if (DBG) { + log("setState: " + s + ", previous state:" + mState); + } + + mState = s; + + if (mState == DctConstants.State.FAILED) { + if (mWaitingApns != null) { + mWaitingApns.clear(); // when teardown the connection and set to IDLE + } + } + } + + public synchronized DctConstants.State getState() { + return mState; + } + + public boolean isDisconnected() { + DctConstants.State currentState = getState(); + return ((currentState == DctConstants.State.IDLE) || + currentState == DctConstants.State.FAILED); + } + + public synchronized void setReason(String reason) { + if (DBG) { + log("set reason as " + reason + ",current state " + mState); + } + mReason = reason; + } + + public synchronized String getReason() { + return mReason; + } + + public synchronized void setRetryCount(int retryCount) { + if (DBG) { + log("setRetryCount: " + retryCount); + } + mRetryCount = retryCount; + DataConnection dc = mDataConnection; + if (dc != null) { + dc.setRetryCount(retryCount); + } + } + + public synchronized int getRetryCount() { + return mRetryCount; + } + + public boolean isReady() { + return mDataEnabled.get() && mDependencyMet.get(); + } + + public void setEnabled(boolean enabled) { + if (DBG) { + log("set enabled as " + enabled + ", current state is " + mDataEnabled.get()); + } + mDataEnabled.set(enabled); + } + + public boolean isEnabled() { + return mDataEnabled.get(); + } + + public void setDependencyMet(boolean met) { + if (DBG) { + log("set mDependencyMet as " + met + " current state is " + mDependencyMet.get()); + } + mDependencyMet.set(met); + } + + public boolean getDependencyMet() { + return mDependencyMet.get(); + } + + @Override + public String toString() { + // We don't print mDataConnection because its recursive. + return "{mApnType=" + mApnType + " mState=" + getState() + " mWaitingApns=" + mWaitingApns + + " mWaitingApnsPermanentFailureCountDown=" + mWaitingApnsPermanentFailureCountDown + + " mApnSetting=" + mApnSetting + " mDataConnectionAc=" + mDataConnectionAc + + " mReason=" + mReason + " mRetryCount=" + mRetryCount + + " mDataEnabled=" + mDataEnabled + " mDependencyMet=" + mDependencyMet + "}"; + } + + protected void log(String s) { + Log.d(LOG_TAG, "[ApnContext:" + mApnType + "] " + s); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("ApnContext: " + this.toString()); + } +} diff --git a/src/java/com/android/internal/telephony/ApnSetting.java b/src/java/com/android/internal/telephony/ApnSetting.java new file mode 100755 index 0000000..b84c69c --- /dev/null +++ b/src/java/com/android/internal/telephony/ApnSetting.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * This class represents a apn setting for create PDP link + */ +public class ApnSetting { + + static final String V2_FORMAT_REGEX = "^\\[ApnSettingV2\\]\\s*"; + + public final String carrier; + public final String apn; + public final String proxy; + public final String port; + public final String mmsc; + public final String mmsProxy; + public final String mmsPort; + public final String user; + public final String password; + public final int authType; + public final String[] types; + public final int id; + public final String numeric; + public final String protocol; + public final String roamingProtocol; + /** + * Current status of APN + * true : enabled APN, false : disabled APN. + */ + public final boolean carrierEnabled; + /** + * Radio Access Technology info + * To check what values can hold, refer to ServiceState.java. + * This should be spread to other technologies, + * but currently only used for LTE(14) and EHRPD(13). + */ + public final int bearer; + + public ApnSetting(int id, String numeric, String carrier, String apn, + String proxy, String port, + String mmsc, String mmsProxy, String mmsPort, + String user, String password, int authType, String[] types, + String protocol, String roamingProtocol, boolean carrierEnabled, int bearer) { + this.id = id; + this.numeric = numeric; + this.carrier = carrier; + this.apn = apn; + this.proxy = proxy; + this.port = port; + this.mmsc = mmsc; + this.mmsProxy = mmsProxy; + this.mmsPort = mmsPort; + this.user = user; + this.password = password; + this.authType = authType; + this.types = types; + this.protocol = protocol; + this.roamingProtocol = roamingProtocol; + this.carrierEnabled = carrierEnabled; + this.bearer = bearer; + } + + /** + * Creates an ApnSetting object from a string. + * + * @param data the string to read. + * + * The string must be in one of two formats (newlines added for clarity, + * spaces are optional): + * + * v1 format: + * <carrier>, <apn>, <proxy>, <port>, <mmsc>, <mmsproxy>, + * <mmsport>, <user>, <password>, <authtype>, <mcc>,<mnc>, + * <type>[, <type>...] + * + * v2 format: + * [ApnSettingV2] <carrier>, <apn>, <proxy>, <port>, <mmsc>, <mmsproxy>, + * <mmsport>, <user>, <password>, <authtype>, <mcc>, <mnc>, + * <type>[| <type>...], <protocol>, <roaming_protocol>, <carrierEnabled>, <bearer> + * + * Note that the strings generated by toString() do not contain the username + * and password and thus cannot be read by this method. + * + * @see ApnSettingTest + */ + public static ApnSetting fromString(String data) { + if (data == null) return null; + + int version; + // matches() operates on the whole string, so append .* to the regex. + if (data.matches(V2_FORMAT_REGEX + ".*")) { + version = 2; + data = data.replaceFirst(V2_FORMAT_REGEX, ""); + } else { + version = 1; + } + + String[] a = data.split("\\s*,\\s*"); + if (a.length < 14) { + return null; + } + + int authType; + try { + authType = Integer.parseInt(a[12]); + } catch (Exception e) { + authType = 0; + } + + String[] typeArray; + String protocol, roamingProtocol; + boolean carrierEnabled; + int bearer; + if (version == 1) { + typeArray = new String[a.length - 13]; + System.arraycopy(a, 13, typeArray, 0, a.length - 13); + protocol = RILConstants.SETUP_DATA_PROTOCOL_IP; + roamingProtocol = RILConstants.SETUP_DATA_PROTOCOL_IP; + carrierEnabled = true; + bearer = 0; + } else { + if (a.length < 18) { + return null; + } + typeArray = a[13].split("\\s*\\|\\s*"); + protocol = a[14]; + roamingProtocol = a[15]; + try { + carrierEnabled = Boolean.parseBoolean(a[16]); + } catch (Exception e) { + carrierEnabled = true; + } + bearer = Integer.parseInt(a[17]); + } + + return new ApnSetting(-1,a[10]+a[11],a[0],a[1],a[2],a[3],a[7],a[8], + a[9],a[4],a[5],authType,typeArray,protocol,roamingProtocol,carrierEnabled,bearer); + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("[ApnSettingV2] ") + .append(carrier) + .append(", ").append(id) + .append(", ").append(numeric) + .append(", ").append(apn) + .append(", ").append(proxy) + .append(", ").append(mmsc) + .append(", ").append(mmsProxy) + .append(", ").append(mmsPort) + .append(", ").append(port) + .append(", ").append(authType).append(", "); + for (int i = 0; i < types.length; i++) { + sb.append(types[i]); + if (i < types.length - 1) { + sb.append(" | "); + } + } + sb.append(", ").append(protocol); + sb.append(", ").append(roamingProtocol); + sb.append(", ").append(carrierEnabled); + sb.append(", ").append(bearer); + return sb.toString(); + } + + public boolean canHandleType(String type) { + for (String t : types) { + // DEFAULT handles all, and HIPRI is handled by DEFAULT + if (t.equalsIgnoreCase(type) || + t.equalsIgnoreCase(PhoneConstants.APN_TYPE_ALL) || + (t.equalsIgnoreCase(PhoneConstants.APN_TYPE_DEFAULT) && + type.equalsIgnoreCase(PhoneConstants.APN_TYPE_HIPRI))) { + return true; + } + } + return false; + } + + // TODO - if we have this function we should also have hashCode. + // Also should handle changes in type order and perhaps case-insensitivity + public boolean equals(Object o) { + if (o instanceof ApnSetting == false) return false; + return (this.toString().equals(o.toString())); + } +} diff --git a/src/java/com/android/internal/telephony/BaseCommands.java b/src/java/com/android/internal/telephony/BaseCommands.java new file mode 100644 index 0000000..1b54656 --- /dev/null +++ b/src/java/com/android/internal/telephony/BaseCommands.java @@ -0,0 +1,647 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + + +import android.content.Context; +import android.os.RegistrantList; +import android.os.Registrant; +import android.os.Handler; +import android.os.AsyncResult; +import android.os.SystemProperties; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * {@hide} + */ +public abstract class BaseCommands implements CommandsInterface { + static final String LOG_TAG = "RILB"; + + //***** Instance Variables + protected Context mContext; + protected RadioState mState = RadioState.RADIO_UNAVAILABLE; + protected Object mStateMonitor = new Object(); + + protected RegistrantList mRadioStateChangedRegistrants = new RegistrantList(); + protected RegistrantList mOnRegistrants = new RegistrantList(); + protected RegistrantList mAvailRegistrants = new RegistrantList(); + protected RegistrantList mOffOrNotAvailRegistrants = new RegistrantList(); + protected RegistrantList mNotAvailRegistrants = new RegistrantList(); + protected RegistrantList mCallStateRegistrants = new RegistrantList(); + protected RegistrantList mVoiceNetworkStateRegistrants = new RegistrantList(); + protected RegistrantList mDataNetworkStateRegistrants = new RegistrantList(); + protected RegistrantList mVoiceRadioTechChangedRegistrants = new RegistrantList(); + protected RegistrantList mIccStatusChangedRegistrants = new RegistrantList(); + protected RegistrantList mVoicePrivacyOnRegistrants = new RegistrantList(); + protected RegistrantList mVoicePrivacyOffRegistrants = new RegistrantList(); + protected Registrant mUnsolOemHookRawRegistrant; + protected RegistrantList mOtaProvisionRegistrants = new RegistrantList(); + protected RegistrantList mCallWaitingInfoRegistrants = new RegistrantList(); + protected RegistrantList mDisplayInfoRegistrants = new RegistrantList(); + protected RegistrantList mSignalInfoRegistrants = new RegistrantList(); + protected RegistrantList mNumberInfoRegistrants = new RegistrantList(); + protected RegistrantList mRedirNumInfoRegistrants = new RegistrantList(); + protected RegistrantList mLineControlInfoRegistrants = new RegistrantList(); + protected RegistrantList mT53ClirInfoRegistrants = new RegistrantList(); + protected RegistrantList mT53AudCntrlInfoRegistrants = new RegistrantList(); + protected RegistrantList mRingbackToneRegistrants = new RegistrantList(); + protected RegistrantList mResendIncallMuteRegistrants = new RegistrantList(); + protected RegistrantList mCdmaSubscriptionChangedRegistrants = new RegistrantList(); + protected RegistrantList mCdmaPrlChangedRegistrants = new RegistrantList(); + protected RegistrantList mExitEmergencyCallbackModeRegistrants = new RegistrantList(); + protected RegistrantList mRilConnectedRegistrants = new RegistrantList(); + protected RegistrantList mIccRefreshRegistrants = new RegistrantList(); + + protected Registrant mGsmSmsRegistrant; + protected Registrant mCdmaSmsRegistrant; + protected Registrant mNITZTimeRegistrant; + protected Registrant mSignalStrengthRegistrant; + protected Registrant mUSSDRegistrant; + protected Registrant mSmsOnSimRegistrant; + protected Registrant mSmsStatusRegistrant; + protected Registrant mSsnRegistrant; + protected Registrant mCatSessionEndRegistrant; + protected Registrant mCatProCmdRegistrant; + protected Registrant mCatEventRegistrant; + protected Registrant mCatCallSetUpRegistrant; + protected Registrant mIccSmsFullRegistrant; + protected Registrant mEmergencyCallbackModeRegistrant; + protected Registrant mRingRegistrant; + protected Registrant mRestrictedStateRegistrant; + protected Registrant mGsmBroadcastSmsRegistrant; + + // Preferred network type received from PhoneFactory. + // This is used when establishing a connection to the + // vendor ril so it starts up in the correct mode. + protected int mPreferredNetworkType; + // CDMA subscription received from PhoneFactory + protected int mCdmaSubscription; + // Type of Phone, GSM or CDMA. Set by CDMAPhone or GSMPhone. + protected int mPhoneType; + // RIL Version + protected int mRilVersion = -1; + + public BaseCommands(Context context) { + mContext = context; // May be null (if so we won't log statistics) + } + + //***** CommandsInterface implementation + + public RadioState getRadioState() { + return mState; + } + + public void registerForRadioStateChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mRadioStateChangedRegistrants.add(r); + r.notifyRegistrant(); + } + } + + public void unregisterForRadioStateChanged(Handler h) { + synchronized (mStateMonitor) { + mRadioStateChangedRegistrants.remove(h); + } + } + + public void registerForOn(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mOnRegistrants.add(r); + + if (mState.isOn()) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + public void unregisterForOn(Handler h) { + synchronized (mStateMonitor) { + mOnRegistrants.remove(h); + } + } + + + public void registerForAvailable(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mAvailRegistrants.add(r); + + if (mState.isAvailable()) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + + public void unregisterForAvailable(Handler h) { + synchronized(mStateMonitor) { + mAvailRegistrants.remove(h); + } + } + + public void registerForNotAvailable(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mNotAvailRegistrants.add(r); + + if (!mState.isAvailable()) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + + public void unregisterForNotAvailable(Handler h) { + synchronized (mStateMonitor) { + mNotAvailRegistrants.remove(h); + } + } + + public void registerForOffOrNotAvailable(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mOffOrNotAvailRegistrants.add(r); + + if (mState == RadioState.RADIO_OFF || !mState.isAvailable()) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + public void unregisterForOffOrNotAvailable(Handler h) { + synchronized(mStateMonitor) { + mOffOrNotAvailRegistrants.remove(h); + } + } + + public void registerForCallStateChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mCallStateRegistrants.add(r); + } + + public void unregisterForCallStateChanged(Handler h) { + mCallStateRegistrants.remove(h); + } + + public void registerForVoiceNetworkStateChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mVoiceNetworkStateRegistrants.add(r); + } + + public void unregisterForVoiceNetworkStateChanged(Handler h) { + mVoiceNetworkStateRegistrants.remove(h); + } + + public void registerForDataNetworkStateChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mDataNetworkStateRegistrants.add(r); + } + + public void unregisterForDataNetworkStateChanged(Handler h) { + mDataNetworkStateRegistrants.remove(h); + } + + public void registerForVoiceRadioTechChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mVoiceRadioTechChangedRegistrants.add(r); + } + + public void unregisterForVoiceRadioTechChanged(Handler h) { + mVoiceRadioTechChangedRegistrants.remove(h); + } + + public void registerForIccStatusChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mIccStatusChangedRegistrants.add(r); + } + + public void unregisterForIccStatusChanged(Handler h) { + mIccStatusChangedRegistrants.remove(h); + } + + public void setOnNewGsmSms(Handler h, int what, Object obj) { + mGsmSmsRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnNewGsmSms(Handler h) { + mGsmSmsRegistrant.clear(); + } + + public void setOnNewCdmaSms(Handler h, int what, Object obj) { + mCdmaSmsRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnNewCdmaSms(Handler h) { + mCdmaSmsRegistrant.clear(); + } + + public void setOnNewGsmBroadcastSms(Handler h, int what, Object obj) { + mGsmBroadcastSmsRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnNewGsmBroadcastSms(Handler h) { + mGsmBroadcastSmsRegistrant.clear(); + } + + public void setOnSmsOnSim(Handler h, int what, Object obj) { + mSmsOnSimRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnSmsOnSim(Handler h) { + mSmsOnSimRegistrant.clear(); + } + + public void setOnSmsStatus(Handler h, int what, Object obj) { + mSmsStatusRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnSmsStatus(Handler h) { + mSmsStatusRegistrant.clear(); + } + + public void setOnSignalStrengthUpdate(Handler h, int what, Object obj) { + mSignalStrengthRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnSignalStrengthUpdate(Handler h) { + mSignalStrengthRegistrant.clear(); + } + + public void setOnNITZTime(Handler h, int what, Object obj) { + mNITZTimeRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnNITZTime(Handler h) { + mNITZTimeRegistrant.clear(); + } + + public void setOnUSSD(Handler h, int what, Object obj) { + mUSSDRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnUSSD(Handler h) { + mUSSDRegistrant.clear(); + } + + public void setOnSuppServiceNotification(Handler h, int what, Object obj) { + mSsnRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnSuppServiceNotification(Handler h) { + mSsnRegistrant.clear(); + } + + public void setOnCatSessionEnd(Handler h, int what, Object obj) { + mCatSessionEndRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnCatSessionEnd(Handler h) { + mCatSessionEndRegistrant.clear(); + } + + public void setOnCatProactiveCmd(Handler h, int what, Object obj) { + mCatProCmdRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnCatProactiveCmd(Handler h) { + mCatProCmdRegistrant.clear(); + } + + public void setOnCatEvent(Handler h, int what, Object obj) { + mCatEventRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnCatEvent(Handler h) { + mCatEventRegistrant.clear(); + } + + public void setOnCatCallSetUp(Handler h, int what, Object obj) { + mCatCallSetUpRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnCatCallSetUp(Handler h) { + mCatCallSetUpRegistrant.clear(); + } + + public void setOnIccSmsFull(Handler h, int what, Object obj) { + mIccSmsFullRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnIccSmsFull(Handler h) { + mIccSmsFullRegistrant.clear(); + } + + public void registerForIccRefresh(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mIccRefreshRegistrants.add(r); + } + public void setOnIccRefresh(Handler h, int what, Object obj) { + registerForIccRefresh(h, what, obj); + } + + public void setEmergencyCallbackMode(Handler h, int what, Object obj) { + mEmergencyCallbackModeRegistrant = new Registrant (h, what, obj); + } + + public void unregisterForIccRefresh(Handler h) { + mIccRefreshRegistrants.remove(h); + } + public void unsetOnIccRefresh(Handler h) { + unregisterForIccRefresh(h); + } + + public void setOnCallRing(Handler h, int what, Object obj) { + mRingRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnCallRing(Handler h) { + mRingRegistrant.clear(); + } + + public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mVoicePrivacyOnRegistrants.add(r); + } + + public void unregisterForInCallVoicePrivacyOn(Handler h){ + mVoicePrivacyOnRegistrants.remove(h); + } + + public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mVoicePrivacyOffRegistrants.add(r); + } + + public void unregisterForInCallVoicePrivacyOff(Handler h){ + mVoicePrivacyOffRegistrants.remove(h); + } + + public void setOnRestrictedStateChanged(Handler h, int what, Object obj) { + mRestrictedStateRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnRestrictedStateChanged(Handler h) { + mRestrictedStateRegistrant.clear(); + } + + public void registerForDisplayInfo(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mDisplayInfoRegistrants.add(r); + } + + public void unregisterForDisplayInfo(Handler h) { + mDisplayInfoRegistrants.remove(h); + } + + public void registerForCallWaitingInfo(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mCallWaitingInfoRegistrants.add(r); + } + + public void unregisterForCallWaitingInfo(Handler h) { + mCallWaitingInfoRegistrants.remove(h); + } + + public void registerForSignalInfo(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mSignalInfoRegistrants.add(r); + } + + public void setOnUnsolOemHookRaw(Handler h, int what, Object obj) { + mUnsolOemHookRawRegistrant = new Registrant (h, what, obj); + } + + public void unSetOnUnsolOemHookRaw(Handler h) { + mUnsolOemHookRawRegistrant.clear(); + } + + public void unregisterForSignalInfo(Handler h) { + mSignalInfoRegistrants.remove(h); + } + + public void registerForCdmaOtaProvision(Handler h,int what, Object obj){ + Registrant r = new Registrant (h, what, obj); + mOtaProvisionRegistrants.add(r); + } + + public void unregisterForCdmaOtaProvision(Handler h){ + mOtaProvisionRegistrants.remove(h); + } + + public void registerForNumberInfo(Handler h,int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mNumberInfoRegistrants.add(r); + } + + public void unregisterForNumberInfo(Handler h){ + mNumberInfoRegistrants.remove(h); + } + + public void registerForRedirectedNumberInfo(Handler h,int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mRedirNumInfoRegistrants.add(r); + } + + public void unregisterForRedirectedNumberInfo(Handler h) { + mRedirNumInfoRegistrants.remove(h); + } + + public void registerForLineControlInfo(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mLineControlInfoRegistrants.add(r); + } + + public void unregisterForLineControlInfo(Handler h) { + mLineControlInfoRegistrants.remove(h); + } + + public void registerFoT53ClirlInfo(Handler h,int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mT53ClirInfoRegistrants.add(r); + } + + public void unregisterForT53ClirInfo(Handler h) { + mT53ClirInfoRegistrants.remove(h); + } + + public void registerForT53AudioControlInfo(Handler h,int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mT53AudCntrlInfoRegistrants.add(r); + } + + public void unregisterForT53AudioControlInfo(Handler h) { + mT53AudCntrlInfoRegistrants.remove(h); + } + + public void registerForRingbackTone(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mRingbackToneRegistrants.add(r); + } + + public void unregisterForRingbackTone(Handler h) { + mRingbackToneRegistrants.remove(h); + } + + public void registerForResendIncallMute(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mResendIncallMuteRegistrants.add(r); + } + + public void unregisterForResendIncallMute(Handler h) { + mResendIncallMuteRegistrants.remove(h); + } + + @Override + public void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mCdmaSubscriptionChangedRegistrants.add(r); + } + + @Override + public void unregisterForCdmaSubscriptionChanged(Handler h) { + mCdmaSubscriptionChangedRegistrants.remove(h); + } + + @Override + public void registerForCdmaPrlChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mCdmaPrlChangedRegistrants.add(r); + } + + @Override + public void unregisterForCdmaPrlChanged(Handler h) { + mCdmaPrlChangedRegistrants.remove(h); + } + + @Override + public void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mExitEmergencyCallbackModeRegistrants.add(r); + } + + @Override + public void unregisterForExitEmergencyCallbackMode(Handler h) { + mExitEmergencyCallbackModeRegistrants.remove(h); + } + + /** + * {@inheritDoc} + */ + @Override + public void registerForRilConnected(Handler h, int what, Object obj) { + Log.d(LOG_TAG, "registerForRilConnected h=" + h + " w=" + what); + Registrant r = new Registrant (h, what, obj); + mRilConnectedRegistrants.add(r); + if (mRilVersion != -1) { + Log.d(LOG_TAG, "Notifying: ril connected mRilVersion=" + mRilVersion); + r.notifyRegistrant(new AsyncResult(null, new Integer(mRilVersion), null)); + } + } + + @Override + public void unregisterForRilConnected(Handler h) { + mRilConnectedRegistrants.remove(h); + } + + /** + * {@inheritDoc} + */ + @Override + public void setCurrentPreferredNetworkType() { + } + + //***** Protected Methods + /** + * Store new RadioState and send notification based on the changes + * + * This function is called only by RIL.java when receiving unsolicited + * RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED + * + * RadioState has 3 values : RADIO_OFF, RADIO_UNAVAILABLE, RADIO_ON. + * + * @param newState new RadioState decoded from RIL_UNSOL_RADIO_STATE_CHANGED + */ + protected void setRadioState(RadioState newState) { + RadioState oldState; + + synchronized (mStateMonitor) { + if (false) { + Log.v(LOG_TAG, "setRadioState old: " + mState + + " new " + newState); + } + + oldState = mState; + mState = newState; + + if (oldState == mState) { + // no state transition + return; + } + + mRadioStateChangedRegistrants.notifyRegistrants(); + + if (mState.isAvailable() && !oldState.isAvailable()) { + Log.d(LOG_TAG,"Notifying: radio available"); + mAvailRegistrants.notifyRegistrants(); + onRadioAvailable(); + } + + if (!mState.isAvailable() && oldState.isAvailable()) { + Log.d(LOG_TAG,"Notifying: radio not available"); + mNotAvailRegistrants.notifyRegistrants(); + } + + if (mState.isOn() && !oldState.isOn()) { + Log.d(LOG_TAG,"Notifying: Radio On"); + mOnRegistrants.notifyRegistrants(); + } + + if ((!mState.isOn() || !mState.isAvailable()) + && !((!oldState.isOn() || !oldState.isAvailable())) + ) { + Log.d(LOG_TAG,"Notifying: radio off or not available"); + mOffOrNotAvailRegistrants.notifyRegistrants(); + } + } + } + + protected void onRadioAvailable() { + } + + /** + * {@inheritDoc} + */ + @Override + public int getLteOnCdmaMode() { + return TelephonyManager.getLteOnCdmaModeStatic(); + } + + @Override + public void testingEmergencyCall() {} +} diff --git a/src/java/com/android/internal/telephony/Call.java b/src/java/com/android/internal/telephony/Call.java new file mode 100644 index 0000000..4967ab8 --- /dev/null +++ b/src/java/com/android/internal/telephony/Call.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import java.util.List; + +import android.util.Log; + +/** + * {@hide} + */ +public abstract class Call { + /* Enums */ + + public enum State { + IDLE, ACTIVE, HOLDING, DIALING, ALERTING, INCOMING, WAITING, DISCONNECTED, DISCONNECTING; + + public boolean isAlive() { + return !(this == IDLE || this == DISCONNECTED || this == DISCONNECTING); + } + + public boolean isRinging() { + return this == INCOMING || this == WAITING; + } + + public boolean isDialing() { + return this == DIALING || this == ALERTING; + } + } + + + /* Instance Variables */ + + public State state = State.IDLE; + + + // Flag to indicate if the current calling/caller information + // is accurate. If false the information is known to be accurate. + // + // For CDMA, during call waiting/3 way, there is no network response + // if call waiting is answered, network timed out, dropped, 3 way + // merged, etc. + protected boolean isGeneric = false; + + protected final String LOG_TAG = "Call"; + + /* Instance Methods */ + + /** Do not modify the List result!!! This list is not yours to keep + * It will change across event loop iterations top + */ + + public abstract List<Connection> getConnections(); + public abstract Phone getPhone(); + public abstract boolean isMultiparty(); + public abstract void hangup() throws CallStateException; + + + /** + * hasConnection + * + * @param c a Connection object + * @return true if the call contains the connection object passed in + */ + public boolean hasConnection(Connection c) { + return c.getCall() == this; + } + + /** + * hasConnections + * @return true if the call contains one or more connections + */ + public boolean hasConnections() { + List connections = getConnections(); + + if (connections == null) { + return false; + } + + return connections.size() > 0; + } + + /** + * getState + * @return state of class call + */ + public State getState() { + return state; + } + + /** + * isIdle + * + * FIXME rename + * @return true if the call contains only disconnected connections (if any) + */ + public boolean isIdle() { + return !getState().isAlive(); + } + + /** + * Returns the Connection associated with this Call that was created + * first, or null if there are no Connections in this Call + */ + public Connection + getEarliestConnection() { + List l; + long time = Long.MAX_VALUE; + Connection c; + Connection earliest = null; + + l = getConnections(); + + if (l.size() == 0) { + return null; + } + + for (int i = 0, s = l.size() ; i < s ; i++) { + c = (Connection) l.get(i); + long t; + + t = c.getCreateTime(); + + if (t < time) { + earliest = c; + time = t; + } + } + + return earliest; + } + + public long + getEarliestCreateTime() { + List l; + long time = Long.MAX_VALUE; + + l = getConnections(); + + if (l.size() == 0) { + return 0; + } + + for (int i = 0, s = l.size() ; i < s ; i++) { + Connection c = (Connection) l.get(i); + long t; + + t = c.getCreateTime(); + + time = t < time ? t : time; + } + + return time; + } + + public long + getEarliestConnectTime() { + long time = Long.MAX_VALUE; + List l = getConnections(); + + if (l.size() == 0) { + return 0; + } + + for (int i = 0, s = l.size() ; i < s ; i++) { + Connection c = (Connection) l.get(i); + long t; + + t = c.getConnectTime(); + + time = t < time ? t : time; + } + + return time; + } + + + public boolean + isDialingOrAlerting() { + return getState().isDialing(); + } + + public boolean + isRinging() { + return getState().isRinging(); + } + + /** + * Returns the Connection associated with this Call that was created + * last, or null if there are no Connections in this Call + */ + public Connection + getLatestConnection() { + List l = getConnections(); + if (l.size() == 0) { + return null; + } + + long time = 0; + Connection latest = null; + for (int i = 0, s = l.size() ; i < s ; i++) { + Connection c = (Connection) l.get(i); + long t = c.getCreateTime(); + + if (t > time) { + latest = c; + time = t; + } + } + + return latest; + } + + /** + * To indicate if the connection information is accurate + * or not. false means accurate. Only used for CDMA. + */ + public boolean isGeneric() { + return isGeneric; + } + + /** + * Set the generic instance variable + */ + public void setGeneric(boolean generic) { + isGeneric = generic; + } + + /** + * Hangup call if it is alive + */ + public void hangupIfAlive() { + if (getState().isAlive()) { + try { + hangup(); + } catch (CallStateException ex) { + Log.w(LOG_TAG, " hangupIfActive: caught " + ex); + } + } + } +} diff --git a/src/java/com/android/internal/telephony/CallForwardInfo.java b/src/java/com/android/internal/telephony/CallForwardInfo.java new file mode 100644 index 0000000..8b853b0 --- /dev/null +++ b/src/java/com/android/internal/telephony/CallForwardInfo.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.telephony.PhoneNumberUtils; + +/** + * See also RIL_CallForwardInfo in include/telephony/ril.h + * + * {@hide} + */ +public class CallForwardInfo { + public int status; /*1 = active, 0 = not active */ + public int reason; /* from TS 27.007 7.11 "reason" */ + public int serviceClass; /* Sum of CommandsInterface.SERVICE_CLASS */ + public int toa; /* "type" from TS 27.007 7.11 */ + public String number; /* "number" from TS 27.007 7.11 */ + public int timeSeconds; /* for CF no reply only */ + + public String toString() { + return super.toString() + (status == 0 ? " not active " : " active ") + + " reason: " + reason + + " serviceClass: " + serviceClass + + " \"" + PhoneNumberUtils.stringFromStringAndTOA(number, toa) + "\" " + + timeSeconds + " seconds"; + + } +} diff --git a/src/java/com/android/internal/telephony/CallManager.java b/src/java/com/android/internal/telephony/CallManager.java new file mode 100644 index 0000000..b87ea50 --- /dev/null +++ b/src/java/com/android/internal/telephony/CallManager.java @@ -0,0 +1,1855 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +import com.android.internal.telephony.sip.SipPhone; + +import android.content.Context; +import android.media.AudioManager; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.RegistrantList; +import android.os.Registrant; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + + +/** + * @hide + * + * CallManager class provides an abstract layer for PhoneApp to access + * and control calls. It implements Phone interface. + * + * CallManager provides call and connection control as well as + * channel capability. + * + * There are three categories of APIs CallManager provided + * + * 1. Call control and operation, such as dial() and hangup() + * 2. Channel capabilities, such as CanConference() + * 3. Register notification + * + * + */ +public final class CallManager { + + private static final String LOG_TAG ="CallManager"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + private static final int EVENT_DISCONNECT = 100; + private static final int EVENT_PRECISE_CALL_STATE_CHANGED = 101; + private static final int EVENT_NEW_RINGING_CONNECTION = 102; + private static final int EVENT_UNKNOWN_CONNECTION = 103; + private static final int EVENT_INCOMING_RING = 104; + private static final int EVENT_RINGBACK_TONE = 105; + private static final int EVENT_IN_CALL_VOICE_PRIVACY_ON = 106; + private static final int EVENT_IN_CALL_VOICE_PRIVACY_OFF = 107; + private static final int EVENT_CALL_WAITING = 108; + private static final int EVENT_DISPLAY_INFO = 109; + private static final int EVENT_SIGNAL_INFO = 110; + private static final int EVENT_CDMA_OTA_STATUS_CHANGE = 111; + private static final int EVENT_RESEND_INCALL_MUTE = 112; + private static final int EVENT_MMI_INITIATE = 113; + private static final int EVENT_MMI_COMPLETE = 114; + private static final int EVENT_ECM_TIMER_RESET = 115; + private static final int EVENT_SUBSCRIPTION_INFO_READY = 116; + private static final int EVENT_SUPP_SERVICE_FAILED = 117; + private static final int EVENT_SERVICE_STATE_CHANGED = 118; + private static final int EVENT_POST_DIAL_CHARACTER = 119; + + // Singleton instance + private static final CallManager INSTANCE = new CallManager(); + + // list of registered phones, which are PhoneBase objs + private final ArrayList<Phone> mPhones; + + // list of supported ringing calls + private final ArrayList<Call> mRingingCalls; + + // list of supported background calls + private final ArrayList<Call> mBackgroundCalls; + + // list of supported foreground calls + private final ArrayList<Call> mForegroundCalls; + + // empty connection list + private final ArrayList<Connection> emptyConnections = new ArrayList<Connection>(); + + // default phone as the first phone registered, which is PhoneBase obj + private Phone mDefaultPhone; + + // state registrants + protected final RegistrantList mPreciseCallStateRegistrants + = new RegistrantList(); + + protected final RegistrantList mNewRingingConnectionRegistrants + = new RegistrantList(); + + protected final RegistrantList mIncomingRingRegistrants + = new RegistrantList(); + + protected final RegistrantList mDisconnectRegistrants + = new RegistrantList(); + + protected final RegistrantList mMmiRegistrants + = new RegistrantList(); + + protected final RegistrantList mUnknownConnectionRegistrants + = new RegistrantList(); + + protected final RegistrantList mRingbackToneRegistrants + = new RegistrantList(); + + protected final RegistrantList mInCallVoicePrivacyOnRegistrants + = new RegistrantList(); + + protected final RegistrantList mInCallVoicePrivacyOffRegistrants + = new RegistrantList(); + + protected final RegistrantList mCallWaitingRegistrants + = new RegistrantList(); + + protected final RegistrantList mDisplayInfoRegistrants + = new RegistrantList(); + + protected final RegistrantList mSignalInfoRegistrants + = new RegistrantList(); + + protected final RegistrantList mCdmaOtaStatusChangeRegistrants + = new RegistrantList(); + + protected final RegistrantList mResendIncallMuteRegistrants + = new RegistrantList(); + + protected final RegistrantList mMmiInitiateRegistrants + = new RegistrantList(); + + protected final RegistrantList mMmiCompleteRegistrants + = new RegistrantList(); + + protected final RegistrantList mEcmTimerResetRegistrants + = new RegistrantList(); + + protected final RegistrantList mSubscriptionInfoReadyRegistrants + = new RegistrantList(); + + protected final RegistrantList mSuppServiceFailedRegistrants + = new RegistrantList(); + + protected final RegistrantList mServiceStateChangedRegistrants + = new RegistrantList(); + + protected final RegistrantList mPostDialCharacterRegistrants + = new RegistrantList(); + + private CallManager() { + mPhones = new ArrayList<Phone>(); + mRingingCalls = new ArrayList<Call>(); + mBackgroundCalls = new ArrayList<Call>(); + mForegroundCalls = new ArrayList<Call>(); + mDefaultPhone = null; + } + + /** + * get singleton instance of CallManager + * @return CallManager + */ + public static CallManager getInstance() { + return INSTANCE; + } + + /** + * Get the corresponding PhoneBase obj + * + * @param phone a Phone object + * @return the corresponding PhoneBase obj in Phone if Phone + * is a PhoneProxy obj + * or the Phone itself if Phone is not a PhoneProxy obj + */ + private static Phone getPhoneBase(Phone phone) { + if (phone instanceof PhoneProxy) { + return phone.getForegroundCall().getPhone(); + } + return phone; + } + + /** + * Check if two phones refer to the same PhoneBase obj + * + * Note: PhoneBase, not PhoneProxy, is to be used inside of CallManager + * + * Both PhoneBase and PhoneProxy implement Phone interface, so + * they have same phone APIs, such as dial(). The real implementation, for + * example in GSM, is in GSMPhone as extend from PhoneBase, so that + * foregroundCall.getPhone() returns GSMPhone obj. On the other hand, + * PhoneFactory.getDefaultPhone() returns PhoneProxy obj, which has a class + * member of GSMPhone. + * + * So for phone returned by PhoneFacotry, which is used by PhoneApp, + * phone.getForegroundCall().getPhone() != phone + * but + * isSamePhone(phone, phone.getForegroundCall().getPhone()) == true + * + * @param p1 is the first Phone obj + * @param p2 is the second Phone obj + * @return true if p1 and p2 refer to the same phone + */ + public static boolean isSamePhone(Phone p1, Phone p2) { + return (getPhoneBase(p1) == getPhoneBase(p2)); + } + + /** + * Returns all the registered phone objects. + * @return all the registered phone objects. + */ + public List<Phone> getAllPhones() { + return Collections.unmodifiableList(mPhones); + } + + /** + * Get current coarse-grained voice call state. + * If the Call Manager has an active call and call waiting occurs, + * then the phone state is RINGING not OFFHOOK + * + */ + public PhoneConstants.State getState() { + PhoneConstants.State s = PhoneConstants.State.IDLE; + + for (Phone phone : mPhones) { + if (phone.getState() == PhoneConstants.State.RINGING) { + s = PhoneConstants.State.RINGING; + } else if (phone.getState() == PhoneConstants.State.OFFHOOK) { + if (s == PhoneConstants.State.IDLE) s = PhoneConstants.State.OFFHOOK; + } + } + return s; + } + + /** + * @return the service state of CallManager, which represents the + * highest priority state of all the service states of phones + * + * The priority is defined as + * + * STATE_IN_SERIVCE > STATE_OUT_OF_SERIVCE > STATE_EMERGENCY > STATE_POWER_OFF + * + */ + + public int getServiceState() { + int resultState = ServiceState.STATE_OUT_OF_SERVICE; + + for (Phone phone : mPhones) { + int serviceState = phone.getServiceState().getState(); + if (serviceState == ServiceState.STATE_IN_SERVICE) { + // IN_SERVICE has the highest priority + resultState = serviceState; + break; + } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE) { + // OUT_OF_SERVICE replaces EMERGENCY_ONLY and POWER_OFF + // Note: EMERGENCY_ONLY is not in use at this moment + if ( resultState == ServiceState.STATE_EMERGENCY_ONLY || + resultState == ServiceState.STATE_POWER_OFF) { + resultState = serviceState; + } + } else if (serviceState == ServiceState.STATE_EMERGENCY_ONLY) { + if (resultState == ServiceState.STATE_POWER_OFF) { + resultState = serviceState; + } + } + } + return resultState; + } + + /** + * Register phone to CallManager + * @param phone to be registered + * @return true if register successfully + */ + public boolean registerPhone(Phone phone) { + Phone basePhone = getPhoneBase(phone); + + if (basePhone != null && !mPhones.contains(basePhone)) { + + if (DBG) { + Log.d(LOG_TAG, "registerPhone(" + + phone.getPhoneName() + " " + phone + ")"); + } + + if (mPhones.isEmpty()) { + mDefaultPhone = basePhone; + } + mPhones.add(basePhone); + mRingingCalls.add(basePhone.getRingingCall()); + mBackgroundCalls.add(basePhone.getBackgroundCall()); + mForegroundCalls.add(basePhone.getForegroundCall()); + registerForPhoneStates(basePhone); + return true; + } + return false; + } + + /** + * unregister phone from CallManager + * @param phone to be unregistered + */ + public void unregisterPhone(Phone phone) { + Phone basePhone = getPhoneBase(phone); + + if (basePhone != null && mPhones.contains(basePhone)) { + + if (DBG) { + Log.d(LOG_TAG, "unregisterPhone(" + + phone.getPhoneName() + " " + phone + ")"); + } + + mPhones.remove(basePhone); + mRingingCalls.remove(basePhone.getRingingCall()); + mBackgroundCalls.remove(basePhone.getBackgroundCall()); + mForegroundCalls.remove(basePhone.getForegroundCall()); + unregisterForPhoneStates(basePhone); + if (basePhone == mDefaultPhone) { + if (mPhones.isEmpty()) { + mDefaultPhone = null; + } else { + mDefaultPhone = mPhones.get(0); + } + } + } + } + + /** + * return the default phone or null if no phone available + */ + public Phone getDefaultPhone() { + return mDefaultPhone; + } + + /** + * @return the phone associated with the foreground call + */ + public Phone getFgPhone() { + return getActiveFgCall().getPhone(); + } + + /** + * @return the phone associated with the background call + */ + public Phone getBgPhone() { + return getFirstActiveBgCall().getPhone(); + } + + /** + * @return the phone associated with the ringing call + */ + public Phone getRingingPhone() { + return getFirstActiveRingingCall().getPhone(); + } + + public void setAudioMode() { + Context context = getContext(); + if (context == null) return; + AudioManager audioManager = (AudioManager) + context.getSystemService(Context.AUDIO_SERVICE); + + // change the audio mode and request/abandon audio focus according to phone state, + // but only on audio mode transitions + switch (getState()) { + case RINGING: + if (audioManager.getMode() != AudioManager.MODE_RINGTONE) { + // only request audio focus if the ringtone is going to be heard + if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) { + if (VDBG) Log.d(LOG_TAG, "requestAudioFocus on STREAM_RING"); + audioManager.requestAudioFocusForCall(AudioManager.STREAM_RING, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } + audioManager.setMode(AudioManager.MODE_RINGTONE); + } + break; + case OFFHOOK: + Phone offhookPhone = getFgPhone(); + if (getActiveFgCallState() == Call.State.IDLE) { + // There is no active Fg calls, the OFFHOOK state + // is set by the Bg call. So set the phone to bgPhone. + offhookPhone = getBgPhone(); + } + + int newAudioMode = AudioManager.MODE_IN_CALL; + if (offhookPhone instanceof SipPhone) { + // enable IN_COMMUNICATION audio mode instead for sipPhone + newAudioMode = AudioManager.MODE_IN_COMMUNICATION; + } + if (audioManager.getMode() != newAudioMode) { + // request audio focus before setting the new mode + if (VDBG) Log.d(LOG_TAG, "requestAudioFocus on STREAM_VOICE_CALL"); + audioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + audioManager.setMode(newAudioMode); + } + break; + case IDLE: + if (audioManager.getMode() != AudioManager.MODE_NORMAL) { + audioManager.setMode(AudioManager.MODE_NORMAL); + if (VDBG) Log.d(LOG_TAG, "abandonAudioFocus"); + // abandon audio focus after the mode has been set back to normal + audioManager.abandonAudioFocusForCall(); + } + break; + } + } + + private Context getContext() { + Phone defaultPhone = getDefaultPhone(); + return ((defaultPhone == null) ? null : defaultPhone.getContext()); + } + + private void registerForPhoneStates(Phone phone) { + // for common events supported by all phones + phone.registerForPreciseCallStateChanged(mHandler, EVENT_PRECISE_CALL_STATE_CHANGED, null); + phone.registerForDisconnect(mHandler, EVENT_DISCONNECT, null); + phone.registerForNewRingingConnection(mHandler, EVENT_NEW_RINGING_CONNECTION, null); + phone.registerForUnknownConnection(mHandler, EVENT_UNKNOWN_CONNECTION, null); + phone.registerForIncomingRing(mHandler, EVENT_INCOMING_RING, null); + phone.registerForRingbackTone(mHandler, EVENT_RINGBACK_TONE, null); + phone.registerForInCallVoicePrivacyOn(mHandler, EVENT_IN_CALL_VOICE_PRIVACY_ON, null); + phone.registerForInCallVoicePrivacyOff(mHandler, EVENT_IN_CALL_VOICE_PRIVACY_OFF, null); + phone.registerForDisplayInfo(mHandler, EVENT_DISPLAY_INFO, null); + phone.registerForSignalInfo(mHandler, EVENT_SIGNAL_INFO, null); + phone.registerForResendIncallMute(mHandler, EVENT_RESEND_INCALL_MUTE, null); + phone.registerForMmiInitiate(mHandler, EVENT_MMI_INITIATE, null); + phone.registerForMmiComplete(mHandler, EVENT_MMI_COMPLETE, null); + phone.registerForSuppServiceFailed(mHandler, EVENT_SUPP_SERVICE_FAILED, null); + phone.registerForServiceStateChanged(mHandler, EVENT_SERVICE_STATE_CHANGED, null); + + // for events supported only by GSM and CDMA phone + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM || + phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + phone.setOnPostDialCharacter(mHandler, EVENT_POST_DIAL_CHARACTER, null); + } + + // for events supported only by CDMA phone + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA ){ + phone.registerForCdmaOtaStatusChange(mHandler, EVENT_CDMA_OTA_STATUS_CHANGE, null); + phone.registerForSubscriptionInfoReady(mHandler, EVENT_SUBSCRIPTION_INFO_READY, null); + phone.registerForCallWaiting(mHandler, EVENT_CALL_WAITING, null); + phone.registerForEcmTimerReset(mHandler, EVENT_ECM_TIMER_RESET, null); + } + } + + private void unregisterForPhoneStates(Phone phone) { + // for common events supported by all phones + phone.unregisterForPreciseCallStateChanged(mHandler); + phone.unregisterForDisconnect(mHandler); + phone.unregisterForNewRingingConnection(mHandler); + phone.unregisterForUnknownConnection(mHandler); + phone.unregisterForIncomingRing(mHandler); + phone.unregisterForRingbackTone(mHandler); + phone.unregisterForInCallVoicePrivacyOn(mHandler); + phone.unregisterForInCallVoicePrivacyOff(mHandler); + phone.unregisterForDisplayInfo(mHandler); + phone.unregisterForSignalInfo(mHandler); + phone.unregisterForResendIncallMute(mHandler); + phone.unregisterForMmiInitiate(mHandler); + phone.unregisterForMmiComplete(mHandler); + phone.unregisterForSuppServiceFailed(mHandler); + phone.unregisterForServiceStateChanged(mHandler); + + // for events supported only by GSM and CDMA phone + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM || + phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + phone.setOnPostDialCharacter(null, EVENT_POST_DIAL_CHARACTER, null); + } + + // for events supported only by CDMA phone + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA ){ + phone.unregisterForCdmaOtaStatusChange(mHandler); + phone.unregisterForSubscriptionInfoReady(mHandler); + phone.unregisterForCallWaiting(mHandler); + phone.unregisterForEcmTimerReset(mHandler); + } + } + + /** + * Answers a ringing or waiting call. + * + * Active call, if any, go on hold. + * If active call can't be held, i.e., a background call of the same channel exists, + * the active call will be hang up. + * + * Answering occurs asynchronously, and final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException when call is not ringing or waiting + */ + public void acceptCall(Call ringingCall) throws CallStateException { + Phone ringingPhone = ringingCall.getPhone(); + + if (VDBG) { + Log.d(LOG_TAG, "acceptCall(" +ringingCall + " from " + ringingCall.getPhone() + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if ( hasActiveFgCall() ) { + Phone activePhone = getActiveFgCall().getPhone(); + boolean hasBgCall = ! (activePhone.getBackgroundCall().isIdle()); + boolean sameChannel = (activePhone == ringingPhone); + + if (VDBG) { + Log.d(LOG_TAG, "hasBgCall: "+ hasBgCall + "sameChannel:" + sameChannel); + } + + if (sameChannel && hasBgCall) { + getActiveFgCall().hangup(); + } else if (!sameChannel && !hasBgCall) { + activePhone.switchHoldingAndActive(); + } else if (!sameChannel && hasBgCall) { + getActiveFgCall().hangup(); + } + } + + ringingPhone.acceptCall(); + + if (VDBG) { + Log.d(LOG_TAG, "End acceptCall(" +ringingCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Reject (ignore) a ringing call. In GSM, this means UDUB + * (User Determined User Busy). Reject occurs asynchronously, + * and final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException when no call is ringing or waiting + */ + public void rejectCall(Call ringingCall) throws CallStateException { + if (VDBG) { + Log.d(LOG_TAG, "rejectCall(" +ringingCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + Phone ringingPhone = ringingCall.getPhone(); + + ringingPhone.rejectCall(); + + if (VDBG) { + Log.d(LOG_TAG, "End rejectCall(" +ringingCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Places active call on hold, and makes held call active. + * Switch occurs asynchronously and may fail. + * + * There are 4 scenarios + * 1. only active call but no held call, aka, hold + * 2. no active call but only held call, aka, unhold + * 3. both active and held calls from same phone, aka, swap + * 4. active and held calls from different phones, aka, phone swap + * + * Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if active call is ringing, waiting, or + * dialing/alerting, or heldCall can't be active. + * In these cases, this operation may not be performed. + */ + public void switchHoldingAndActive(Call heldCall) throws CallStateException { + Phone activePhone = null; + Phone heldPhone = null; + + if (VDBG) { + Log.d(LOG_TAG, "switchHoldingAndActive(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + activePhone = getActiveFgCall().getPhone(); + } + + if (heldCall != null) { + heldPhone = heldCall.getPhone(); + } + + if (activePhone != null) { + activePhone.switchHoldingAndActive(); + } + + if (heldPhone != null && heldPhone != activePhone) { + heldPhone.switchHoldingAndActive(); + } + + if (VDBG) { + Log.d(LOG_TAG, "End switchHoldingAndActive(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Hangup foreground call and resume the specific background call + * + * Note: this is noop if there is no foreground call or the heldCall is null + * + * @param heldCall to become foreground + * @throws CallStateException + */ + public void hangupForegroundResumeBackground(Call heldCall) throws CallStateException { + Phone foregroundPhone = null; + Phone backgroundPhone = null; + + if (VDBG) { + Log.d(LOG_TAG, "hangupForegroundResumeBackground(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + foregroundPhone = getFgPhone(); + if (heldCall != null) { + backgroundPhone = heldCall.getPhone(); + if (foregroundPhone == backgroundPhone) { + getActiveFgCall().hangup(); + } else { + // the call to be hangup and resumed belongs to different phones + getActiveFgCall().hangup(); + switchHoldingAndActive(heldCall); + } + } + } + + if (VDBG) { + Log.d(LOG_TAG, "End hangupForegroundResumeBackground(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Whether or not the phone can conference in the current phone + * state--that is, one call holding and one call active. + * @return true if the phone can conference; false otherwise. + */ + public boolean canConference(Call heldCall) { + Phone activePhone = null; + Phone heldPhone = null; + + if (hasActiveFgCall()) { + activePhone = getActiveFgCall().getPhone(); + } + + if (heldCall != null) { + heldPhone = heldCall.getPhone(); + } + + return heldPhone.getClass().equals(activePhone.getClass()); + } + + /** + * Conferences holding and active. Conference occurs asynchronously + * and may fail. Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if canConference() would return false. + * In these cases, this operation may not be performed. + */ + public void conference(Call heldCall) throws CallStateException { + + if (VDBG) { + Log.d(LOG_TAG, "conference(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + + Phone fgPhone = getFgPhone(); + if (fgPhone instanceof SipPhone) { + ((SipPhone) fgPhone).conference(heldCall); + } else if (canConference(heldCall)) { + fgPhone.conference(); + } else { + throw(new CallStateException("Can't conference foreground and selected background call")); + } + + if (VDBG) { + Log.d(LOG_TAG, "End conference(" +heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + } + + /** + * Initiate a new voice connection. This happens asynchronously, so you + * cannot assume the audio path is connected (or a call index has been + * assigned) until PhoneStateChanged notification has occurred. + * + * @exception CallStateException if a new outgoing call is not currently + * possible because no more call slots exist or a call exists that is + * dialing, alerting, ringing, or waiting. Other errors are + * handled asynchronously. + */ + public Connection dial(Phone phone, String dialString) throws CallStateException { + Phone basePhone = getPhoneBase(phone); + Connection result; + + if (VDBG) { + Log.d(LOG_TAG, " dial(" + basePhone + ", "+ dialString + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (!canDial(phone)) { + throw new CallStateException("cannot dial in current state"); + } + + if ( hasActiveFgCall() ) { + Phone activePhone = getActiveFgCall().getPhone(); + boolean hasBgCall = !(activePhone.getBackgroundCall().isIdle()); + + if (DBG) { + Log.d(LOG_TAG, "hasBgCall: "+ hasBgCall + " sameChannel:" + (activePhone == basePhone)); + } + + if (activePhone != basePhone) { + if (hasBgCall) { + Log.d(LOG_TAG, "Hangup"); + getActiveFgCall().hangup(); + } else { + Log.d(LOG_TAG, "Switch"); + activePhone.switchHoldingAndActive(); + } + } + } + + result = basePhone.dial(dialString); + + if (VDBG) { + Log.d(LOG_TAG, "End dial(" + basePhone + ", "+ dialString + ")"); + Log.d(LOG_TAG, this.toString()); + } + + return result; + } + + /** + * Initiate a new voice connection. This happens asynchronously, so you + * cannot assume the audio path is connected (or a call index has been + * assigned) until PhoneStateChanged notification has occurred. + * + * @exception CallStateException if a new outgoing call is not currently + * possible because no more call slots exist or a call exists that is + * dialing, alerting, ringing, or waiting. Other errors are + * handled asynchronously. + */ + public Connection dial(Phone phone, String dialString, UUSInfo uusInfo) throws CallStateException { + return phone.dial(dialString, uusInfo); + } + + /** + * clear disconnect connection for each phone + */ + public void clearDisconnected() { + for(Phone phone : mPhones) { + phone.clearDisconnected(); + } + } + + /** + * Phone can make a call only if ALL of the following are true: + * - Phone is not powered off + * - There's no incoming or waiting call + * - There's available call slot in either foreground or background + * - The foreground call is ACTIVE or IDLE or DISCONNECTED. + * (We mainly need to make sure it *isn't* DIALING or ALERTING.) + * @param phone + * @return true if the phone can make a new call + */ + private boolean canDial(Phone phone) { + int serviceState = phone.getServiceState().getState(); + boolean hasRingingCall = hasActiveRingingCall(); + boolean hasActiveCall = hasActiveFgCall(); + boolean hasHoldingCall = hasActiveBgCall(); + boolean allLinesTaken = hasActiveCall && hasHoldingCall; + Call.State fgCallState = getActiveFgCallState(); + + boolean result = (serviceState != ServiceState.STATE_POWER_OFF + && !hasRingingCall + && !allLinesTaken + && ((fgCallState == Call.State.ACTIVE) + || (fgCallState == Call.State.IDLE) + || (fgCallState == Call.State.DISCONNECTED))); + + if (result == false) { + Log.d(LOG_TAG, "canDial serviceState=" + serviceState + + " hasRingingCall=" + hasRingingCall + + " hasActiveCall=" + hasActiveCall + + " hasHoldingCall=" + hasHoldingCall + + " allLinesTaken=" + allLinesTaken + + " fgCallState=" + fgCallState); + } + return result; + } + + /** + * Whether or not the phone can do explicit call transfer in the current + * phone state--that is, one call holding and one call active. + * @return true if the phone can do explicit call transfer; false otherwise. + */ + public boolean canTransfer(Call heldCall) { + Phone activePhone = null; + Phone heldPhone = null; + + if (hasActiveFgCall()) { + activePhone = getActiveFgCall().getPhone(); + } + + if (heldCall != null) { + heldPhone = heldCall.getPhone(); + } + + return (heldPhone == activePhone && activePhone.canTransfer()); + } + + /** + * Connects the held call and active call + * Disconnects the subscriber from both calls + * + * Explicit Call Transfer occurs asynchronously + * and may fail. Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if canTransfer() would return false. + * In these cases, this operation may not be performed. + */ + public void explicitCallTransfer(Call heldCall) throws CallStateException { + if (VDBG) { + Log.d(LOG_TAG, " explicitCallTransfer(" + heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (canTransfer(heldCall)) { + heldCall.getPhone().explicitCallTransfer(); + } + + if (VDBG) { + Log.d(LOG_TAG, "End explicitCallTransfer(" + heldCall + ")"); + Log.d(LOG_TAG, this.toString()); + } + + } + + /** + * Returns a list of MMI codes that are pending for a phone. (They have initiated + * but have not yet completed). + * Presently there is only ever one. + * + * Use <code>registerForMmiInitiate</code> + * and <code>registerForMmiComplete</code> for change notification. + * @return null if phone doesn't have or support mmi code + */ + public List<? extends MmiCode> getPendingMmiCodes(Phone phone) { + Log.e(LOG_TAG, "getPendingMmiCodes not implemented"); + return null; + } + + /** + * Sends user response to a USSD REQUEST message. An MmiCode instance + * representing this response is sent to handlers registered with + * registerForMmiInitiate. + * + * @param ussdMessge Message to send in the response. + * @return false if phone doesn't support ussd service + */ + public boolean sendUssdResponse(Phone phone, String ussdMessge) { + Log.e(LOG_TAG, "sendUssdResponse not implemented"); + return false; + } + + /** + * Mutes or unmutes the microphone for the active call. The microphone + * is automatically unmuted if a call is answered, dialed, or resumed + * from a holding state. + * + * @param muted true to mute the microphone, + * false to activate the microphone. + */ + + public void setMute(boolean muted) { + if (VDBG) { + Log.d(LOG_TAG, " setMute(" + muted + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + getActiveFgCall().getPhone().setMute(muted); + } + + if (VDBG) { + Log.d(LOG_TAG, "End setMute(" + muted + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Gets current mute status. Use + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()} + * as a change notifcation, although presently phone state changed is not + * fired when setMute() is called. + * + * @return true is muting, false is unmuting + */ + public boolean getMute() { + if (hasActiveFgCall()) { + return getActiveFgCall().getPhone().getMute(); + } else if (hasActiveBgCall()) { + return getFirstActiveBgCall().getPhone().getMute(); + } + return false; + } + + /** + * Enables or disables echo suppression. + */ + public void setEchoSuppressionEnabled(boolean enabled) { + if (VDBG) { + Log.d(LOG_TAG, " setEchoSuppression(" + enabled + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + getActiveFgCall().getPhone().setEchoSuppressionEnabled(enabled); + } + + if (VDBG) { + Log.d(LOG_TAG, "End setEchoSuppression(" + enabled + ")"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * Play a DTMF tone on the active call. + * + * @param c should be one of 0-9, '*' or '#'. Other values will be + * silently ignored. + * @return false if no active call or the active call doesn't support + * dtmf tone + */ + public boolean sendDtmf(char c) { + boolean result = false; + + if (VDBG) { + Log.d(LOG_TAG, " sendDtmf(" + c + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + getActiveFgCall().getPhone().sendDtmf(c); + result = true; + } + + if (VDBG) { + Log.d(LOG_TAG, "End sendDtmf(" + c + ")"); + Log.d(LOG_TAG, this.toString()); + } + return result; + } + + /** + * Start to paly a DTMF tone on the active call. + * or there is a playing DTMF tone. + * @param c should be one of 0-9, '*' or '#'. Other values will be + * silently ignored. + * + * @return false if no active call or the active call doesn't support + * dtmf tone + */ + public boolean startDtmf(char c) { + boolean result = false; + + if (VDBG) { + Log.d(LOG_TAG, " startDtmf(" + c + ")"); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) { + getActiveFgCall().getPhone().startDtmf(c); + result = true; + } + + if (VDBG) { + Log.d(LOG_TAG, "End startDtmf(" + c + ")"); + Log.d(LOG_TAG, this.toString()); + } + + return result; + } + + /** + * Stop the playing DTMF tone. Ignored if there is no playing DTMF + * tone or no active call. + */ + public void stopDtmf() { + if (VDBG) { + Log.d(LOG_TAG, " stopDtmf()" ); + Log.d(LOG_TAG, this.toString()); + } + + if (hasActiveFgCall()) getFgPhone().stopDtmf(); + + if (VDBG) { + Log.d(LOG_TAG, "End stopDtmf()"); + Log.d(LOG_TAG, this.toString()); + } + } + + /** + * send burst DTMF tone, it can send the string as single character or multiple character + * ignore if there is no active call or not valid digits string. + * Valid digit means only includes characters ISO-LATIN characters 0-9, *, # + * The difference between sendDtmf and sendBurstDtmf is sendDtmf only sends one character, + * this api can send single character and multiple character, also, this api has response + * back to caller. + * + * @param dtmfString is string representing the dialing digit(s) in the active call + * @param on the DTMF ON length in milliseconds, or 0 for default + * @param off the DTMF OFF length in milliseconds, or 0 for default + * @param onComplete is the callback message when the action is processed by BP + * + */ + public boolean sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) { + if (hasActiveFgCall()) { + getActiveFgCall().getPhone().sendBurstDtmf(dtmfString, on, off, onComplete); + return true; + } + return false; + } + + /** + * Notifies when a voice connection has disconnected, either due to local + * or remote hangup or error. + * + * Messages received from this will have the following members:<p> + * <ul><li>Message.obj will be an AsyncResult</li> + * <li>AsyncResult.userObj = obj</li> + * <li>AsyncResult.result = a Connection object that is + * no longer connected.</li></ul> + */ + public void registerForDisconnect(Handler h, int what, Object obj) { + mDisconnectRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for voice disconnection notification. + * Extraneous calls are tolerated silently + */ + public void unregisterForDisconnect(Handler h){ + mDisconnectRegistrants.remove(h); + } + + /** + * Register for getting notifications for change in the Call State {@link Call.State} + * This is called PreciseCallState because the call state is more precise than the + * {@link Phone.State} which can be obtained using the {@link PhoneStateListener} + * + * Resulting events will have an AsyncResult in <code>Message.obj</code>. + * AsyncResult.userData will be set to the obj argument here. + * The <em>h</em> parameter is held only by a weak reference. + */ + public void registerForPreciseCallStateChanged(Handler h, int what, Object obj){ + mPreciseCallStateRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for voice call state change notifications. + * Extraneous calls are tolerated silently. + */ + public void unregisterForPreciseCallStateChanged(Handler h){ + mPreciseCallStateRegistrants.remove(h); + } + + /** + * Notifies when a previously untracked non-ringing/waiting connection has appeared. + * This is likely due to some other entity (eg, SIM card application) initiating a call. + */ + public void registerForUnknownConnection(Handler h, int what, Object obj){ + mUnknownConnectionRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for unknown connection notifications. + */ + public void unregisterForUnknownConnection(Handler h){ + mUnknownConnectionRegistrants.remove(h); + } + + + /** + * Notifies when a new ringing or waiting connection has appeared.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = a Connection. <p> + * Please check Connection.isRinging() to make sure the Connection + * has not dropped since this message was posted. + * If Connection.isRinging() is true, then + * Connection.getCall() == Phone.getRingingCall() + */ + public void registerForNewRingingConnection(Handler h, int what, Object obj){ + mNewRingingConnectionRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for new ringing connection notification. + * Extraneous calls are tolerated silently + */ + + public void unregisterForNewRingingConnection(Handler h){ + mNewRingingConnectionRegistrants.remove(h); + } + + /** + * Notifies when an incoming call rings.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = a Connection. <p> + */ + public void registerForIncomingRing(Handler h, int what, Object obj){ + mIncomingRingRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for ring notification. + * Extraneous calls are tolerated silently + */ + + public void unregisterForIncomingRing(Handler h){ + mIncomingRingRegistrants.remove(h); + } + + /** + * Notifies when out-band ringback tone is needed.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = boolean, true to start play ringback tone + * and false to stop. <p> + */ + public void registerForRingbackTone(Handler h, int what, Object obj){ + mRingbackToneRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for ringback tone notification. + */ + + public void unregisterForRingbackTone(Handler h){ + mRingbackToneRegistrants.remove(h); + } + + /** + * Registers the handler to reset the uplink mute state to get + * uplink audio. + */ + public void registerForResendIncallMute(Handler h, int what, Object obj){ + mResendIncallMuteRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for resend incall mute notifications. + */ + public void unregisterForResendIncallMute(Handler h){ + mResendIncallMuteRegistrants.remove(h); + } + + /** + * Register for notifications of initiation of a new MMI code request. + * MMI codes for GSM are discussed in 3GPP TS 22.030.<p> + * + * Example: If Phone.dial is called with "*#31#", then the app will + * be notified here.<p> + * + * The returned <code>Message.obj</code> will contain an AsyncResult. + * + * <code>obj.result</code> will be an "MmiCode" object. + */ + public void registerForMmiInitiate(Handler h, int what, Object obj){ + mMmiInitiateRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for new MMI initiate notification. + * Extraneous calls are tolerated silently + */ + public void unregisterForMmiInitiate(Handler h){ + mMmiInitiateRegistrants.remove(h); + } + + /** + * Register for notifications that an MMI request has completed + * its network activity and is in its final state. This may mean a state + * of COMPLETE, FAILED, or CANCELLED. + * + * <code>Message.obj</code> will contain an AsyncResult. + * <code>obj.result</code> will be an "MmiCode" object + */ + public void registerForMmiComplete(Handler h, int what, Object obj){ + mMmiCompleteRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for MMI complete notification. + * Extraneous calls are tolerated silently + */ + public void unregisterForMmiComplete(Handler h){ + mMmiCompleteRegistrants.remove(h); + } + + /** + * Registration point for Ecm timer reset + * @param h handler to notify + * @param what user-defined message code + * @param obj placed in Message.obj + */ + public void registerForEcmTimerReset(Handler h, int what, Object obj){ + mEcmTimerResetRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notification for Ecm timer reset + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForEcmTimerReset(Handler h){ + mEcmTimerResetRegistrants.remove(h); + } + + /** + * Register for ServiceState changed. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a ServiceState instance + */ + public void registerForServiceStateChanged(Handler h, int what, Object obj){ + mServiceStateChangedRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for ServiceStateChange notification. + * Extraneous calls are tolerated silently + */ + public void unregisterForServiceStateChanged(Handler h){ + mServiceStateChangedRegistrants.remove(h); + } + + /** + * Register for notifications when a supplementary service attempt fails. + * Message.obj will contain an AsyncResult. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForSuppServiceFailed(Handler h, int what, Object obj){ + mSuppServiceFailedRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications when a supplementary service attempt fails. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForSuppServiceFailed(Handler h){ + mSuppServiceFailedRegistrants.remove(h); + } + + /** + * Register for notifications when a sInCall VoicePrivacy is enabled + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj){ + mInCallVoicePrivacyOnRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications when a sInCall VoicePrivacy is enabled + * + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForInCallVoicePrivacyOn(Handler h){ + mInCallVoicePrivacyOnRegistrants.remove(h); + } + + /** + * Register for notifications when a sInCall VoicePrivacy is disabled + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj){ + mInCallVoicePrivacyOffRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications when a sInCall VoicePrivacy is disabled + * + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForInCallVoicePrivacyOff(Handler h){ + mInCallVoicePrivacyOffRegistrants.remove(h); + } + + /** + * Register for notifications when CDMA call waiting comes + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForCallWaiting(Handler h, int what, Object obj){ + mCallWaitingRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications when CDMA Call waiting comes + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForCallWaiting(Handler h){ + mCallWaitingRegistrants.remove(h); + } + + + /** + * Register for signal information notifications from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a SuppServiceNotification instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + + public void registerForSignalInfo(Handler h, int what, Object obj){ + mSignalInfoRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for signal information notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForSignalInfo(Handler h){ + mSignalInfoRegistrants.remove(h); + } + + /** + * Register for display information notifications from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a SuppServiceNotification instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForDisplayInfo(Handler h, int what, Object obj){ + mDisplayInfoRegistrants.addUnique(h, what, obj); + } + + /** + * Unregisters for display information notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForDisplayInfo(Handler h) { + mDisplayInfoRegistrants.remove(h); + } + + /** + * Register for notifications when CDMA OTA Provision status change + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj){ + mCdmaOtaStatusChangeRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications when CDMA OTA Provision status change + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForCdmaOtaStatusChange(Handler h){ + mCdmaOtaStatusChangeRegistrants.remove(h); + } + + /** + * Registration point for subscription info ready + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj){ + mSubscriptionInfoReadyRegistrants.addUnique(h, what, obj); + } + + /** + * Unregister for notifications for subscription info + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForSubscriptionInfoReady(Handler h){ + mSubscriptionInfoReadyRegistrants.remove(h); + } + + /** + * Sets an event to be fired when the telephony system processes + * a post-dial character on an outgoing call.<p> + * + * Messages of type <code>what</code> will be sent to <code>h</code>. + * The <code>obj</code> field of these Message's will be instances of + * <code>AsyncResult</code>. <code>Message.obj.result</code> will be + * a Connection object.<p> + * + * Message.arg1 will be the post dial character being processed, + * or 0 ('\0') if end of string.<p> + * + * If Connection.getPostDialState() == WAIT, + * the application must call + * {@link com.android.internal.telephony.Connection#proceedAfterWaitChar() + * Connection.proceedAfterWaitChar()} or + * {@link com.android.internal.telephony.Connection#cancelPostDial() + * Connection.cancelPostDial()} + * for the telephony system to continue playing the post-dial + * DTMF sequence.<p> + * + * If Connection.getPostDialState() == WILD, + * the application must call + * {@link com.android.internal.telephony.Connection#proceedAfterWildChar + * Connection.proceedAfterWildChar()} + * or + * {@link com.android.internal.telephony.Connection#cancelPostDial() + * Connection.cancelPostDial()} + * for the telephony system to continue playing the + * post-dial DTMF sequence.<p> + * + */ + public void registerForPostDialCharacter(Handler h, int what, Object obj){ + mPostDialCharacterRegistrants.addUnique(h, what, obj); + } + + public void unregisterForPostDialCharacter(Handler h){ + mPostDialCharacterRegistrants.remove(h); + } + + /* APIs to access foregroudCalls, backgroudCalls, and ringingCalls + * 1. APIs to access list of calls + * 2. APIs to check if any active call, which has connection other than + * disconnected ones, pleaser refer to Call.isIdle() + * 3. APIs to return first active call + * 4. APIs to return the connections of first active call + * 5. APIs to return other property of first active call + */ + + /** + * @return list of all ringing calls + */ + public List<Call> getRingingCalls() { + return Collections.unmodifiableList(mRingingCalls); + } + + /** + * @return list of all foreground calls + */ + public List<Call> getForegroundCalls() { + return Collections.unmodifiableList(mForegroundCalls); + } + + /** + * @return list of all background calls + */ + public List<Call> getBackgroundCalls() { + return Collections.unmodifiableList(mBackgroundCalls); + } + + /** + * Return true if there is at least one active foreground call + */ + public boolean hasActiveFgCall() { + return (getFirstActiveCall(mForegroundCalls) != null); + } + + /** + * Return true if there is at least one active background call + */ + public boolean hasActiveBgCall() { + // TODO since hasActiveBgCall may get called often + // better to cache it to improve performance + return (getFirstActiveCall(mBackgroundCalls) != null); + } + + /** + * Return true if there is at least one active ringing call + * + */ + public boolean hasActiveRingingCall() { + return (getFirstActiveCall(mRingingCalls) != null); + } + + /** + * return the active foreground call from foreground calls + * + * Active call means the call is NOT in Call.State.IDLE + * + * 1. If there is active foreground call, return it + * 2. If there is no active foreground call, return the + * foreground call associated with default phone, which state is IDLE. + * 3. If there is no phone registered at all, return null. + * + */ + public Call getActiveFgCall() { + Call call = getFirstNonIdleCall(mForegroundCalls); + if (call == null) { + call = (mDefaultPhone == null) + ? null + : mDefaultPhone.getForegroundCall(); + } + return call; + } + + // Returns the first call that is not in IDLE state. If both active calls + // and disconnecting/disconnected calls exist, return the first active call. + private Call getFirstNonIdleCall(List<Call> calls) { + Call result = null; + for (Call call : calls) { + if (!call.isIdle()) { + return call; + } else if (call.getState() != Call.State.IDLE) { + if (result == null) result = call; + } + } + return result; + } + + /** + * return one active background call from background calls + * + * Active call means the call is NOT idle defined by Call.isIdle() + * + * 1. If there is only one active background call, return it + * 2. If there is more than one active background call, return the first one + * 3. If there is no active background call, return the background call + * associated with default phone, which state is IDLE. + * 4. If there is no background call at all, return null. + * + * Complete background calls list can be get by getBackgroundCalls() + */ + public Call getFirstActiveBgCall() { + Call call = getFirstNonIdleCall(mBackgroundCalls); + if (call == null) { + call = (mDefaultPhone == null) + ? null + : mDefaultPhone.getBackgroundCall(); + } + return call; + } + + /** + * return one active ringing call from ringing calls + * + * Active call means the call is NOT idle defined by Call.isIdle() + * + * 1. If there is only one active ringing call, return it + * 2. If there is more than one active ringing call, return the first one + * 3. If there is no active ringing call, return the ringing call + * associated with default phone, which state is IDLE. + * 4. If there is no ringing call at all, return null. + * + * Complete ringing calls list can be get by getRingingCalls() + */ + public Call getFirstActiveRingingCall() { + Call call = getFirstNonIdleCall(mRingingCalls); + if (call == null) { + call = (mDefaultPhone == null) + ? null + : mDefaultPhone.getRingingCall(); + } + return call; + } + + /** + * @return the state of active foreground call + * return IDLE if there is no active foreground call + */ + public Call.State getActiveFgCallState() { + Call fgCall = getActiveFgCall(); + + if (fgCall != null) { + return fgCall.getState(); + } + + return Call.State.IDLE; + } + + /** + * @return the connections of active foreground call + * return empty list if there is no active foreground call + */ + public List<Connection> getFgCallConnections() { + Call fgCall = getActiveFgCall(); + if ( fgCall != null) { + return fgCall.getConnections(); + } + return emptyConnections; + } + + /** + * @return the connections of active background call + * return empty list if there is no active background call + */ + public List<Connection> getBgCallConnections() { + Call bgCall = getFirstActiveBgCall(); + if ( bgCall != null) { + return bgCall.getConnections(); + } + return emptyConnections; + } + + /** + * @return the latest connection of active foreground call + * return null if there is no active foreground call + */ + public Connection getFgCallLatestConnection() { + Call fgCall = getActiveFgCall(); + if ( fgCall != null) { + return fgCall.getLatestConnection(); + } + return null; + } + + /** + * @return true if there is at least one Foreground call in disconnected state + */ + public boolean hasDisconnectedFgCall() { + return (getFirstCallOfState(mForegroundCalls, Call.State.DISCONNECTED) != null); + } + + /** + * @return true if there is at least one background call in disconnected state + */ + public boolean hasDisconnectedBgCall() { + return (getFirstCallOfState(mBackgroundCalls, Call.State.DISCONNECTED) != null); + } + + /** + * @return the first active call from a call list + */ + private Call getFirstActiveCall(ArrayList<Call> calls) { + for (Call call : calls) { + if (!call.isIdle()) { + return call; + } + } + return null; + } + + /** + * @return the first call in a the Call.state from a call list + */ + private Call getFirstCallOfState(ArrayList<Call> calls, Call.State state) { + for (Call call : calls) { + if (call.getState() == state) { + return call; + } + } + return null; + } + + + private boolean hasMoreThanOneRingingCall() { + int count = 0; + for (Call call : mRingingCalls) { + if (call.getState().isRinging()) { + if (++count > 1) return true; + } + } + return false; + } + + private Handler mHandler = new Handler() { + + @Override + public void handleMessage(Message msg) { + + switch (msg.what) { + case EVENT_DISCONNECT: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_DISCONNECT)"); + mDisconnectRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_PRECISE_CALL_STATE_CHANGED: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_PRECISE_CALL_STATE_CHANGED)"); + mPreciseCallStateRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_NEW_RINGING_CONNECTION: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_NEW_RINGING_CONNECTION)"); + if (getActiveFgCallState().isDialing() || hasMoreThanOneRingingCall()) { + Connection c = (Connection) ((AsyncResult) msg.obj).result; + try { + Log.d(LOG_TAG, "silently drop incoming call: " + c.getCall()); + c.getCall().hangup(); + } catch (CallStateException e) { + Log.w(LOG_TAG, "new ringing connection", e); + } + } else { + mNewRingingConnectionRegistrants.notifyRegistrants((AsyncResult) msg.obj); + } + break; + case EVENT_UNKNOWN_CONNECTION: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_UNKNOWN_CONNECTION)"); + mUnknownConnectionRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_INCOMING_RING: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_INCOMING_RING)"); + // The event may come from RIL who's not aware of an ongoing fg call + if (!hasActiveFgCall()) { + mIncomingRingRegistrants.notifyRegistrants((AsyncResult) msg.obj); + } + break; + case EVENT_RINGBACK_TONE: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_RINGBACK_TONE)"); + mRingbackToneRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_IN_CALL_VOICE_PRIVACY_ON: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_IN_CALL_VOICE_PRIVACY_ON)"); + mInCallVoicePrivacyOnRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_IN_CALL_VOICE_PRIVACY_OFF: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_IN_CALL_VOICE_PRIVACY_OFF)"); + mInCallVoicePrivacyOffRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_CALL_WAITING: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_CALL_WAITING)"); + mCallWaitingRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_DISPLAY_INFO: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_DISPLAY_INFO)"); + mDisplayInfoRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_SIGNAL_INFO: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_SIGNAL_INFO)"); + mSignalInfoRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_CDMA_OTA_STATUS_CHANGE: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_CDMA_OTA_STATUS_CHANGE)"); + mCdmaOtaStatusChangeRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_RESEND_INCALL_MUTE: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_RESEND_INCALL_MUTE)"); + mResendIncallMuteRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_MMI_INITIATE: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_MMI_INITIATE)"); + mMmiInitiateRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_MMI_COMPLETE: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_MMI_COMPLETE)"); + mMmiCompleteRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_ECM_TIMER_RESET: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_ECM_TIMER_RESET)"); + mEcmTimerResetRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_SUBSCRIPTION_INFO_READY: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_SUBSCRIPTION_INFO_READY)"); + mSubscriptionInfoReadyRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_SUPP_SERVICE_FAILED: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_SUPP_SERVICE_FAILED)"); + mSuppServiceFailedRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_SERVICE_STATE_CHANGED: + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_SERVICE_STATE_CHANGED)"); + mServiceStateChangedRegistrants.notifyRegistrants((AsyncResult) msg.obj); + break; + case EVENT_POST_DIAL_CHARACTER: + // we need send the character that is being processed in msg.arg1 + // so can't use notifyRegistrants() + if (VDBG) Log.d(LOG_TAG, " handleMessage (EVENT_POST_DIAL_CHARACTER)"); + for(int i=0; i < mPostDialCharacterRegistrants.size(); i++) { + Message notifyMsg; + notifyMsg = ((Registrant)mPostDialCharacterRegistrants.get(i)).messageForRegistrant(); + notifyMsg.obj = msg.obj; + notifyMsg.arg1 = msg.arg1; + notifyMsg.sendToTarget(); + } + break; + } + } + }; + + @Override + public String toString() { + Call call; + StringBuilder b = new StringBuilder(); + + b.append("CallManager {"); + b.append("\nstate = " + getState()); + call = getActiveFgCall(); + b.append("\n- Foreground: " + getActiveFgCallState()); + b.append(" from " + call.getPhone()); + b.append("\n Conn: ").append(getFgCallConnections()); + call = getFirstActiveBgCall(); + b.append("\n- Background: " + call.getState()); + b.append(" from " + call.getPhone()); + b.append("\n Conn: ").append(getBgCallConnections()); + call = getFirstActiveRingingCall(); + b.append("\n- Ringing: " +call.getState()); + b.append(" from " + call.getPhone()); + + for (Phone phone : getAllPhones()) { + if (phone != null) { + b.append("\nPhone: " + phone + ", name = " + phone.getPhoneName() + + ", state = " + phone.getState()); + call = phone.getForegroundCall(); + b.append("\n- Foreground: ").append(call); + call = phone.getBackgroundCall(); + b.append(" Background: ").append(call); + call = phone.getRingingCall(); + b.append(" Ringing: ").append(call); + } + } + b.append("\n}"); + return b.toString(); + } +} diff --git a/src/java/com/android/internal/telephony/CallStateException.java b/src/java/com/android/internal/telephony/CallStateException.java new file mode 100644 index 0000000..6087124 --- /dev/null +++ b/src/java/com/android/internal/telephony/CallStateException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class CallStateException extends Exception +{ + public + CallStateException() + { + } + + public + CallStateException(String string) + { + super(string); + } +} diff --git a/src/java/com/android/internal/telephony/CallTracker.java b/src/java/com/android/internal/telephony/CallTracker.java new file mode 100644 index 0000000..62caf01 --- /dev/null +++ b/src/java/com/android/internal/telephony/CallTracker.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telephony.CommandException; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + + +/** + * {@hide} + */ +public abstract class CallTracker extends Handler { + + private static final boolean DBG_POLL = false; + + //***** Constants + + static final int POLL_DELAY_MSEC = 250; + + protected int pendingOperations; + protected boolean needsPoll; + protected Message lastRelevantPoll; + + public CommandsInterface cm; + + + //***** Events + + protected static final int EVENT_POLL_CALLS_RESULT = 1; + protected static final int EVENT_CALL_STATE_CHANGE = 2; + protected static final int EVENT_REPOLL_AFTER_DELAY = 3; + protected static final int EVENT_OPERATION_COMPLETE = 4; + protected static final int EVENT_GET_LAST_CALL_FAIL_CAUSE = 5; + + protected static final int EVENT_SWITCH_RESULT = 8; + protected static final int EVENT_RADIO_AVAILABLE = 9; + protected static final int EVENT_RADIO_NOT_AVAILABLE = 10; + protected static final int EVENT_CONFERENCE_RESULT = 11; + protected static final int EVENT_SEPARATE_RESULT = 12; + protected static final int EVENT_ECT_RESULT = 13; + protected static final int EVENT_EXIT_ECM_RESPONSE_CDMA = 14; + protected static final int EVENT_CALL_WAITING_INFO_CDMA = 15; + protected static final int EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA = 16; + + protected void pollCallsWhenSafe() { + needsPoll = true; + + if (checkNoOperationsPending()) { + lastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT); + cm.getCurrentCalls(lastRelevantPoll); + } + } + + protected void + pollCallsAfterDelay() { + Message msg = obtainMessage(); + + msg.what = EVENT_REPOLL_AFTER_DELAY; + sendMessageDelayed(msg, POLL_DELAY_MSEC); + } + + protected boolean + isCommandExceptionRadioNotAvailable(Throwable e) { + return e != null && e instanceof CommandException + && ((CommandException)e).getCommandError() + == CommandException.Error.RADIO_NOT_AVAILABLE; + } + + protected abstract void handlePollCalls(AsyncResult ar); + + protected void handleRadioAvailable() { + pollCallsWhenSafe(); + } + + /** + * Obtain a complete message that indicates that this operation + * does not require polling of getCurrentCalls(). However, if other + * operations that do need getCurrentCalls() are pending or are + * scheduled while this operation is pending, the invocation + * of getCurrentCalls() will be postponed until this + * operation is also complete. + */ + protected Message + obtainNoPollCompleteMessage(int what) { + pendingOperations++; + lastRelevantPoll = null; + return obtainMessage(what); + } + + /** + * @return true if we're idle or there's a call to getCurrentCalls() pending + * but nothing else + */ + private boolean + checkNoOperationsPending() { + if (DBG_POLL) log("checkNoOperationsPending: pendingOperations=" + + pendingOperations); + return pendingOperations == 0; + } + + /** + * Routine called from dial to check if the number is a test Emergency number + * and if so remap the number. This allows a short emergency number to be remapped + * to a regular number for testing how the frameworks handles emergency numbers + * without actually calling an emergency number. + * + * This is not a full test and is not a substitute for testing real emergency + * numbers but can be useful. + * + * To use this feature set a system property ril.test.emergencynumber to a pair of + * numbers separated by a colon. If the first number matches the number parameter + * this routine returns the second number. Example: + * + * ril.test.emergencynumber=112:1-123-123-45678 + * + * To test Dial 112 take call then hang up on MO device to enter ECM + * see RIL#processSolicited RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND + * + * @param number to test if it should be remapped + * @return the same number or the remapped number. + */ + protected String checkForTestEmergencyNumber(String dialString) { + String testEn = SystemProperties.get("ril.test.emergencynumber"); + if (DBG_POLL) { + log("checkForTestEmergencyNumber: dialString=" + dialString + + " testEn=" + testEn); + } + if (!TextUtils.isEmpty(testEn)) { + String values[] = testEn.split(":"); + log("checkForTestEmergencyNumber: values.length=" + values.length); + if (values.length == 2) { + if (values[0].equals( + android.telephony.PhoneNumberUtils.stripSeparators(dialString))) { + cm.testingEmergencyCall(); + log("checkForTestEmergencyNumber: remap " + + dialString + " to " + values[1]); + dialString = values[1]; + } + } + } + return dialString; + } + + //***** Overridden from Handler + public abstract void handleMessage (Message msg); + public abstract void registerForVoiceCallStarted(Handler h, int what, Object obj); + public abstract void unregisterForVoiceCallStarted(Handler h); + public abstract void registerForVoiceCallEnded(Handler h, int what, Object obj); + public abstract void unregisterForVoiceCallEnded(Handler h); + + protected abstract void log(String msg); + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CallTracker:"); + pw.println(" pendingOperations=" + pendingOperations); + pw.println(" needsPoll=" + needsPoll); + pw.println(" lastRelevantPoll=" + lastRelevantPoll); + } +} diff --git a/src/java/com/android/internal/telephony/CommandException.java b/src/java/com/android/internal/telephony/CommandException.java new file mode 100644 index 0000000..94c544e --- /dev/null +++ b/src/java/com/android/internal/telephony/CommandException.java @@ -0,0 +1,98 @@ +/* + * 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 com.android.internal.telephony; + +import com.android.internal.telephony.RILConstants; + +import android.util.Log; + +/** + * {@hide} + */ +public class CommandException extends RuntimeException { + private Error e; + + public enum Error { + INVALID_RESPONSE, + RADIO_NOT_AVAILABLE, + GENERIC_FAILURE, + PASSWORD_INCORRECT, + SIM_PIN2, + SIM_PUK2, + REQUEST_NOT_SUPPORTED, + OP_NOT_ALLOWED_DURING_VOICE_CALL, + OP_NOT_ALLOWED_BEFORE_REG_NW, + SMS_FAIL_RETRY, + SIM_ABSENT, + SUBSCRIPTION_NOT_AVAILABLE, + MODE_NOT_SUPPORTED, + FDN_CHECK_FAILURE, + ILLEGAL_SIM_OR_ME, + } + + public CommandException(Error e) { + super(e.toString()); + this.e = e; + } + + public static CommandException + fromRilErrno(int ril_errno) { + switch(ril_errno) { + case RILConstants.SUCCESS: return null; + case RILConstants.RIL_ERRNO_INVALID_RESPONSE: + return new CommandException(Error.INVALID_RESPONSE); + case RILConstants.RADIO_NOT_AVAILABLE: + return new CommandException(Error.RADIO_NOT_AVAILABLE); + case RILConstants.GENERIC_FAILURE: + return new CommandException(Error.GENERIC_FAILURE); + case RILConstants.PASSWORD_INCORRECT: + return new CommandException(Error.PASSWORD_INCORRECT); + case RILConstants.SIM_PIN2: + return new CommandException(Error.SIM_PIN2); + case RILConstants.SIM_PUK2: + return new CommandException(Error.SIM_PUK2); + case RILConstants.REQUEST_NOT_SUPPORTED: + return new CommandException(Error.REQUEST_NOT_SUPPORTED); + case RILConstants.OP_NOT_ALLOWED_DURING_VOICE_CALL: + return new CommandException(Error.OP_NOT_ALLOWED_DURING_VOICE_CALL); + case RILConstants.OP_NOT_ALLOWED_BEFORE_REG_NW: + return new CommandException(Error.OP_NOT_ALLOWED_BEFORE_REG_NW); + case RILConstants.SMS_SEND_FAIL_RETRY: + return new CommandException(Error.SMS_FAIL_RETRY); + case RILConstants.SIM_ABSENT: + return new CommandException(Error.SIM_ABSENT); + case RILConstants.SUBSCRIPTION_NOT_AVAILABLE: + return new CommandException(Error.SUBSCRIPTION_NOT_AVAILABLE); + case RILConstants.MODE_NOT_SUPPORTED: + return new CommandException(Error.MODE_NOT_SUPPORTED); + case RILConstants.FDN_CHECK_FAILURE: + return new CommandException(Error.FDN_CHECK_FAILURE); + case RILConstants.ILLEGAL_SIM_OR_ME: + return new CommandException(Error.ILLEGAL_SIM_OR_ME); + default: + Log.e("GSM", "Unrecognized RIL errno " + ril_errno); + return new CommandException(Error.INVALID_RESPONSE); + } + } + + public Error getCommandError() { + return e; + } + + + +} diff --git a/src/java/com/android/internal/telephony/CommandsInterface.java b/src/java/com/android/internal/telephony/CommandsInterface.java new file mode 100644 index 0000000..f7757b3 --- /dev/null +++ b/src/java/com/android/internal/telephony/CommandsInterface.java @@ -0,0 +1,1579 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; + +import android.os.Message; +import android.os.Handler; +import android.util.Log; + +/** + * {@hide} + */ +public interface CommandsInterface { + enum RadioState { + RADIO_OFF, /* Radio explicitly powered off (eg CFUN=0) */ + RADIO_UNAVAILABLE, /* Radio unavailable (eg, resetting or not booted) */ + RADIO_ON; /* Radio is on */ + + public boolean isOn() /* and available...*/ { + return this == RADIO_ON; + } + + public boolean isAvailable() { + return this != RADIO_UNAVAILABLE; + } + } + + //***** Constants + + // Used as parameter to dial() and setCLIR() below + static final int CLIR_DEFAULT = 0; // "use subscription default value" + static final int CLIR_INVOCATION = 1; // (restrict CLI presentation) + static final int CLIR_SUPPRESSION = 2; // (allow CLI presentation) + + + // Used as parameters for call forward methods below + static final int CF_ACTION_DISABLE = 0; + static final int CF_ACTION_ENABLE = 1; +// static final int CF_ACTION_UNUSED = 2; + static final int CF_ACTION_REGISTRATION = 3; + static final int CF_ACTION_ERASURE = 4; + + static final int CF_REASON_UNCONDITIONAL = 0; + static final int CF_REASON_BUSY = 1; + static final int CF_REASON_NO_REPLY = 2; + static final int CF_REASON_NOT_REACHABLE = 3; + static final int CF_REASON_ALL = 4; + static final int CF_REASON_ALL_CONDITIONAL = 5; + + // Used for call barring methods below + static final String CB_FACILITY_BAOC = "AO"; + static final String CB_FACILITY_BAOIC = "OI"; + static final String CB_FACILITY_BAOICxH = "OX"; + static final String CB_FACILITY_BAIC = "AI"; + static final String CB_FACILITY_BAICr = "IR"; + static final String CB_FACILITY_BA_ALL = "AB"; + static final String CB_FACILITY_BA_MO = "AG"; + static final String CB_FACILITY_BA_MT = "AC"; + static final String CB_FACILITY_BA_SIM = "SC"; + static final String CB_FACILITY_BA_FD = "FD"; + + + // Used for various supp services apis + // See 27.007 +CCFC or +CLCK + static final int SERVICE_CLASS_NONE = 0; // no user input + static final int SERVICE_CLASS_VOICE = (1 << 0); + static final int SERVICE_CLASS_DATA = (1 << 1); //synonym for 16+32+64+128 + static final int SERVICE_CLASS_FAX = (1 << 2); + static final int SERVICE_CLASS_SMS = (1 << 3); + static final int SERVICE_CLASS_DATA_SYNC = (1 << 4); + static final int SERVICE_CLASS_DATA_ASYNC = (1 << 5); + static final int SERVICE_CLASS_PACKET = (1 << 6); + static final int SERVICE_CLASS_PAD = (1 << 7); + static final int SERVICE_CLASS_MAX = (1 << 7); // Max SERVICE_CLASS value + + // Numeric representation of string values returned + // by messages sent to setOnUSSD handler + static final int USSD_MODE_NOTIFY = 0; + static final int USSD_MODE_REQUEST = 1; + + // GSM SMS fail cause for acknowledgeLastIncomingSMS. From TS 23.040, 9.2.3.22. + static final int GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED = 0xD3; + static final int GSM_SMS_FAIL_CAUSE_USIM_APP_TOOLKIT_BUSY = 0xD4; + static final int GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR = 0xD5; + static final int GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR = 0xFF; + + // CDMA SMS fail cause for acknowledgeLastIncomingCdmaSms. From TS N.S0005, 6.5.2.125. + static final int CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID = 4; + static final int CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE = 35; + static final int CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM = 39; + static final int CDMA_SMS_FAIL_CAUSE_ENCODING_PROBLEM = 96; + + //***** Methods + RadioState getRadioState(); + + void getVoiceRadioTechnology(Message result); + + /** + * Fires on any RadioState transition + * Always fires immediately as well + * + * do not attempt to calculate transitions by storing getRadioState() values + * on previous invocations of this notification. Instead, use the other + * registration methods + */ + void registerForRadioStateChanged(Handler h, int what, Object obj); + void unregisterForRadioStateChanged(Handler h); + + void registerForVoiceRadioTechChanged(Handler h, int what, Object obj); + void unregisterForVoiceRadioTechChanged(Handler h); + + /** + * Fires on any transition into RadioState.isOn() + * Fires immediately if currently in that state + * In general, actions should be idempotent. State may change + * before event is received. + */ + void registerForOn(Handler h, int what, Object obj); + void unregisterForOn(Handler h); + + /** + * Fires on any transition out of RadioState.isAvailable() + * Fires immediately if currently in that state + * In general, actions should be idempotent. State may change + * before event is received. + */ + void registerForAvailable(Handler h, int what, Object obj); + void unregisterForAvailable(Handler h); + + /** + * Fires on any transition into !RadioState.isAvailable() + * Fires immediately if currently in that state + * In general, actions should be idempotent. State may change + * before event is received. + */ + void registerForNotAvailable(Handler h, int what, Object obj); + void unregisterForNotAvailable(Handler h); + + /** + * Fires on any transition into RADIO_OFF or !RadioState.isAvailable() + * Fires immediately if currently in that state + * In general, actions should be idempotent. State may change + * before event is received. + */ + void registerForOffOrNotAvailable(Handler h, int what, Object obj); + void unregisterForOffOrNotAvailable(Handler h); + + /** + * Fires on any change in ICC status + */ + void registerForIccStatusChanged(Handler h, int what, Object obj); + void unregisterForIccStatusChanged(Handler h); + + void registerForCallStateChanged(Handler h, int what, Object obj); + void unregisterForCallStateChanged(Handler h); + void registerForVoiceNetworkStateChanged(Handler h, int what, Object obj); + void unregisterForVoiceNetworkStateChanged(Handler h); + void registerForDataNetworkStateChanged(Handler h, int what, Object obj); + void unregisterForDataNetworkStateChanged(Handler h); + + /** InCall voice privacy notifications */ + void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj); + void unregisterForInCallVoicePrivacyOn(Handler h); + void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj); + void unregisterForInCallVoicePrivacyOff(Handler h); + + /** + * unlike the register* methods, there's only one new 3GPP format SMS handler. + * if you need to unregister, you should also tell the radio to stop + * sending SMS's to you (via AT+CNMI) + * + * AsyncResult.result is a String containing the SMS PDU + */ + void setOnNewGsmSms(Handler h, int what, Object obj); + void unSetOnNewGsmSms(Handler h); + + /** + * unlike the register* methods, there's only one new 3GPP2 format SMS handler. + * if you need to unregister, you should also tell the radio to stop + * sending SMS's to you (via AT+CNMI) + * + * AsyncResult.result is a String containing the SMS PDU + */ + void setOnNewCdmaSms(Handler h, int what, Object obj); + void unSetOnNewCdmaSms(Handler h); + + /** + * Set the handler for SMS Cell Broadcast messages. + * + * AsyncResult.result is a byte array containing the SMS-CB PDU + */ + void setOnNewGsmBroadcastSms(Handler h, int what, Object obj); + void unSetOnNewGsmBroadcastSms(Handler h); + + /** + * Register for NEW_SMS_ON_SIM unsolicited message + * + * AsyncResult.result is an int array containing the index of new SMS + */ + void setOnSmsOnSim(Handler h, int what, Object obj); + void unSetOnSmsOnSim(Handler h); + + /** + * Register for NEW_SMS_STATUS_REPORT unsolicited message + * + * AsyncResult.result is a String containing the status report PDU + */ + void setOnSmsStatus(Handler h, int what, Object obj); + void unSetOnSmsStatus(Handler h); + + /** + * unlike the register* methods, there's only one NITZ time handler + * + * AsyncResult.result is an Object[] + * ((Object[])AsyncResult.result)[0] is a String containing the NITZ time string + * ((Object[])AsyncResult.result)[1] is a Long containing the milliseconds since boot as + * returned by elapsedRealtime() when this NITZ time + * was posted. + * + * Please note that the delivery of this message may be delayed several + * seconds on system startup + */ + void setOnNITZTime(Handler h, int what, Object obj); + void unSetOnNITZTime(Handler h); + + /** + * unlike the register* methods, there's only one USSD notify handler + * + * Represents the arrival of a USSD "notify" message, which may + * or may not have been triggered by a previous USSD send + * + * AsyncResult.result is a String[] + * ((String[])(AsyncResult.result))[0] contains status code + * "0" USSD-Notify -- text in ((const char **)data)[1] + * "1" USSD-Request -- text in ((const char **)data)[1] + * "2" Session terminated by network + * "3" other local client (eg, SIM Toolkit) has responded + * "4" Operation not supported + * "5" Network timeout + * + * ((String[])(AsyncResult.result))[1] contains the USSD message + * The numeric representations of these are in USSD_MODE_* + */ + + void setOnUSSD(Handler h, int what, Object obj); + void unSetOnUSSD(Handler h); + + /** + * unlike the register* methods, there's only one signal strength handler + * AsyncResult.result is an int[2] + * response.obj.result[0] is received signal strength (0-31, 99) + * response.obj.result[1] is bit error rate (0-7, 99) + * as defined in TS 27.007 8.5 + */ + + void setOnSignalStrengthUpdate(Handler h, int what, Object obj); + void unSetOnSignalStrengthUpdate(Handler h); + + /** + * Sets the handler for SIM/RUIM SMS storage full unsolicited message. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnIccSmsFull(Handler h, int what, Object obj); + void unSetOnIccSmsFull(Handler h); + + /** + * Sets the handler for SIM Refresh notifications. + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForIccRefresh(Handler h, int what, Object obj); + void unregisterForIccRefresh(Handler h); + + void setOnIccRefresh(Handler h, int what, Object obj); + void unsetOnIccRefresh(Handler h); + + /** + * Sets the handler for RING notifications. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnCallRing(Handler h, int what, Object obj); + void unSetOnCallRing(Handler h); + + /** + * Sets the handler for RESTRICTED_STATE changed notification, + * eg, for Domain Specific Access Control + * unlike the register* methods, there's only one signal strength handler + * + * AsyncResult.result is an int[1] + * response.obj.result[0] is a bitmask of RIL_RESTRICTED_STATE_* values + */ + + void setOnRestrictedStateChanged(Handler h, int what, Object obj); + void unSetOnRestrictedStateChanged(Handler h); + + /** + * Sets the handler for Supplementary Service Notifications. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnSuppServiceNotification(Handler h, int what, Object obj); + void unSetOnSuppServiceNotification(Handler h); + + /** + * Sets the handler for Session End Notifications for CAT. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnCatSessionEnd(Handler h, int what, Object obj); + void unSetOnCatSessionEnd(Handler h); + + /** + * Sets the handler for Proactive Commands for CAT. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnCatProactiveCmd(Handler h, int what, Object obj); + void unSetOnCatProactiveCmd(Handler h); + + /** + * Sets the handler for Event Notifications for CAT. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnCatEvent(Handler h, int what, Object obj); + void unSetOnCatEvent(Handler h); + + /** + * Sets the handler for Call Set Up Notifications for CAT. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void setOnCatCallSetUp(Handler h, int what, Object obj); + void unSetOnCatCallSetUp(Handler h); + + /** + * Enables/disbables supplementary service related notifications from + * the network. + * + * @param enable true to enable notifications, false to disable. + * @param result Message to be posted when command completes. + */ + void setSuppServiceNotifications(boolean enable, Message result); + //void unSetSuppServiceNotifications(Handler h); + + /** + * Sets the handler for Event Notifications for CDMA Display Info. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForDisplayInfo(Handler h, int what, Object obj); + void unregisterForDisplayInfo(Handler h); + + /** + * Sets the handler for Event Notifications for CallWaiting Info. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForCallWaitingInfo(Handler h, int what, Object obj); + void unregisterForCallWaitingInfo(Handler h); + + /** + * Sets the handler for Event Notifications for Signal Info. + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForSignalInfo(Handler h, int what, Object obj); + void unregisterForSignalInfo(Handler h); + + /** + * Registers the handler for CDMA number information record + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForNumberInfo(Handler h, int what, Object obj); + void unregisterForNumberInfo(Handler h); + + /** + * Registers the handler for CDMA redirected number Information record + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForRedirectedNumberInfo(Handler h, int what, Object obj); + void unregisterForRedirectedNumberInfo(Handler h); + + /** + * Registers the handler for CDMA line control information record + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForLineControlInfo(Handler h, int what, Object obj); + void unregisterForLineControlInfo(Handler h); + + /** + * Registers the handler for CDMA T53 CLIR information record + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerFoT53ClirlInfo(Handler h, int what, Object obj); + void unregisterForT53ClirInfo(Handler h); + + /** + * Registers the handler for CDMA T53 audio control information record + * Unlike the register* methods, there's only one notification handler + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForT53AudioControlInfo(Handler h, int what, Object obj); + void unregisterForT53AudioControlInfo(Handler h); + + /** + * Fires on if Modem enters Emergency Callback mode + */ + void setEmergencyCallbackMode(Handler h, int what, Object obj); + + /** + * Fires on any CDMA OTA provision status change + */ + void registerForCdmaOtaProvision(Handler h,int what, Object obj); + void unregisterForCdmaOtaProvision(Handler h); + + /** + * Registers the handler when out-band ringback tone is needed.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = boolean. <p> + */ + void registerForRingbackTone(Handler h, int what, Object obj); + void unregisterForRingbackTone(Handler h); + + /** + * Registers the handler when mute/unmute need to be resent to get + * uplink audio during a call.<p> + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + * + */ + void registerForResendIncallMute(Handler h, int what, Object obj); + void unregisterForResendIncallMute(Handler h); + + /** + * Registers the handler for when Cdma subscription changed events + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + * + */ + void registerForCdmaSubscriptionChanged(Handler h, int what, Object obj); + void unregisterForCdmaSubscriptionChanged(Handler h); + + /** + * Registers the handler for when Cdma prl changed events + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + * + */ + void registerForCdmaPrlChanged(Handler h, int what, Object obj); + void unregisterForCdmaPrlChanged(Handler h); + + /** + * Registers the handler for when Cdma prl changed events + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + * + */ + void registerForExitEmergencyCallbackMode(Handler h, int what, Object obj); + void unregisterForExitEmergencyCallbackMode(Handler h); + + /** + * Registers the handler for RIL_UNSOL_RIL_CONNECT events. + * + * When ril connects or disconnects a message is sent to the registrant + * which contains an AsyncResult, ar, in msg.obj. The ar.result is an + * Integer which is the version of the ril or -1 if the ril disconnected. + * + * @param h Handler for notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForRilConnected(Handler h, int what, Object obj); + void unregisterForRilConnected(Handler h); + + /** + * Supply the ICC PIN to the ICC card + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPin(String pin, Message result); + + /** + * Supply the PIN for the app with this AID on the ICC card + * + * AID (Application ID), See ETSI 102.221 8.1 and 101.220 4 + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPinForApp(String pin, String aid, Message result); + + /** + * Supply the ICC PUK and newPin to the ICC card + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPuk(String puk, String newPin, Message result); + + /** + * Supply the PUK, new pin for the app with this AID on the ICC card + * + * AID (Application ID), See ETSI 102.221 8.1 and 101.220 4 + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPukForApp(String puk, String newPin, String aid, Message result); + + /** + * Supply the ICC PIN2 to the ICC card + * Only called following operation where ICC_PIN2 was + * returned as a a failure from a previous operation + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPin2(String pin2, Message result); + + /** + * Supply the PIN2 for the app with this AID on the ICC card + * Only called following operation where ICC_PIN2 was + * returned as a a failure from a previous operation + * + * AID (Application ID), See ETSI 102.221 8.1 and 101.220 4 + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPin2ForApp(String pin2, String aid, Message result); + + /** + * Supply the SIM PUK2 to the SIM card + * Only called following operation where SIM_PUK2 was + * returned as a a failure from a previous operation + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPuk2(String puk2, String newPin2, Message result); + + /** + * Supply the PUK2, newPin2 for the app with this AID on the ICC card + * Only called following operation where SIM_PUK2 was + * returned as a a failure from a previous operation + * + * AID (Application ID), See ETSI 102.221 8.1 and 101.220 4 + * + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * This exception is CommandException with an error of PASSWORD_INCORRECT + * if the password is incorrect + * + * ar.exception and ar.result are null on success + */ + + void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message result); + + void changeIccPin(String oldPin, String newPin, Message result); + void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message result); + void changeIccPin2(String oldPin2, String newPin2, Message result); + void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, Message result); + + void changeBarringPassword(String facility, String oldPwd, String newPwd, Message result); + + void supplyNetworkDepersonalization(String netpin, Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result contains a List of DriverCall + * The ar.result List is sorted by DriverCall.index + */ + void getCurrentCalls (Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result contains a List of DataCallState + * @deprecated Do not use. + */ + @Deprecated + void getPDPContextList(Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result contains a List of DataCallState + */ + void getDataCallList(Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + * + * CLIR_DEFAULT == on "use subscription default value" + * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation) + * CLIR_INVOCATION == on "CLIR invocation" (restrict CLI presentation) + */ + void dial (String address, int clirMode, Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + * + * CLIR_DEFAULT == on "use subscription default value" + * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation) + * CLIR_INVOCATION == on "CLIR invocation" (restrict CLI presentation) + */ + void dial(String address, int clirMode, UUSInfo uusInfo, Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is String containing IMSI on success + */ + void getIMSI(Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is String containing IMSI on success + */ + void getIMSIForApp(String aid, Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is String containing IMEI on success + */ + void getIMEI(Message result); + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is String containing IMEISV on success + */ + void getIMEISV(Message result); + + /** + * Hang up one individual connection. + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + * + * 3GPP 22.030 6.5.5 + * "Releases a specific active call X" + */ + void hangupConnection (int gsmIndex, Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Releases all held calls or sets User Determined User Busy (UDUB) + * for a waiting call." + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void hangupWaitingOrBackground (Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Releases all active calls (if any exist) and accepts + * the other (held or waiting) call." + * + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void hangupForegroundResumeBackground (Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Places all active calls (if any exist) on hold and accepts + * the other (held or waiting) call." + * + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void switchWaitingOrHoldingAndActive (Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Adds a held call to the conversation" + * + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void conference (Message result); + + /** + * Set preferred Voice Privacy (VP). + * + * @param enable true is enhanced and false is normal VP + * @param result is a callback message + */ + void setPreferredVoicePrivacy(boolean enable, Message result); + + /** + * Get currently set preferred Voice Privacy (VP) mode. + * + * @param result is a callback message + */ + void getPreferredVoicePrivacy(Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Places all active calls on hold except call X with which + * communication shall be supported." + */ + void separateConnection (int gsmIndex, Message result); + + /** + * + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void acceptCall (Message result); + + /** + * also known as UDUB + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void rejectCall (Message result); + + /** + * 3GPP 22.030 6.5.5 + * "Connects the two calls and disconnects the subscriber from both calls" + * + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void explicitCallTransfer (Message result); + + /** + * cause code returned as int[0] in Message.obj.response + * Returns integer cause code defined in TS 24.008 + * Annex H or closest approximation. + * Most significant codes: + * - Any defined in 22.001 F.4 (for generating busy/congestion) + * - Cause 68: ACM >= ACMMax + */ + void getLastCallFailCause (Message result); + + + /** + * Reason for last PDP context deactivate or failure to activate + * cause code returned as int[0] in Message.obj.response + * returns an integer cause code defined in TS 24.008 + * section 6.1.3.1.3 or close approximation + * @deprecated Do not use. + */ + @Deprecated + void getLastPdpFailCause (Message result); + + /** + * The preferred new alternative to getLastPdpFailCause + * that is also CDMA-compatible. + */ + void getLastDataCallFailCause (Message result); + + void setMute (boolean enableMute, Message response); + + void getMute (Message response); + + /** + * response.obj is an AsyncResult + * response.obj.result is an int[2] + * response.obj.result[0] is received signal strength (0-31, 99) + * response.obj.result[1] is bit error rate (0-7, 99) + * as defined in TS 27.007 8.5 + */ + void getSignalStrength (Message response); + + + /** + * response.obj.result is an int[3] + * response.obj.result[0] is registration state 0-5 from TS 27.007 7.2 + * response.obj.result[1] is LAC if registered or -1 if not + * response.obj.result[2] is CID if registered or -1 if not + * valid LAC and CIDs are 0x0000 - 0xffff + * + * Please note that registration state 4 ("unknown") is treated + * as "out of service" above + */ + void getVoiceRegistrationState (Message response); + + /** + * response.obj.result is an int[3] + * response.obj.result[0] is registration state 0-5 from TS 27.007 7.2 + * response.obj.result[1] is LAC if registered or -1 if not + * response.obj.result[2] is CID if registered or -1 if not + * valid LAC and CIDs are 0x0000 - 0xffff + * + * Please note that registration state 4 ("unknown") is treated + * as "out of service" above + */ + void getDataRegistrationState (Message response); + + /** + * response.obj.result is a String[3] + * response.obj.result[0] is long alpha or null if unregistered + * response.obj.result[1] is short alpha or null if unregistered + * response.obj.result[2] is numeric or null if unregistered + */ + void getOperator(Message response); + + /** + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void sendDtmf(char c, Message result); + + + /** + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void startDtmf(char c, Message result); + + /** + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void stopDtmf(Message result); + + /** + * ar.exception carries exception on failure + * ar.userObject contains the orignal value of result.obj + * ar.result is null on success and failure + */ + void sendBurstDtmf(String dtmfString, int on, int off, Message result); + + /** + * smscPDU is smsc address in PDU form GSM BCD format prefixed + * by a length byte (as expected by TS 27.005) or NULL for default SMSC + * pdu is SMS in PDU format as an ASCII hex string + * less the SMSC address + */ + void sendSMS (String smscPDU, String pdu, Message response); + + /** + * @param pdu is CDMA-SMS in internal pseudo-PDU format + * @param response sent when operation completes + */ + void sendCdmaSms(byte[] pdu, Message response); + + /** + * Deletes the specified SMS record from SIM memory (EF_SMS). + * + * @param index index of the SMS record to delete + * @param response sent when operation completes + */ + void deleteSmsOnSim(int index, Message response); + + /** + * Deletes the specified SMS record from RUIM memory (EF_SMS in DF_CDMA). + * + * @param index index of the SMS record to delete + * @param response sent when operation completes + */ + void deleteSmsOnRuim(int index, Message response); + + /** + * Writes an SMS message to SIM memory (EF_SMS). + * + * @param status status of message on SIM. One of: + * SmsManger.STATUS_ON_ICC_READ + * SmsManger.STATUS_ON_ICC_UNREAD + * SmsManger.STATUS_ON_ICC_SENT + * SmsManger.STATUS_ON_ICC_UNSENT + * @param pdu message PDU, as hex string + * @param response sent when operation completes. + * response.obj will be an AsyncResult, and will indicate + * any error that may have occurred (eg, out of memory). + */ + void writeSmsToSim(int status, String smsc, String pdu, Message response); + + void writeSmsToRuim(int status, String pdu, Message response); + + void setRadioPower(boolean on, Message response); + + void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message response); + + void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message response); + + /** + * Acknowledge successful or failed receipt of last incoming SMS, + * including acknowledgement TPDU to send as the RP-User-Data element + * of the RP-ACK or RP-ERROR PDU. + * + * @param success true to send RP-ACK, false to send RP-ERROR + * @param ackPdu the acknowledgement TPDU in hexadecimal format + * @param response sent when operation completes. + */ + void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, Message response); + + /** + * parameters equivalent to 27.007 AT+CRSM command + * response.obj will be an AsyncResult + * response.obj.result will be an IccIoResult on success + */ + void iccIO (int command, int fileid, String path, int p1, int p2, int p3, + String data, String pin2, Message response); + + /** + * parameters equivalent to 27.007 AT+CRSM command + * response.obj will be an AsyncResult + * response.obj.userObj will be a IccIoResult on success + */ + void iccIOForApp (int command, int fileid, String path, int p1, int p2, int p3, + String data, String pin2, String aid, Message response); + + /** + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * 1 for "CLIP is provisioned", and 0 for "CLIP is not provisioned". + * + * @param response is callback message + */ + + void queryCLIP(Message response); + + /** + * response.obj will be a an int[2] + * + * response.obj[0] will be TS 27.007 +CLIR parameter 'n' + * 0 presentation indicator is used according to the subscription of the CLIR service + * 1 CLIR invocation + * 2 CLIR suppression + * + * response.obj[1] will be TS 27.007 +CLIR parameter 'm' + * 0 CLIR not provisioned + * 1 CLIR provisioned in permanent mode + * 2 unknown (e.g. no network, etc.) + * 3 CLIR temporary mode presentation restricted + * 4 CLIR temporary mode presentation allowed + */ + + void getCLIR(Message response); + + /** + * clirMode is one of the CLIR_* constants above + * + * response.obj is null + */ + + void setCLIR(int clirMode, Message response); + + /** + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * 0 for disabled, 1 for enabled. + * + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + + void queryCallWaiting(int serviceClass, Message response); + + /** + * @param enable is true to enable, false to disable + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + + void setCallWaiting(boolean enable, int serviceClass, Message response); + + /** + * @param action is one of CF_ACTION_* + * @param cfReason is one of CF_REASON_* + * @param serviceClass is a sum of SERVICE_CLASSS_* + */ + void setCallForward(int action, int cfReason, int serviceClass, + String number, int timeSeconds, Message response); + + /** + * cfReason is one of CF_REASON_* + * + * ((AsyncResult)response.obj).result will be an array of + * CallForwardInfo's + * + * An array of length 0 means "disabled for all codes" + */ + void queryCallForwardStatus(int cfReason, int serviceClass, + String number, Message response); + + void setNetworkSelectionModeAutomatic(Message response); + + void setNetworkSelectionModeManual(String operatorNumeric, Message response); + + /** + * Queries whether the current network selection mode is automatic + * or manual + * + * ((AsyncResult)response.obj).result is an int[] with element [0] being + * a 0 for automatic selection and a 1 for manual selection + */ + + void getNetworkSelectionMode(Message response); + + /** + * Queries the currently available networks + * + * ((AsyncResult)response.obj).result is a List of NetworkInfo objects + */ + void getAvailableNetworks(Message response); + + void getBasebandVersion (Message response); + + + /** + * (AsyncResult)response.obj).result will be an Integer representing + * the sum of enabled service classes (sum of SERVICE_CLASS_*) + * + * @param facility one of CB_FACILTY_* + * @param password password or "" if not required + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + + void queryFacilityLock (String facility, String password, int serviceClass, + Message response); + + /** + * (AsyncResult)response.obj).result will be an Integer representing + * the sum of enabled service classes (sum of SERVICE_CLASS_*) for the + * application with appId. + * + * @param facility one of CB_FACILTY_* + * @param password password or "" if not required + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param appId is application Id or null if none + * @param response is callback message + */ + + void queryFacilityLockForApp(String facility, String password, int serviceClass, String appId, + Message response); + + /** + * @param facility one of CB_FACILTY_* + * @param lockState true means lock, false means unlock + * @param password password or "" if not required + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + void setFacilityLock (String facility, boolean lockState, String password, + int serviceClass, Message response); + + /** + * Set the facility lock for the app with this AID on the ICC card. + * + * @param facility one of CB_FACILTY_* + * @param lockState true means lock, false means unlock + * @param password password or "" if not required + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param appId is application Id or null if none + * @param response is callback message + */ + void setFacilityLockForApp(String facility, boolean lockState, String password, + int serviceClass, String appId, Message response); + + void sendUSSD (String ussdString, Message response); + + /** + * Cancels a pending USSD session if one exists. + * @param response callback message + */ + void cancelPendingUssd (Message response); + + void resetRadio(Message result); + + /** + * Assign a specified band for RF configuration. + * + * @param bandMode one of BM_*_BAND + * @param response is callback message + */ + void setBandMode (int bandMode, Message response); + + /** + * Query the list of band mode supported by RF. + * + * @param response is callback message + * ((AsyncResult)response.obj).result is an int[] with every + * element representing one avialable BM_*_BAND + */ + void queryAvailableBandMode (Message response); + + /** + * Set the current preferred network type. This will be the last + * networkType that was passed to setPreferredNetworkType. + */ + void setCurrentPreferredNetworkType(); + + /** + * Requests to set the preferred network type for searching and registering + * (CS/PS domain, RAT, and operation mode) + * @param networkType one of NT_*_TYPE + * @param response is callback message + */ + void setPreferredNetworkType(int networkType , Message response); + + /** + * Query the preferred network type setting + * + * @param response is callback message to report one of NT_*_TYPE + */ + void getPreferredNetworkType(Message response); + + /** + * Query neighboring cell ids + * + * @param response s callback message to cell ids + */ + void getNeighboringCids(Message response); + + /** + * Request to enable/disable network state change notifications when + * location information (lac and/or cid) has changed. + * + * @param enable true to enable, false to disable + * @param response callback message + */ + void setLocationUpdates(boolean enable, Message response); + + /** + * Gets the default SMSC address. + * + * @param result Callback message contains the SMSC address. + */ + void getSmscAddress(Message result); + + /** + * Sets the default SMSC address. + * + * @param address new SMSC address + * @param result Callback message is empty on completion + */ + void setSmscAddress(String address, Message result); + + /** + * Indicates whether there is storage available for new SMS messages. + * @param available true if storage is available + * @param result callback message + */ + void reportSmsMemoryStatus(boolean available, Message result); + + /** + * Indicates to the vendor ril that StkService is running + * and is ready to receive RIL_UNSOL_STK_XXXX commands. + * + * @param result callback message + */ + void reportStkServiceIsRunning(Message result); + + void invokeOemRilRequestRaw(byte[] data, Message response); + + void invokeOemRilRequestStrings(String[] strings, Message response); + + + /** + * Send TERMINAL RESPONSE to the SIM, after processing a proactive command + * sent by the SIM. + * + * @param contents String containing SAT/USAT response in hexadecimal + * format starting with first byte of response data. See + * TS 102 223 for details. + * @param response Callback message + */ + public void sendTerminalResponse(String contents, Message response); + + /** + * Send ENVELOPE to the SIM, after processing a proactive command sent by + * the SIM. + * + * @param contents String containing SAT/USAT response in hexadecimal + * format starting with command tag. See TS 102 223 for + * details. + * @param response Callback message + */ + public void sendEnvelope(String contents, Message response); + + /** + * Send ENVELOPE to the SIM, such as an SMS-PP data download envelope + * for a SIM data download message. This method has one difference + * from {@link #sendEnvelope}: The SW1 and SW2 status bytes from the UICC response + * are returned along with the response data. + * + * response.obj will be an AsyncResult + * response.obj.result will be an IccIoResult on success + * + * @param contents String containing SAT/USAT response in hexadecimal + * format starting with command tag. See TS 102 223 for + * details. + * @param response Callback message + */ + public void sendEnvelopeWithStatus(String contents, Message response); + + /** + * Accept or reject the call setup request from SIM. + * + * @param accept true if the call is to be accepted, false otherwise. + * @param response Callback message + */ + public void handleCallSetupRequestFromSim(boolean accept, Message response); + + /** + * Activate or deactivate cell broadcast SMS for GSM. + * + * @param activate + * true = activate, false = deactivate + * @param result Callback message is empty on completion + */ + public void setGsmBroadcastActivation(boolean activate, Message result); + + /** + * Configure cell broadcast SMS for GSM. + * + * @param response Callback message is empty on completion + */ + public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response); + + /** + * Query the current configuration of cell broadcast SMS of GSM. + * + * @param response + * Callback message contains the configuration from the modem + * on completion + */ + public void getGsmBroadcastConfig(Message response); + + //***** new Methods for CDMA support + + /** + * Request the device ESN / MEID / IMEI / IMEISV. + * "response" is const char ** + * [0] is IMEI if GSM subscription is available + * [1] is IMEISV if GSM subscription is available + * [2] is ESN if CDMA subscription is available + * [3] is MEID if CDMA subscription is available + */ + public void getDeviceIdentity(Message response); + + /** + * Request the device MDN / H_SID / H_NID / MIN. + * "response" is const char ** + * [0] is MDN if CDMA subscription is available + * [1] is a comma separated list of H_SID (Home SID) in decimal format + * if CDMA subscription is available + * [2] is a comma separated list of H_NID (Home NID) in decimal format + * if CDMA subscription is available + * [3] is MIN (10 digits, MIN2+MIN1) if CDMA subscription is available + */ + public void getCDMASubscription(Message response); + + /** + * Send Flash Code. + * "response" is is NULL + * [0] is a FLASH string + */ + public void sendCDMAFeatureCode(String FeatureCode, Message response); + + /** Set the Phone type created */ + void setPhoneType(int phoneType); + + /** + * Query the CDMA roaming preference setting + * + * @param response is callback message to report one of CDMA_RM_* + */ + void queryCdmaRoamingPreference(Message response); + + /** + * Requests to set the CDMA roaming preference + * @param cdmaRoamingType one of CDMA_RM_* + * @param response is callback message + */ + void setCdmaRoamingPreference(int cdmaRoamingType, Message response); + + /** + * Requests to set the CDMA subscription mode + * @param cdmaSubscriptionType one of CDMA_SUBSCRIPTION_* + * @param response is callback message + */ + void setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response); + + /** + * Requests to get the CDMA subscription srouce + * @param response is callback message + */ + void getCdmaSubscriptionSource(Message response); + + /** + * Set the TTY mode + * + * @param ttyMode one of the following: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * @param response is callback message + */ + void setTTYMode(int ttyMode, Message response); + + /** + * Query the TTY mode + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * tty mode: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * @param response is callback message + */ + void queryTTYMode(Message response); + + /** + * Setup a packet data connection On successful completion, the result + * message will return a {@link DataCallState} object containing the connection + * information. + * + * @param radioTechnology + * indicates whether to setup connection on radio technology CDMA + * (0) or GSM/UMTS (1) + * @param profile + * Profile Number or NULL to indicate default profile + * @param apn + * the APN to connect to if radio technology is GSM/UMTS. + * Otherwise null for CDMA. + * @param user + * the username for APN, or NULL + * @param password + * the password for APN, or NULL + * @param authType + * the PAP / CHAP auth type. Values is one of SETUP_DATA_AUTH_* + * @param protocol + * one of the PDP_type values in TS 27.007 section 10.1.1. + * For example, "IP", "IPV6", "IPV4V6", or "PPP". + * @param result + * Callback message + */ + public void setupDataCall(String radioTechnology, String profile, + String apn, String user, String password, String authType, + String protocol, Message result); + + /** + * Deactivate packet data connection + * + * @param cid + * The connection ID + * @param reason + * Data disconnect reason. + * @param result + * Callback message is empty on completion + */ + public void deactivateDataCall(int cid, int reason, Message result); + + /** + * Activate or deactivate cell broadcast SMS for CDMA. + * + * @param activate + * true = activate, false = deactivate + * @param result + * Callback message is empty on completion + */ + public void setCdmaBroadcastActivation(boolean activate, Message result); + + /** + * Configure cdma cell broadcast SMS. + * + * @param result + * Callback message is empty on completion + */ + // TODO: Change the configValuesArray to a RIL_BroadcastSMSConfig + public void setCdmaBroadcastConfig(int[] configValuesArray, Message result); + + /** + * Query the current configuration of cdma cell broadcast SMS. + * + * @param result + * Callback message contains the configuration from the modem on completion + */ + public void getCdmaBroadcastConfig(Message result); + + /** + * Requests the radio's system selection module to exit emergency callback mode. + * This function should only be called from CDMAPHone.java. + * + * @param response callback message + */ + public void exitEmergencyCallbackMode(Message response); + + /** + * Request the status of the ICC and UICC cards. + * + * @param result + * Callback message containing {@link IccCardStatus} structure for the card. + */ + public void getIccCardStatus(Message result); + + /** + * Return if the current radio is LTE on CDMA. This + * is a tri-state return value as for a period of time + * the mode may be unknown. + * + * @return {@link Phone#LTE_ON_CDMA_UNKNOWN}, {@link Phone#LTE_ON_CDMA_FALSE} + * or {@link Phone#LTE_ON_CDMA_TRUE} + */ + public int getLteOnCdmaMode(); + + /** + * Request the ISIM application on the UICC to perform the AKA + * challenge/response algorithm for IMS authentication. The nonce string + * and challenge response are Base64 encoded Strings. + * + * @param nonce the nonce string to pass with the ISIM authentication request + * @param response a callback message with the String response in the obj field + */ + public void requestIsimAuthentication(String nonce, Message response); + + /** + * Notifiy that we are testing an emergency call + */ + public void testingEmergencyCall(); +} diff --git a/src/java/com/android/internal/telephony/Connection.java b/src/java/com/android/internal/telephony/Connection.java new file mode 100644 index 0000000..554d974 --- /dev/null +++ b/src/java/com/android/internal/telephony/Connection.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.util.Log; + +/** + * {@hide} + */ +public abstract class Connection { + + //Caller Name Display + protected String cnapName; + protected int cnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED; + + private static String LOG_TAG = "TelephonyConnection"; + + public enum DisconnectCause { + NOT_DISCONNECTED, /* has not yet disconnected */ + INCOMING_MISSED, /* an incoming call that was missed and never answered */ + NORMAL, /* normal; remote */ + LOCAL, /* normal; local hangup */ + BUSY, /* outgoing call to busy line */ + CONGESTION, /* outgoing call to congested network */ + MMI, /* not presently used; dial() returns null */ + INVALID_NUMBER, /* invalid dial string */ + NUMBER_UNREACHABLE, /* cannot reach the peer */ + SERVER_UNREACHABLE, /* cannot reach the server */ + INVALID_CREDENTIALS, /* invalid credentials */ + OUT_OF_NETWORK, /* calling from out of network is not allowed */ + SERVER_ERROR, /* server error */ + TIMED_OUT, /* client timed out */ + LOST_SIGNAL, + LIMIT_EXCEEDED, /* eg GSM ACM limit exceeded */ + INCOMING_REJECTED, /* an incoming call that was rejected */ + POWER_OFF, /* radio is turned off explicitly */ + OUT_OF_SERVICE, /* out of service */ + ICC_ERROR, /* No ICC, ICC locked, or other ICC error */ + CALL_BARRED, /* call was blocked by call barring */ + FDN_BLOCKED, /* call was blocked by fixed dial number */ + CS_RESTRICTED, /* call was blocked by restricted all voice access */ + CS_RESTRICTED_NORMAL, /* call was blocked by restricted normal voice access */ + CS_RESTRICTED_EMERGENCY, /* call was blocked by restricted emergency voice access */ + UNOBTAINABLE_NUMBER, /* Unassigned number (3GPP TS 24.008 table 10.5.123) */ + CDMA_LOCKED_UNTIL_POWER_CYCLE, /* MS is locked until next power cycle */ + CDMA_DROP, + CDMA_INTERCEPT, /* INTERCEPT order received, MS state idle entered */ + CDMA_REORDER, /* MS has been redirected, call is cancelled */ + CDMA_SO_REJECT, /* service option rejection */ + CDMA_RETRY_ORDER, /* requested service is rejected, retry delay is set */ + CDMA_ACCESS_FAILURE, + CDMA_PREEMPTED, + CDMA_NOT_EMERGENCY, /* not an emergency call */ + CDMA_ACCESS_BLOCKED, /* Access Blocked by CDMA network */ + ERROR_UNSPECIFIED + } + + Object userData; + + /* Instance Methods */ + + /** + * Gets address (e.g. phone number) associated with connection. + * TODO: distinguish reasons for unavailability + * + * @return address or null if unavailable + */ + + public abstract String getAddress(); + + /** + * Gets CNAP name associated with connection. + * @return cnap name or null if unavailable + */ + public String getCnapName() { + return cnapName; + } + + /** + * Get original dial string. + * @return original dial string or null if unavailable + */ + public String getOrigDialString(){ + return null; + } + + /** + * Gets CNAP presentation associated with connection. + * @return cnap name or null if unavailable + */ + + public int getCnapNamePresentation() { + return cnapNamePresentation; + }; + + /** + * @return Call that owns this Connection, or null if none + */ + public abstract Call getCall(); + + /** + * Connection create time in currentTimeMillis() format + * Basically, set when object is created. + * Effectively, when an incoming call starts ringing or an + * outgoing call starts dialing + */ + public abstract long getCreateTime(); + + /** + * Connection connect time in currentTimeMillis() format. + * For outgoing calls: Begins at (DIALING|ALERTING) -> ACTIVE transition. + * For incoming calls: Begins at (INCOMING|WAITING) -> ACTIVE transition. + * Returns 0 before then. + */ + public abstract long getConnectTime(); + + /** + * Disconnect time in currentTimeMillis() format. + * The time when this Connection makes a transition into ENDED or FAIL. + * Returns 0 before then. + */ + public abstract long getDisconnectTime(); + + /** + * Returns the number of milliseconds the call has been connected, + * or 0 if the call has never connected. + * If the call is still connected, then returns the elapsed + * time since connect. + */ + public abstract long getDurationMillis(); + + /** + * If this connection is HOLDING, return the number of milliseconds + * that it has been on hold for (approximately). + * If this connection is in any other state, return 0. + */ + + public abstract long getHoldDurationMillis(); + + /** + * Returns "NOT_DISCONNECTED" if not yet disconnected. + */ + public abstract DisconnectCause getDisconnectCause(); + + /** + * Returns true of this connection originated elsewhere + * ("MT" or mobile terminated; another party called this terminal) + * or false if this call originated here (MO or mobile originated). + */ + public abstract boolean isIncoming(); + + /** + * If this Connection is connected, then it is associated with + * a Call. + * + * Returns getCall().getState() or Call.State.IDLE if not + * connected + */ + public Call.State getState() { + Call c; + + c = getCall(); + + if (c == null) { + return Call.State.IDLE; + } else { + return c.getState(); + } + } + + /** + * isAlive() + * + * @return true if the connection isn't disconnected + * (could be active, holding, ringing, dialing, etc) + */ + public boolean + isAlive() { + return getState().isAlive(); + } + + /** + * Returns true if Connection is connected and is INCOMING or WAITING + */ + public boolean + isRinging() { + return getState().isRinging(); + } + + /** + * + * @return the userdata set in setUserData() + */ + public Object getUserData() { + return userData; + } + + /** + * + * @param userdata user can store an any userdata in the Connection object. + */ + public void setUserData(Object userdata) { + this.userData = userdata; + } + + /** + * Hangup individual Connection + */ + public abstract void hangup() throws CallStateException; + + /** + * Separate this call from its owner Call and assigns it to a new Call + * (eg if it is currently part of a Conference call + * TODO: Throw exception? Does GSM require error display on failure here? + */ + public abstract void separate() throws CallStateException; + + public enum PostDialState { + NOT_STARTED, /* The post dial string playback hasn't + been started, or this call is not yet + connected, or this is an incoming call */ + STARTED, /* The post dial string playback has begun */ + WAIT, /* The post dial string playback is waiting for a + call to proceedAfterWaitChar() */ + WILD, /* The post dial string playback is waiting for a + call to proceedAfterWildChar() */ + COMPLETE, /* The post dial string playback is complete */ + CANCELLED, /* The post dial string playback was cancelled + with cancelPostDial() */ + PAUSE /* The post dial string playback is pausing for a + call to processNextPostDialChar*/ + } + + public void clearUserData(){ + userData = null; + } + + public abstract PostDialState getPostDialState(); + + /** + * Returns the portion of the post dial string that has not + * yet been dialed, or "" if none + */ + public abstract String getRemainingPostDialString(); + + /** + * See Phone.setOnPostDialWaitCharacter() + */ + + public abstract void proceedAfterWaitChar(); + + /** + * See Phone.setOnPostDialWildCharacter() + */ + public abstract void proceedAfterWildChar(String str); + /** + * Cancel any post + */ + public abstract void cancelPostDial(); + + /** + * Returns the caller id presentation type for incoming and waiting calls + * @return one of PRESENTATION_* + */ + public abstract int getNumberPresentation(); + + /** + * Returns the User to User Signaling (UUS) information associated with + * incoming and waiting calls + * @return UUSInfo containing the UUS userdata. + */ + public abstract UUSInfo getUUSInfo(); + + /** + * Build a human representation of a connection instance, suitable for debugging. + * Don't log personal stuff unless in debug mode. + * @return a string representing the internal state of this connection. + */ + public String toString() { + StringBuilder str = new StringBuilder(128); + + if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { + str.append("addr: " + getAddress()) + .append(" pres.: " + getNumberPresentation()) + .append(" dial: " + getOrigDialString()) + .append(" postdial: " + getRemainingPostDialString()) + .append(" cnap name: " + getCnapName()) + .append("(" + getCnapNamePresentation() + ")"); + } + str.append(" incoming: " + isIncoming()) + .append(" state: " + getState()) + .append(" post dial state: " + getPostDialState()); + return str.toString(); + } +} diff --git a/src/java/com/android/internal/telephony/DataCallState.java b/src/java/com/android/internal/telephony/DataCallState.java new file mode 100644 index 0000000..efbf608 --- /dev/null +++ b/src/java/com/android/internal/telephony/DataCallState.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2009 Qualcomm Innovation Center, Inc. All Rights Reserved. + * Copyright (C) 2009 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.internal.telephony; + +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.NetworkUtils; +import android.net.RouteInfo; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.telephony.DataConnection.FailCause; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * This is RIL_Data_Call_Response_v5 from ril.h + * TODO: Rename to DataCallResponse. + */ +public class DataCallState { + private final boolean DBG = true; + private final String LOG_TAG = "GSM"; + + public int version = 0; + public int status = 0; + public int cid = 0; + public int active = 0; + public String type = ""; + public String ifname = ""; + public String [] addresses = new String[0]; + public String [] dnses = new String[0]; + public String[] gateways = new String[0]; + public int suggestedRetryTime = -1; + + /** + * Class returned by onSetupConnectionCompleted. + */ + public enum SetupResult { + SUCCESS, + ERR_BadCommand, + ERR_UnacceptableParameter, + ERR_GetLastErrorFromRil, + ERR_Stale, + ERR_RilError; + + public FailCause mFailCause; + + SetupResult() { + mFailCause = FailCause.fromInt(0); + } + + @Override + public String toString() { + return name() + " SetupResult.mFailCause=" + mFailCause; + } + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("DataCallState: {") + .append("version=").append(version) + .append(" status=").append(status) + .append(" retry=").append(suggestedRetryTime) + .append(" cid=").append(cid) + .append(" active=").append(active) + .append(" type=").append(type) + .append("' ifname='").append(ifname); + sb.append("' addresses=["); + for (String addr : addresses) { + sb.append(addr); + sb.append(","); + } + if (addresses.length > 0) sb.deleteCharAt(sb.length()-1); + sb.append("] dnses=["); + for (String addr : dnses) { + sb.append(addr); + sb.append(","); + } + if (dnses.length > 0) sb.deleteCharAt(sb.length()-1); + sb.append("] gateways=["); + for (String addr : gateways) { + sb.append(addr); + sb.append(","); + } + if (gateways.length > 0) sb.deleteCharAt(sb.length()-1); + sb.append("]}"); + return sb.toString(); + } + + public SetupResult setLinkProperties(LinkProperties linkProperties, + boolean okToUseSystemPropertyDns) { + SetupResult result; + + // Start with clean network properties and if we have + // a failure we'll clear again at the bottom of this code. + if (linkProperties == null) + linkProperties = new LinkProperties(); + else + linkProperties.clear(); + + if (status == FailCause.NONE.getErrorCode()) { + String propertyPrefix = "net." + ifname + "."; + + try { + // set interface name + linkProperties.setInterfaceName(ifname); + + // set link addresses + if (addresses != null && addresses.length > 0) { + for (String addr : addresses) { + addr = addr.trim(); + if (addr.isEmpty()) continue; + LinkAddress la; + int addrPrefixLen; + + String [] ap = addr.split("/"); + if (ap.length == 2) { + addr = ap[0]; + addrPrefixLen = Integer.parseInt(ap[1]); + } else { + addrPrefixLen = 0; + } + InetAddress ia; + try { + ia = NetworkUtils.numericToInetAddress(addr); + } catch (IllegalArgumentException e) { + throw new UnknownHostException("Non-numeric ip addr=" + addr); + } + if (! ia.isAnyLocalAddress()) { + if (addrPrefixLen == 0) { + // Assume point to point + addrPrefixLen = (ia instanceof Inet4Address) ? 32 : 128; + } + if (DBG) Log.d(LOG_TAG, "addr/pl=" + addr + "/" + addrPrefixLen); + la = new LinkAddress(ia, addrPrefixLen); + linkProperties.addLinkAddress(la); + } + } + } else { + throw new UnknownHostException("no address for ifname=" + ifname); + } + + // set dns servers + if (dnses != null && dnses.length > 0) { + for (String addr : dnses) { + addr = addr.trim(); + if (addr.isEmpty()) continue; + InetAddress ia; + try { + ia = NetworkUtils.numericToInetAddress(addr); + } catch (IllegalArgumentException e) { + throw new UnknownHostException("Non-numeric dns addr=" + addr); + } + if (! ia.isAnyLocalAddress()) { + linkProperties.addDns(ia); + } + } + } else if (okToUseSystemPropertyDns){ + String dnsServers[] = new String[2]; + dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1"); + dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2"); + for (String dnsAddr : dnsServers) { + dnsAddr = dnsAddr.trim(); + if (dnsAddr.isEmpty()) continue; + InetAddress ia; + try { + ia = NetworkUtils.numericToInetAddress(dnsAddr); + } catch (IllegalArgumentException e) { + throw new UnknownHostException("Non-numeric dns addr=" + dnsAddr); + } + if (! ia.isAnyLocalAddress()) { + linkProperties.addDns(ia); + } + } + } else { + throw new UnknownHostException("Empty dns response and no system default dns"); + } + + // set gateways + if ((gateways == null) || (gateways.length == 0)) { + String sysGateways = SystemProperties.get(propertyPrefix + "gw"); + if (sysGateways != null) { + gateways = sysGateways.split(" "); + } else { + gateways = new String[0]; + } + } + for (String addr : gateways) { + addr = addr.trim(); + if (addr.isEmpty()) continue; + InetAddress ia; + try { + ia = NetworkUtils.numericToInetAddress(addr); + } catch (IllegalArgumentException e) { + throw new UnknownHostException("Non-numeric gateway addr=" + addr); + } + if (! ia.isAnyLocalAddress()) { + linkProperties.addRoute(new RouteInfo(ia)); + } + } + + result = SetupResult.SUCCESS; + } catch (UnknownHostException e) { + Log.d(LOG_TAG, "setLinkProperties: UnknownHostException " + e); + e.printStackTrace(); + result = SetupResult.ERR_UnacceptableParameter; + } + } else { + if (version < 4) { + result = SetupResult.ERR_GetLastErrorFromRil; + } else { + result = SetupResult.ERR_RilError; + } + } + + // An error occurred so clear properties + if (result != SetupResult.SUCCESS) { + if(DBG) { + Log.d(LOG_TAG, "setLinkProperties: error clearing LinkProperties " + + "status=" + status + " result=" + result); + } + linkProperties.clear(); + } + + return result; + } +} diff --git a/src/java/com/android/internal/telephony/DataConnection.java b/src/java/com/android/internal/telephony/DataConnection.java new file mode 100644 index 0000000..9751040 --- /dev/null +++ b/src/java/com/android/internal/telephony/DataConnection.java @@ -0,0 +1,1286 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + + +import com.android.internal.telephony.DataCallState.SetupResult; +import com.android.internal.util.AsyncChannel; +import com.android.internal.util.Protocol; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; + +import android.app.PendingIntent; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.net.ProxyProperties; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.TimeUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@hide} + * + * DataConnection StateMachine. + * + * This is an abstract base class for representing a single data connection. + * Instances of this class such as <code>CdmaDataConnection</code> and + * <code>GsmDataConnection</code>, * represent a connection via the cellular network. + * There may be multiple data connections and all of them are managed by the + * <code>DataConnectionTracker</code>. + * + * Instances are asynchronous state machines and have two primary entry points + * <code>connect()</code> and <code>disconnect</code>. The message a parameter will be returned + * hen the operation completes. The <code>msg.obj</code> will contain an AsyncResult + * object and <code>AsyncResult.userObj</code> is the original <code>msg.obj</code>. if successful + * with the <code>AsyncResult.result == null</code> and <code>AsyncResult.exception == null</code>. + * If an error <code>AsyncResult.result = FailCause</code> and + * <code>AsyncResult.exception = new Exception()</code>. + * + * The other public methods are provided for debugging. + */ +public abstract class DataConnection extends StateMachine { + protected static final boolean DBG = true; + protected static final boolean VDBG = false; + + protected static AtomicInteger mCount = new AtomicInteger(0); + protected AsyncChannel mAc; + + protected List<ApnContext> mApnList = null; + PendingIntent mReconnectIntent = null; + + private DataConnectionTracker mDataConnectionTracker = null; + + /** + * Used internally for saving connecting parameters. + */ + protected static class ConnectionParams { + public ConnectionParams(ApnSetting apn, Message onCompletedMsg) { + this.apn = apn; + this.onCompletedMsg = onCompletedMsg; + } + + public int tag; + public ApnSetting apn; + public Message onCompletedMsg; + } + + /** + * Used internally for saving disconnecting parameters. + */ + protected static class DisconnectParams { + public DisconnectParams(String reason, Message onCompletedMsg) { + this.reason = reason; + this.onCompletedMsg = onCompletedMsg; + } + public int tag; + public String reason; + public Message onCompletedMsg; + } + + /** + * Returned as the reason for a connection failure as defined + * by RIL_DataCallFailCause in ril.h and some local errors. + */ + public enum FailCause { + NONE(0), + + // This series of errors as specified by the standards + // specified in ril.h + OPERATOR_BARRED(0x08), + INSUFFICIENT_RESOURCES(0x1A), + MISSING_UNKNOWN_APN(0x1B), + UNKNOWN_PDP_ADDRESS_TYPE(0x1C), + USER_AUTHENTICATION(0x1D), + ACTIVATION_REJECT_GGSN(0x1E), + ACTIVATION_REJECT_UNSPECIFIED(0x1F), + SERVICE_OPTION_NOT_SUPPORTED(0x20), + SERVICE_OPTION_NOT_SUBSCRIBED(0x21), + SERVICE_OPTION_OUT_OF_ORDER(0x22), + NSAPI_IN_USE(0x23), + ONLY_IPV4_ALLOWED(0x32), + ONLY_IPV6_ALLOWED(0x33), + ONLY_SINGLE_BEARER_ALLOWED(0x34), + PROTOCOL_ERRORS(0x6F), + + // Local errors generated by Vendor RIL + // specified in ril.h + REGISTRATION_FAIL(-1), + GPRS_REGISTRATION_FAIL(-2), + SIGNAL_LOST(-3), + PREF_RADIO_TECH_CHANGED(-4), + RADIO_POWER_OFF(-5), + TETHERED_CALL_ACTIVE(-6), + ERROR_UNSPECIFIED(0xFFFF), + + // Errors generated by the Framework + // specified here + UNKNOWN(0x10000), + RADIO_NOT_AVAILABLE(0x10001), + UNACCEPTABLE_NETWORK_PARAMETER(0x10002), + CONNECTION_TO_DATACONNECTIONAC_BROKEN(0x10003); + + private final int mErrorCode; + private static final HashMap<Integer, FailCause> sErrorCodeToFailCauseMap; + static { + sErrorCodeToFailCauseMap = new HashMap<Integer, FailCause>(); + for (FailCause fc : values()) { + sErrorCodeToFailCauseMap.put(fc.getErrorCode(), fc); + } + } + + FailCause(int errorCode) { + mErrorCode = errorCode; + } + + int getErrorCode() { + return mErrorCode; + } + + public boolean isPermanentFail() { + return (this == OPERATOR_BARRED) || (this == MISSING_UNKNOWN_APN) || + (this == UNKNOWN_PDP_ADDRESS_TYPE) || (this == USER_AUTHENTICATION) || + (this == SERVICE_OPTION_NOT_SUPPORTED) || + (this == SERVICE_OPTION_NOT_SUBSCRIBED) || (this == NSAPI_IN_USE) || + (this == PROTOCOL_ERRORS); + } + + public boolean isEventLoggable() { + return (this == OPERATOR_BARRED) || (this == INSUFFICIENT_RESOURCES) || + (this == UNKNOWN_PDP_ADDRESS_TYPE) || (this == USER_AUTHENTICATION) || + (this == ACTIVATION_REJECT_GGSN) || (this == ACTIVATION_REJECT_UNSPECIFIED) || + (this == SERVICE_OPTION_NOT_SUBSCRIBED) || + (this == SERVICE_OPTION_NOT_SUPPORTED) || + (this == SERVICE_OPTION_OUT_OF_ORDER) || (this == NSAPI_IN_USE) || + (this == PROTOCOL_ERRORS) || + (this == UNACCEPTABLE_NETWORK_PARAMETER); + } + + public static FailCause fromInt(int errorCode) { + FailCause fc = sErrorCodeToFailCauseMap.get(errorCode); + if (fc == null) { + fc = UNKNOWN; + } + return fc; + } + } + + public static class CallSetupException extends Exception { + private int mRetryOverride = -1; + + CallSetupException (int retryOverride) { + mRetryOverride = retryOverride; + } + + public int getRetryOverride() { + return mRetryOverride; + } + } + + // ***** Event codes for driving the state machine + protected static final int BASE = Protocol.BASE_DATA_CONNECTION; + protected static final int EVENT_CONNECT = BASE + 0; + protected static final int EVENT_SETUP_DATA_CONNECTION_DONE = BASE + 1; + protected static final int EVENT_GET_LAST_FAIL_DONE = BASE + 2; + protected static final int EVENT_DEACTIVATE_DONE = BASE + 3; + protected static final int EVENT_DISCONNECT = BASE + 4; + protected static final int EVENT_RIL_CONNECTED = BASE + 5; + protected static final int EVENT_DISCONNECT_ALL = BASE + 6; + + private static final int CMD_TO_STRING_COUNT = EVENT_DISCONNECT_ALL - BASE + 1; + private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; + static { + sCmdToString[EVENT_CONNECT - BASE] = "EVENT_CONNECT"; + sCmdToString[EVENT_SETUP_DATA_CONNECTION_DONE - BASE] = + "EVENT_SETUP_DATA_CONNECTION_DONE"; + sCmdToString[EVENT_GET_LAST_FAIL_DONE - BASE] = "EVENT_GET_LAST_FAIL_DONE"; + sCmdToString[EVENT_DEACTIVATE_DONE - BASE] = "EVENT_DEACTIVATE_DONE"; + sCmdToString[EVENT_DISCONNECT - BASE] = "EVENT_DISCONNECT"; + sCmdToString[EVENT_RIL_CONNECTED - BASE] = "EVENT_RIL_CONNECTED"; + sCmdToString[EVENT_DISCONNECT_ALL - BASE] = "EVENT_DISCONNECT_ALL"; + } + protected static String cmdToString(int cmd) { + cmd -= BASE; + if ((cmd >= 0) && (cmd < sCmdToString.length)) { + return sCmdToString[cmd]; + } else { + return null; + } + } + + //***** Tag IDs for EventLog + protected static final int EVENT_LOG_BAD_DNS_ADDRESS = 50100; + + //***** Member Variables + protected ApnSetting mApn; + protected int mTag; + protected PhoneBase phone; + protected int mRilVersion = -1; + protected int cid; + protected LinkProperties mLinkProperties = new LinkProperties(); + protected LinkCapabilities mCapabilities = new LinkCapabilities(); + protected long createTime; + protected long lastFailTime; + protected FailCause lastFailCause; + protected int mRetryOverride = -1; + protected static final String NULL_IP = "0.0.0.0"; + protected int mRefCount; + Object userData; + + //***** Abstract methods + @Override + public abstract String toString(); + + protected abstract void onConnect(ConnectionParams cp); + + protected abstract boolean isDnsOk(String[] domainNameServers); + + protected abstract void log(String s); + + //***** Constructor + protected DataConnection(PhoneBase phone, String name, int id, RetryManager rm, + DataConnectionTracker dct) { + super(name); + setLogRecSize(100); + if (DBG) log("DataConnection constructor E"); + this.phone = phone; + this.mDataConnectionTracker = dct; + mId = id; + mRetryMgr = rm; + this.cid = -1; + + setDbg(false); + addState(mDefaultState); + addState(mInactiveState, mDefaultState); + addState(mActivatingState, mDefaultState); + addState(mActiveState, mDefaultState); + addState(mDisconnectingState, mDefaultState); + addState(mDisconnectingErrorCreatingConnection, mDefaultState); + setInitialState(mInactiveState); + + mApnList = new ArrayList<ApnContext>(); + if (DBG) log("DataConnection constructor X"); + } + + /** + * Shut down this instance and its state machine. + */ + private void shutDown() { + if (DBG) log("shutDown"); + + if (mAc != null) { + mAc.disconnected(); + mAc = null; + } + mApnList = null; + mReconnectIntent = null; + mDataConnectionTracker = null; + mApn = null; + phone = null; + mLinkProperties = null; + mCapabilities = null; + lastFailCause = null; + userData = null; + } + + /** + * TearDown the data connection. + * + * @param o will be returned in AsyncResult.userObj + * and is either a DisconnectParams or ConnectionParams. + */ + private void tearDownData(Object o) { + int discReason = RILConstants.DEACTIVATE_REASON_NONE; + if ((o != null) && (o instanceof DisconnectParams)) { + DisconnectParams dp = (DisconnectParams)o; + Message m = dp.onCompletedMsg; + if (TextUtils.equals(dp.reason, Phone.REASON_RADIO_TURNED_OFF)) { + discReason = RILConstants.DEACTIVATE_REASON_RADIO_OFF; + } else if (TextUtils.equals(dp.reason, Phone.REASON_PDP_RESET)) { + discReason = RILConstants.DEACTIVATE_REASON_PDP_RESET; + } + } + if (phone.mCM.getRadioState().isOn()) { + if (DBG) log("tearDownData radio is on, call deactivateDataCall"); + phone.mCM.deactivateDataCall(cid, discReason, obtainMessage(EVENT_DEACTIVATE_DONE, o)); + } else { + if (DBG) log("tearDownData radio is off sendMessage EVENT_DEACTIVATE_DONE immediately"); + AsyncResult ar = new AsyncResult(o, null, null); + sendMessage(obtainMessage(EVENT_DEACTIVATE_DONE, ar)); + } + } + + /** + * Send the connectionCompletedMsg. + * + * @param cp is the ConnectionParams + * @param cause + */ + private void notifyConnectCompleted(ConnectionParams cp, FailCause cause) { + Message connectionCompletedMsg = cp.onCompletedMsg; + if (connectionCompletedMsg == null) { + return; + } + + long timeStamp = System.currentTimeMillis(); + connectionCompletedMsg.arg1 = cid; + + if (cause == FailCause.NONE) { + createTime = timeStamp; + AsyncResult.forMessage(connectionCompletedMsg); + } else { + lastFailCause = cause; + lastFailTime = timeStamp; + AsyncResult.forMessage(connectionCompletedMsg, cause, + new CallSetupException(mRetryOverride)); + } + if (DBG) log("notifyConnectionCompleted at " + timeStamp + " cause=" + cause); + + connectionCompletedMsg.sendToTarget(); + } + + /** + * Send ar.userObj if its a message, which is should be back to originator. + * + * @param dp is the DisconnectParams. + */ + private void notifyDisconnectCompleted(DisconnectParams dp, boolean sendAll) { + if (VDBG) log("NotifyDisconnectCompleted"); + + ApnContext alreadySent = null; + String reason = null; + + if (dp.onCompletedMsg != null) { + // Get ApnContext, but only valid on GSM devices this is a string on CDMA devices. + Message msg = dp.onCompletedMsg; + if (msg.obj instanceof ApnContext) { + alreadySent = (ApnContext)msg.obj; + } + reason = dp.reason; + if (VDBG) { + log(String.format("msg=%s msg.obj=%s", msg.toString(), + ((msg.obj instanceof String) ? (String) msg.obj : "<no-reason>"))); + } + AsyncResult.forMessage(msg); + msg.sendToTarget(); + } + if (sendAll) { + for (ApnContext a : mApnList) { + if (a == alreadySent) continue; + if (reason != null) a.setReason(reason); + Message msg = mDataConnectionTracker.obtainMessage( + DctConstants.EVENT_DISCONNECT_DONE, a); + AsyncResult.forMessage(msg); + msg.sendToTarget(); + } + } + + if (DBG) log("NotifyDisconnectCompleted DisconnectParams=" + dp); + } + + protected int getRilRadioTechnology(int defaultRilRadioTechnology) { + int rilRadioTechnology; + if (mRilVersion < 6) { + rilRadioTechnology = defaultRilRadioTechnology; + } else { + rilRadioTechnology = phone.getServiceState().getRilRadioTechnology() + 2; + } + return rilRadioTechnology; + } + + /* + * ************************************************************************** + * Begin Members and methods owned by DataConnectionTracker but stored + * in a DataConnection because there is one per connection. + * ************************************************************************** + */ + + /* + * The id is owned by DataConnectionTracker. + */ + private int mId; + + /** + * Get the DataConnection ID + */ + public int getDataConnectionId() { + return mId; + } + + /* + * The retry manager is currently owned by the DataConnectionTracker but is stored + * in the DataConnection because there is one per connection. These methods + * should only be used by the DataConnectionTracker although someday the retrying + * maybe managed by the DataConnection itself and these methods could disappear. + */ + private RetryManager mRetryMgr; + + /** + * @return retry manager retryCount + */ + public int getRetryCount() { + return mRetryMgr.getRetryCount(); + } + + /** + * set retry manager retryCount + */ + public void setRetryCount(int retryCount) { + if (DBG) log("setRetryCount: " + retryCount); + mRetryMgr.setRetryCount(retryCount); + } + + /** + * @return retry manager retryTimer + */ + public int getRetryTimer() { + return mRetryMgr.getRetryTimer(); + } + + /** + * increaseRetryCount of retry manager + */ + public void increaseRetryCount() { + mRetryMgr.increaseRetryCount(); + } + + /** + * @return retry manager isRetryNeeded + */ + public boolean isRetryNeeded() { + return mRetryMgr.isRetryNeeded(); + } + + /** + * resetRetryCount of retry manager + */ + public void resetRetryCount() { + mRetryMgr.resetRetryCount(); + } + + /** + * set retryForeverUsingLasttimeout of retry manager + */ + public void retryForeverUsingLastTimeout() { + mRetryMgr.retryForeverUsingLastTimeout(); + } + + /** + * @return retry manager isRetryForever + */ + public boolean isRetryForever() { + return mRetryMgr.isRetryForever(); + } + + /** + * @return whether the retry config is set successfully or not + */ + public boolean configureRetry(int maxRetryCount, int retryTime, int randomizationTime) { + return mRetryMgr.configure(maxRetryCount, retryTime, randomizationTime); + } + + /** + * @return whether the retry config is set successfully or not + */ + public boolean configureRetry(String configStr) { + return mRetryMgr.configure(configStr); + } + + /* + * ************************************************************************** + * End members owned by DataConnectionTracker + * ************************************************************************** + */ + + /** + * Clear all settings called when entering mInactiveState. + */ + protected void clearSettings() { + if (DBG) log("clearSettings"); + + createTime = -1; + lastFailTime = -1; + lastFailCause = FailCause.NONE; + mRetryOverride = -1; + mRefCount = 0; + cid = -1; + + mLinkProperties = new LinkProperties(); + mApn = null; + } + + /** + * Process setup completion. + * + * @param ar is the result + * @return SetupResult. + */ + private DataCallState.SetupResult onSetupConnectionCompleted(AsyncResult ar) { + DataCallState response = (DataCallState) ar.result; + ConnectionParams cp = (ConnectionParams) ar.userObj; + DataCallState.SetupResult result; + + if (ar.exception != null) { + if (DBG) { + log("onSetupConnectionCompleted failed, ar.exception=" + ar.exception + + " response=" + response); + } + + if (ar.exception instanceof CommandException + && ((CommandException) (ar.exception)).getCommandError() + == CommandException.Error.RADIO_NOT_AVAILABLE) { + result = DataCallState.SetupResult.ERR_BadCommand; + result.mFailCause = FailCause.RADIO_NOT_AVAILABLE; + } else if ((response == null) || (response.version < 4)) { + result = DataCallState.SetupResult.ERR_GetLastErrorFromRil; + } else { + result = DataCallState.SetupResult.ERR_RilError; + result.mFailCause = FailCause.fromInt(response.status); + } + } else if (cp.tag != mTag) { + if (DBG) { + log("BUG: onSetupConnectionCompleted is stale cp.tag=" + cp.tag + ", mtag=" + mTag); + } + result = DataCallState.SetupResult.ERR_Stale; + } else if (response.status != 0) { + result = DataCallState.SetupResult.ERR_RilError; + result.mFailCause = FailCause.fromInt(response.status); + } else { + if (DBG) log("onSetupConnectionCompleted received DataCallState: " + response); + cid = response.cid; + result = updateLinkProperty(response).setupResult; + } + + return result; + } + + private int getSuggestedRetryTime(AsyncResult ar) { + int retry = -1; + if (ar.exception == null) { + DataCallState response = (DataCallState) ar.result; + retry = response.suggestedRetryTime; + } + return retry; + } + + private DataCallState.SetupResult setLinkProperties(DataCallState response, + LinkProperties lp) { + // Check if system property dns usable + boolean okToUseSystemPropertyDns = false; + String propertyPrefix = "net." + response.ifname + "."; + String dnsServers[] = new String[2]; + dnsServers[0] = SystemProperties.get(propertyPrefix + "dns1"); + dnsServers[1] = SystemProperties.get(propertyPrefix + "dns2"); + okToUseSystemPropertyDns = isDnsOk(dnsServers); + + // set link properties based on data call response + return response.setLinkProperties(lp, okToUseSystemPropertyDns); + } + + public static class UpdateLinkPropertyResult { + public DataCallState.SetupResult setupResult = DataCallState.SetupResult.SUCCESS; + public LinkProperties oldLp; + public LinkProperties newLp; + public UpdateLinkPropertyResult(LinkProperties curLp) { + oldLp = curLp; + newLp = curLp; + } + } + + private UpdateLinkPropertyResult updateLinkProperty(DataCallState newState) { + UpdateLinkPropertyResult result = new UpdateLinkPropertyResult(mLinkProperties); + + if (newState == null) return result; + + DataCallState.SetupResult setupResult; + result.newLp = new LinkProperties(); + + // set link properties based on data call response + result.setupResult = setLinkProperties(newState, result.newLp); + if (result.setupResult != DataCallState.SetupResult.SUCCESS) { + if (DBG) log("updateLinkProperty failed : " + result.setupResult); + return result; + } + // copy HTTP proxy as it is not part DataCallState. + result.newLp.setHttpProxy(mLinkProperties.getHttpProxy()); + + if (DBG && (! result.oldLp.equals(result.newLp))) { + log("updateLinkProperty old LP=" + result.oldLp); + log("updateLinkProperty new LP=" + result.newLp); + } + mLinkProperties = result.newLp; + + return result; + } + + /** + * The parent state for all other states. + */ + private class DcDefaultState extends State { + @Override + public void enter() { + phone.mCM.registerForRilConnected(getHandler(), EVENT_RIL_CONNECTED, null); + } + @Override + public void exit() { + phone.mCM.unregisterForRilConnected(getHandler()); + shutDown(); + } + @Override + public boolean processMessage(Message msg) { + boolean retVal = HANDLED; + AsyncResult ar; + + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: { + if (mAc != null) { + if (VDBG) log("Disconnecting to previous connection mAc=" + mAc); + mAc.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, + AsyncChannel.STATUS_FULL_CONNECTION_REFUSED_ALREADY_CONNECTED); + } else { + mAc = new AsyncChannel(); + mAc.connected(null, getHandler(), msg.replyTo); + if (VDBG) log("DcDefaultState: FULL_CONNECTION reply connected"); + mAc.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, + AsyncChannel.STATUS_SUCCESSFUL, mId, "hi"); + } + break; + } + case AsyncChannel.CMD_CHANNEL_DISCONNECTED: { + if (VDBG) log("CMD_CHANNEL_DISCONNECTED"); + quit(); + break; + } + case DataConnectionAc.REQ_IS_INACTIVE: { + boolean val = getCurrentState() == mInactiveState; + if (VDBG) log("REQ_IS_INACTIVE isInactive=" + val); + mAc.replyToMessage(msg, DataConnectionAc.RSP_IS_INACTIVE, val ? 1 : 0); + break; + } + case DataConnectionAc.REQ_GET_CID: { + if (VDBG) log("REQ_GET_CID cid=" + cid); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_CID, cid); + break; + } + case DataConnectionAc.REQ_GET_APNSETTING: { + if (VDBG) log("REQ_GET_APNSETTING apnSetting=" + mApn); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_APNSETTING, mApn); + break; + } + case DataConnectionAc.REQ_GET_LINK_PROPERTIES: { + LinkProperties lp = new LinkProperties(mLinkProperties); + if (VDBG) log("REQ_GET_LINK_PROPERTIES linkProperties" + lp); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_LINK_PROPERTIES, lp); + break; + } + case DataConnectionAc.REQ_SET_LINK_PROPERTIES_HTTP_PROXY: { + ProxyProperties proxy = (ProxyProperties) msg.obj; + if (VDBG) log("REQ_SET_LINK_PROPERTIES_HTTP_PROXY proxy=" + proxy); + mLinkProperties.setHttpProxy(proxy); + mAc.replyToMessage(msg, DataConnectionAc.RSP_SET_LINK_PROPERTIES_HTTP_PROXY); + break; + } + case DataConnectionAc.REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE: { + DataCallState newState = (DataCallState) msg.obj; + UpdateLinkPropertyResult result = + updateLinkProperty(newState); + if (VDBG) { + log("REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE result=" + + result + " newState=" + newState); + } + mAc.replyToMessage(msg, + DataConnectionAc.RSP_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE, + result); + break; + } + case DataConnectionAc.REQ_GET_LINK_CAPABILITIES: { + LinkCapabilities lc = new LinkCapabilities(mCapabilities); + if (VDBG) log("REQ_GET_LINK_CAPABILITIES linkCapabilities" + lc); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_LINK_CAPABILITIES, lc); + break; + } + case DataConnectionAc.REQ_RESET: + if (VDBG) log("DcDefaultState: msg.what=REQ_RESET"); + mAc.replyToMessage(msg, DataConnectionAc.RSP_RESET); + transitionTo(mInactiveState); + break; + case DataConnectionAc.REQ_GET_REFCOUNT: { + if (VDBG) log("REQ_GET_REFCOUNT refCount=" + mRefCount); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_REFCOUNT, mRefCount); + break; + } + case DataConnectionAc.REQ_ADD_APNCONTEXT: { + ApnContext apnContext = (ApnContext) msg.obj; + if (VDBG) log("REQ_ADD_APNCONTEXT apn=" + apnContext.getApnType()); + if (!mApnList.contains(apnContext)) { + mApnList.add(apnContext); + } + mAc.replyToMessage(msg, DataConnectionAc.RSP_ADD_APNCONTEXT); + break; + } + case DataConnectionAc.REQ_REMOVE_APNCONTEXT: { + ApnContext apnContext = (ApnContext) msg.obj; + if (VDBG) log("REQ_REMOVE_APNCONTEXT apn=" + apnContext.getApnType()); + mApnList.remove(apnContext); + mAc.replyToMessage(msg, DataConnectionAc.RSP_REMOVE_APNCONTEXT); + break; + } + case DataConnectionAc.REQ_GET_APNCONTEXT_LIST: { + if (VDBG) log("REQ_GET_APNCONTEXT_LIST num in list=" + mApnList.size()); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_APNCONTEXT_LIST, + new ArrayList<ApnContext>(mApnList)); + break; + } + case DataConnectionAc.REQ_SET_RECONNECT_INTENT: { + PendingIntent intent = (PendingIntent) msg.obj; + if (VDBG) log("REQ_SET_RECONNECT_INTENT"); + mReconnectIntent = intent; + mAc.replyToMessage(msg, DataConnectionAc.RSP_SET_RECONNECT_INTENT); + break; + } + case DataConnectionAc.REQ_GET_RECONNECT_INTENT: { + if (VDBG) log("REQ_GET_RECONNECT_INTENT"); + mAc.replyToMessage(msg, DataConnectionAc.RSP_GET_RECONNECT_INTENT, + mReconnectIntent); + break; + } + case EVENT_CONNECT: + if (DBG) log("DcDefaultState: msg.what=EVENT_CONNECT, fail not expected"); + ConnectionParams cp = (ConnectionParams) msg.obj; + notifyConnectCompleted(cp, FailCause.UNKNOWN); + break; + + case EVENT_DISCONNECT: + if (DBG) { + log("DcDefaultState deferring msg.what=EVENT_DISCONNECT" + mRefCount); + } + deferMessage(msg); + break; + + case EVENT_DISCONNECT_ALL: + if (DBG) { + log("DcDefaultState deferring msg.what=EVENT_DISCONNECT_ALL" + mRefCount); + } + deferMessage(msg); + break; + + case EVENT_RIL_CONNECTED: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + mRilVersion = (Integer)ar.result; + if (DBG) { + log("DcDefaultState: msg.what=EVENT_RIL_CONNECTED mRilVersion=" + + mRilVersion); + } + } else { + log("Unexpected exception on EVENT_RIL_CONNECTED"); + mRilVersion = -1; + } + break; + + default: + if (DBG) { + log("DcDefaultState: shouldn't happen but ignore msg.what=0x" + + Integer.toHexString(msg.what)); + } + break; + } + + return retVal; + } + } + private DcDefaultState mDefaultState = new DcDefaultState(); + + /** + * The state machine is inactive and expects a EVENT_CONNECT. + */ + private class DcInactiveState extends State { + private ConnectionParams mConnectionParams = null; + private FailCause mFailCause = null; + private DisconnectParams mDisconnectParams = null; + + public void setEnterNotificationParams(ConnectionParams cp, FailCause cause, + int retryOverride) { + if (VDBG) log("DcInactiveState: setEnterNoticationParams cp,cause"); + mConnectionParams = cp; + mFailCause = cause; + mRetryOverride = retryOverride; + } + + public void setEnterNotificationParams(DisconnectParams dp) { + if (VDBG) log("DcInactiveState: setEnterNoticationParams dp"); + mDisconnectParams = dp; + } + + @Override + public void enter() { + mTag += 1; + + /** + * Now that we've transitioned to Inactive state we + * can send notifications. Previously we sent the + * notifications in the processMessage handler but + * that caused a race condition because the synchronous + * call to isInactive. + */ + if ((mConnectionParams != null) && (mFailCause != null)) { + if (VDBG) log("DcInactiveState: enter notifyConnectCompleted"); + notifyConnectCompleted(mConnectionParams, mFailCause); + } + if (mDisconnectParams != null) { + if (VDBG) log("DcInactiveState: enter notifyDisconnectCompleted"); + notifyDisconnectCompleted(mDisconnectParams, true); + } + clearSettings(); + } + + @Override + public void exit() { + // clear notifications + mConnectionParams = null; + mFailCause = null; + mDisconnectParams = null; + } + + @Override + public boolean processMessage(Message msg) { + boolean retVal; + + switch (msg.what) { + case DataConnectionAc.REQ_RESET: + if (DBG) { + log("DcInactiveState: msg.what=RSP_RESET, ignore we're already reset"); + } + mAc.replyToMessage(msg, DataConnectionAc.RSP_RESET); + retVal = HANDLED; + break; + + case EVENT_CONNECT: + ConnectionParams cp = (ConnectionParams) msg.obj; + cp.tag = mTag; + if (DBG) { + log("DcInactiveState msg.what=EVENT_CONNECT." + "RefCount = " + + mRefCount); + } + mRefCount = 1; + onConnect(cp); + transitionTo(mActivatingState); + retVal = HANDLED; + break; + + case EVENT_DISCONNECT: + if (DBG) log("DcInactiveState: msg.what=EVENT_DISCONNECT"); + notifyDisconnectCompleted((DisconnectParams)msg.obj, false); + retVal = HANDLED; + break; + + case EVENT_DISCONNECT_ALL: + if (DBG) log("DcInactiveState: msg.what=EVENT_DISCONNECT_ALL"); + notifyDisconnectCompleted((DisconnectParams)msg.obj, false); + retVal = HANDLED; + break; + + default: + if (VDBG) { + log("DcInactiveState nothandled msg.what=0x" + + Integer.toHexString(msg.what)); + } + retVal = NOT_HANDLED; + break; + } + return retVal; + } + } + private DcInactiveState mInactiveState = new DcInactiveState(); + + /** + * The state machine is activating a connection. + */ + private class DcActivatingState extends State { + @Override + public boolean processMessage(Message msg) { + boolean retVal; + AsyncResult ar; + ConnectionParams cp; + + switch (msg.what) { + case EVENT_CONNECT: + if (DBG) log("DcActivatingState deferring msg.what=EVENT_CONNECT refCount = " + + mRefCount); + deferMessage(msg); + retVal = HANDLED; + break; + + case EVENT_SETUP_DATA_CONNECTION_DONE: + if (DBG) log("DcActivatingState msg.what=EVENT_SETUP_DATA_CONNECTION_DONE"); + + ar = (AsyncResult) msg.obj; + cp = (ConnectionParams) ar.userObj; + + DataCallState.SetupResult result = onSetupConnectionCompleted(ar); + if (DBG) log("DcActivatingState onSetupConnectionCompleted result=" + result); + switch (result) { + case SUCCESS: + // All is well + mActiveState.setEnterNotificationParams(cp, FailCause.NONE); + transitionTo(mActiveState); + break; + case ERR_BadCommand: + // Vendor ril rejected the command and didn't connect. + // Transition to inactive but send notifications after + // we've entered the mInactive state. + mInactiveState.setEnterNotificationParams(cp, result.mFailCause, -1); + transitionTo(mInactiveState); + break; + case ERR_UnacceptableParameter: + // The addresses given from the RIL are bad + tearDownData(cp); + transitionTo(mDisconnectingErrorCreatingConnection); + break; + case ERR_GetLastErrorFromRil: + // Request failed and this is an old RIL + phone.mCM.getLastDataCallFailCause( + obtainMessage(EVENT_GET_LAST_FAIL_DONE, cp)); + break; + case ERR_RilError: + // Request failed and mFailCause has the reason + mInactiveState.setEnterNotificationParams(cp, result.mFailCause, + getSuggestedRetryTime(ar)); + transitionTo(mInactiveState); + break; + case ERR_Stale: + // Request is stale, ignore. + break; + default: + throw new RuntimeException("Unknown SetupResult, should not happen"); + } + retVal = HANDLED; + break; + + case EVENT_GET_LAST_FAIL_DONE: + ar = (AsyncResult) msg.obj; + cp = (ConnectionParams) ar.userObj; + FailCause cause = FailCause.UNKNOWN; + + if (cp.tag == mTag) { + if (DBG) log("DcActivatingState msg.what=EVENT_GET_LAST_FAIL_DONE"); + if (ar.exception == null) { + int rilFailCause = ((int[]) (ar.result))[0]; + cause = FailCause.fromInt(rilFailCause); + } + // Transition to inactive but send notifications after + // we've entered the mInactive state. + mInactiveState.setEnterNotificationParams(cp, cause, -1); + transitionTo(mInactiveState); + } else { + if (DBG) { + log("DcActivatingState EVENT_GET_LAST_FAIL_DONE is stale cp.tag=" + + cp.tag + ", mTag=" + mTag); + } + } + + retVal = HANDLED; + break; + + default: + if (VDBG) { + log("DcActivatingState not handled msg.what=0x" + + Integer.toHexString(msg.what)); + } + retVal = NOT_HANDLED; + break; + } + return retVal; + } + } + private DcActivatingState mActivatingState = new DcActivatingState(); + + /** + * The state machine is connected, expecting an EVENT_DISCONNECT. + */ + private class DcActiveState extends State { + private ConnectionParams mConnectionParams = null; + private FailCause mFailCause = null; + + public void setEnterNotificationParams(ConnectionParams cp, FailCause cause) { + if (VDBG) log("DcInactiveState: setEnterNoticationParams cp,cause"); + mConnectionParams = cp; + mFailCause = cause; + } + + @Override public void enter() { + /** + * Now that we've transitioned to Active state we + * can send notifications. Previously we sent the + * notifications in the processMessage handler but + * that caused a race condition because the synchronous + * call to isActive. + */ + if ((mConnectionParams != null) && (mFailCause != null)) { + if (VDBG) log("DcActiveState: enter notifyConnectCompleted"); + notifyConnectCompleted(mConnectionParams, mFailCause); + } + } + + @Override + public void exit() { + // clear notifications + mConnectionParams = null; + mFailCause = null; + } + + @Override + public boolean processMessage(Message msg) { + boolean retVal; + + switch (msg.what) { + case EVENT_CONNECT: + mRefCount++; + if (DBG) log("DcActiveState msg.what=EVENT_CONNECT RefCount=" + mRefCount); + if (msg.obj != null) { + notifyConnectCompleted((ConnectionParams) msg.obj, FailCause.NONE); + } + retVal = HANDLED; + break; + case EVENT_DISCONNECT: + mRefCount--; + if (DBG) log("DcActiveState msg.what=EVENT_DISCONNECT RefCount=" + mRefCount); + if (mRefCount == 0) + { + DisconnectParams dp = (DisconnectParams) msg.obj; + dp.tag = mTag; + tearDownData(dp); + transitionTo(mDisconnectingState); + } else { + if (msg.obj != null) { + notifyDisconnectCompleted((DisconnectParams) msg.obj, false); + } + } + retVal = HANDLED; + break; + + case EVENT_DISCONNECT_ALL: + if (DBG) { + log("DcActiveState msg.what=EVENT_DISCONNECT_ALL RefCount=" + mRefCount); + } + mRefCount = 0; + DisconnectParams dp = (DisconnectParams) msg.obj; + dp.tag = mTag; + tearDownData(dp); + transitionTo(mDisconnectingState); + retVal = HANDLED; + break; + + default: + if (VDBG) { + log("DcActiveState not handled msg.what=0x" + + Integer.toHexString(msg.what)); + } + retVal = NOT_HANDLED; + break; + } + return retVal; + } + } + private DcActiveState mActiveState = new DcActiveState(); + + /** + * The state machine is disconnecting. + */ + private class DcDisconnectingState extends State { + @Override + public boolean processMessage(Message msg) { + boolean retVal; + + switch (msg.what) { + case EVENT_CONNECT: + if (DBG) log("DcDisconnectingState msg.what=EVENT_CONNECT. Defer. RefCount = " + + mRefCount); + deferMessage(msg); + retVal = HANDLED; + break; + + case EVENT_DEACTIVATE_DONE: + if (DBG) log("DcDisconnectingState msg.what=EVENT_DEACTIVATE_DONE"); + AsyncResult ar = (AsyncResult) msg.obj; + DisconnectParams dp = (DisconnectParams) ar.userObj; + if (dp.tag == mTag) { + // Transition to inactive but send notifications after + // we've entered the mInactive state. + mInactiveState.setEnterNotificationParams((DisconnectParams) ar.userObj); + transitionTo(mInactiveState); + } else { + if (DBG) log("DcDisconnectState EVENT_DEACTIVATE_DONE stale dp.tag=" + + dp.tag + " mTag=" + mTag); + } + retVal = HANDLED; + break; + + default: + if (VDBG) { + log("DcDisconnectingState not handled msg.what=0x" + + Integer.toHexString(msg.what)); + } + retVal = NOT_HANDLED; + break; + } + return retVal; + } + } + private DcDisconnectingState mDisconnectingState = new DcDisconnectingState(); + + /** + * The state machine is disconnecting after an creating a connection. + */ + private class DcDisconnectionErrorCreatingConnection extends State { + @Override + public boolean processMessage(Message msg) { + boolean retVal; + + switch (msg.what) { + case EVENT_DEACTIVATE_DONE: + AsyncResult ar = (AsyncResult) msg.obj; + ConnectionParams cp = (ConnectionParams) ar.userObj; + if (cp.tag == mTag) { + if (DBG) { + log("DcDisconnectionErrorCreatingConnection" + + " msg.what=EVENT_DEACTIVATE_DONE"); + } + + // Transition to inactive but send notifications after + // we've entered the mInactive state. + mInactiveState.setEnterNotificationParams(cp, + FailCause.UNACCEPTABLE_NETWORK_PARAMETER, -1); + transitionTo(mInactiveState); + } else { + if (DBG) { + log("DcDisconnectionErrorCreatingConnection EVENT_DEACTIVATE_DONE" + + " stale dp.tag=" + cp.tag + ", mTag=" + mTag); + } + } + retVal = HANDLED; + break; + + default: + if (VDBG) { + log("DcDisconnectionErrorCreatingConnection not handled msg.what=0x" + + Integer.toHexString(msg.what)); + } + retVal = NOT_HANDLED; + break; + } + return retVal; + } + } + private DcDisconnectionErrorCreatingConnection mDisconnectingErrorCreatingConnection = + new DcDisconnectionErrorCreatingConnection(); + + // ******* public interface + + /** + * Bring up a connection to the apn and return an AsyncResult in onCompletedMsg. + * Used for cellular networks that use Acesss Point Names (APN) such + * as GSM networks. + * + * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object. + * With AsyncResult.userObj set to the original msg.obj, + * AsyncResult.result = FailCause and AsyncResult.exception = Exception(). + * @param apn is the Access Point Name to bring up a connection to + */ + public void bringUp(Message onCompletedMsg, ApnSetting apn) { + sendMessage(obtainMessage(EVENT_CONNECT, new ConnectionParams(apn, onCompletedMsg))); + } + + /** + * Tear down the connection through the apn on the network. + * + * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object. + * With AsyncResult.userObj set to the original msg.obj. + */ + public void tearDown(String reason, Message onCompletedMsg) { + sendMessage(obtainMessage(EVENT_DISCONNECT, new DisconnectParams(reason, onCompletedMsg))); + } + + /** + * Tear down the connection through the apn on the network. Ignores refcount and + * and always tears down. + * + * @param onCompletedMsg is sent with its msg.obj as an AsyncResult object. + * With AsyncResult.userObj set to the original msg.obj. + */ + public void tearDownAll(String reason, Message onCompletedMsg) { + sendMessage(obtainMessage(EVENT_DISCONNECT_ALL, + new DisconnectParams(reason, onCompletedMsg))); + } + + /** + * @return the string for msg.what as our info. + */ + @Override + protected String getWhatToString(int what) { + String info = null; + info = cmdToString(what); + if (info == null) { + info = DataConnectionAc.cmdToString(what); + } + return info; + } + + /** + * Dump the current state. + * + * @param fd + * @param pw + * @param args + */ + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.print("DataConnection "); + super.dump(fd, pw, args); + pw.println(" mApnList=" + mApnList); + pw.flush(); + pw.println(" mDataConnectionTracker=" + mDataConnectionTracker); + pw.println(" mApn=" + mApn); + pw.println(" mTag=" + mTag); + pw.flush(); + pw.println(" phone=" + phone); + pw.println(" mRilVersion=" + mRilVersion); + pw.println(" cid=" + cid); + pw.flush(); + pw.println(" mLinkProperties=" + mLinkProperties); + pw.flush(); + pw.println(" mCapabilities=" + mCapabilities); + pw.println(" createTime=" + TimeUtils.logTimeOfDay(createTime)); + pw.println(" lastFailTime=" + TimeUtils.logTimeOfDay(lastFailTime)); + pw.println(" lastFailCause=" + lastFailCause); + pw.flush(); + pw.println(" mRetryOverride=" + mRetryOverride); + pw.println(" mRefCount=" + mRefCount); + pw.println(" userData=" + userData); + if (mRetryMgr != null) pw.println(" " + mRetryMgr); + pw.flush(); + } +} diff --git a/src/java/com/android/internal/telephony/DataConnectionAc.java b/src/java/com/android/internal/telephony/DataConnectionAc.java new file mode 100644 index 0000000..a24414f --- /dev/null +++ b/src/java/com/android/internal/telephony/DataConnectionAc.java @@ -0,0 +1,591 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import com.android.internal.telephony.DataConnection.UpdateLinkPropertyResult; +import com.android.internal.util.AsyncChannel; +import com.android.internal.util.Protocol; + +import android.app.PendingIntent; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.net.ProxyProperties; +import android.os.Message; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * AsyncChannel to a DataConnection + */ +public class DataConnectionAc extends AsyncChannel { + private static final boolean DBG = false; + private String mLogTag; + + public DataConnection dataConnection; + + public static final int BASE = Protocol.BASE_DATA_CONNECTION_AC; + + public static final int REQ_IS_INACTIVE = BASE + 0; + public static final int RSP_IS_INACTIVE = BASE + 1; + + public static final int REQ_GET_CID = BASE + 2; + public static final int RSP_GET_CID = BASE + 3; + + public static final int REQ_GET_APNSETTING = BASE + 4; + public static final int RSP_GET_APNSETTING = BASE + 5; + + public static final int REQ_GET_LINK_PROPERTIES = BASE + 6; + public static final int RSP_GET_LINK_PROPERTIES = BASE + 7; + + public static final int REQ_SET_LINK_PROPERTIES_HTTP_PROXY = BASE + 8; + public static final int RSP_SET_LINK_PROPERTIES_HTTP_PROXY = BASE + 9; + + public static final int REQ_GET_LINK_CAPABILITIES = BASE + 10; + public static final int RSP_GET_LINK_CAPABILITIES = BASE + 11; + + public static final int REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE = BASE + 12; + public static final int RSP_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE = BASE + 13; + + public static final int REQ_RESET = BASE + 14; + public static final int RSP_RESET = BASE + 15; + + public static final int REQ_GET_REFCOUNT = BASE + 16; + public static final int RSP_GET_REFCOUNT = BASE + 17; + + public static final int REQ_ADD_APNCONTEXT = BASE + 18; + public static final int RSP_ADD_APNCONTEXT = BASE + 19; + + public static final int REQ_REMOVE_APNCONTEXT = BASE + 20; + public static final int RSP_REMOVE_APNCONTEXT = BASE + 21; + + public static final int REQ_GET_APNCONTEXT_LIST = BASE + 22; + public static final int RSP_GET_APNCONTEXT_LIST = BASE + 23; + + public static final int REQ_SET_RECONNECT_INTENT = BASE + 24; + public static final int RSP_SET_RECONNECT_INTENT = BASE + 25; + + public static final int REQ_GET_RECONNECT_INTENT = BASE + 26; + public static final int RSP_GET_RECONNECT_INTENT = BASE + 27; + + private static final int CMD_TO_STRING_COUNT = RSP_GET_RECONNECT_INTENT - BASE + 1; + private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; + static { + sCmdToString[REQ_IS_INACTIVE - BASE] = "REQ_IS_INACTIVE"; + sCmdToString[RSP_IS_INACTIVE - BASE] = "RSP_IS_INACTIVE"; + sCmdToString[REQ_GET_CID - BASE] = "REQ_GET_CID"; + sCmdToString[RSP_GET_CID - BASE] = "RSP_GET_CID"; + sCmdToString[REQ_GET_APNSETTING - BASE] = "REQ_GET_APNSETTING"; + sCmdToString[RSP_GET_APNSETTING - BASE] = "RSP_GET_APNSETTING"; + sCmdToString[REQ_GET_LINK_PROPERTIES - BASE] = "REQ_GET_LINK_PROPERTIES"; + sCmdToString[RSP_GET_LINK_PROPERTIES - BASE] = "RSP_GET_LINK_PROPERTIES"; + sCmdToString[REQ_SET_LINK_PROPERTIES_HTTP_PROXY - BASE] = + "REQ_SET_LINK_PROPERTIES_HTTP_PROXY"; + sCmdToString[RSP_SET_LINK_PROPERTIES_HTTP_PROXY - BASE] = + "RSP_SET_LINK_PROPERTIES_HTTP_PROXY"; + sCmdToString[REQ_GET_LINK_CAPABILITIES - BASE] = "REQ_GET_LINK_CAPABILITIES"; + sCmdToString[RSP_GET_LINK_CAPABILITIES - BASE] = "RSP_GET_LINK_CAPABILITIES"; + sCmdToString[REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE - BASE] = + "REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE"; + sCmdToString[RSP_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE - BASE] = + "RSP_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE"; + sCmdToString[REQ_RESET - BASE] = "REQ_RESET"; + sCmdToString[RSP_RESET - BASE] = "RSP_RESET"; + sCmdToString[REQ_GET_REFCOUNT - BASE] = "REQ_GET_REFCOUNT"; + sCmdToString[RSP_GET_REFCOUNT - BASE] = "RSP_GET_REFCOUNT"; + sCmdToString[REQ_ADD_APNCONTEXT - BASE] = "REQ_ADD_APNCONTEXT"; + sCmdToString[RSP_ADD_APNCONTEXT - BASE] = "RSP_ADD_APNCONTEXT"; + sCmdToString[REQ_REMOVE_APNCONTEXT - BASE] = "REQ_REMOVE_APNCONTEXT"; + sCmdToString[RSP_REMOVE_APNCONTEXT - BASE] = "RSP_REMOVE_APNCONTEXT"; + sCmdToString[REQ_GET_APNCONTEXT_LIST - BASE] = "REQ_GET_APNCONTEXT_LIST"; + sCmdToString[RSP_GET_APNCONTEXT_LIST - BASE] = "RSP_GET_APNCONTEXT_LIST"; + sCmdToString[REQ_SET_RECONNECT_INTENT - BASE] = "REQ_SET_RECONNECT_INTENT"; + sCmdToString[RSP_SET_RECONNECT_INTENT - BASE] = "RSP_SET_RECONNECT_INTENT"; + sCmdToString[REQ_GET_RECONNECT_INTENT - BASE] = "REQ_GET_RECONNECT_INTENT"; + sCmdToString[RSP_GET_RECONNECT_INTENT - BASE] = "RSP_GET_RECONNECT_INTENT"; + } + protected static String cmdToString(int cmd) { + cmd -= BASE; + if ((cmd >= 0) && (cmd < sCmdToString.length)) { + return sCmdToString[cmd]; + } else { + return AsyncChannel.cmdToString(cmd + BASE); + } + } + + /** + * enum used to notify action taken or necessary to be + * taken after the link property is changed. + */ + public enum LinkPropertyChangeAction { + NONE, CHANGED, RESET; + + public static LinkPropertyChangeAction fromInt(int value) { + if (value == NONE.ordinal()) { + return NONE; + } else if (value == CHANGED.ordinal()) { + return CHANGED; + } else if (value == RESET.ordinal()) { + return RESET; + } else { + throw new RuntimeException("LinkPropertyChangeAction.fromInt: bad value=" + value); + } + } + } + + public DataConnectionAc(DataConnection dc, String logTag) { + dataConnection = dc; + mLogTag = logTag; + } + + /** + * Request if the state machine is in the inactive state. + * Response {@link #rspIsInactive} + */ + public void reqIsInactive() { + sendMessage(REQ_IS_INACTIVE); + if (DBG) log("reqIsInactive"); + } + + /** + * Evaluate RSP_IS_INACTIVE. + * + * @return true if the state machine is in the inactive state. + */ + public boolean rspIsInactive(Message response) { + boolean retVal = response.arg1 == 1; + if (DBG) log("rspIsInactive=" + retVal); + return retVal; + } + + /** + * @return true if the state machine is in the inactive state. + */ + public boolean isInactiveSync() { + Message response = sendMessageSynchronously(REQ_IS_INACTIVE); + if ((response != null) && (response.what == RSP_IS_INACTIVE)) { + return rspIsInactive(response); + } else { + log("rspIsInactive error response=" + response); + return false; + } + } + + /** + * Request the Connection ID. + * Response {@link #rspCid} + */ + public void reqCid() { + sendMessage(REQ_GET_CID); + if (DBG) log("reqCid"); + } + + /** + * Evaluate a RSP_GET_CID message and return the cid. + * + * @param response Message + * @return connection id or -1 if an error + */ + public int rspCid(Message response) { + int retVal = response.arg1; + if (DBG) log("rspCid=" + retVal); + return retVal; + } + + /** + * @return connection id or -1 if an error + */ + public int getCidSync() { + Message response = sendMessageSynchronously(REQ_GET_CID); + if ((response != null) && (response.what == RSP_GET_CID)) { + return rspCid(response); + } else { + log("rspCid error response=" + response); + return -1; + } + } + + /** + * Request the Reference Count. + * Response {@link #rspRefCount} + */ + public void reqRefCount() { + sendMessage(REQ_GET_REFCOUNT); + if (DBG) log("reqRefCount"); + } + + /** + * Evaluate a RSP_GET_REFCOUNT message and return the refCount. + * + * @param response Message + * @return ref count or -1 if an error + */ + public int rspRefCount(Message response) { + int retVal = response.arg1; + if (DBG) log("rspRefCount=" + retVal); + return retVal; + } + + /** + * @return connection id or -1 if an error + */ + public int getRefCountSync() { + Message response = sendMessageSynchronously(REQ_GET_REFCOUNT); + if ((response != null) && (response.what == RSP_GET_REFCOUNT)) { + return rspRefCount(response); + } else { + log("rspRefCount error response=" + response); + return -1; + } + } + + /** + * Request the connections ApnSetting. + * Response {@link #rspApnSetting} + */ + public void reqApnSetting() { + sendMessage(REQ_GET_APNSETTING); + if (DBG) log("reqApnSetting"); + } + + /** + * Evaluate a RSP_APN_SETTING message and return the ApnSetting. + * + * @param response Message + * @return ApnSetting, maybe null + */ + public ApnSetting rspApnSetting(Message response) { + ApnSetting retVal = (ApnSetting) response.obj; + if (DBG) log("rspApnSetting=" + retVal); + return retVal; + } + + /** + * Get the connections ApnSetting. + * + * @return ApnSetting or null if an error + */ + public ApnSetting getApnSettingSync() { + Message response = sendMessageSynchronously(REQ_GET_APNSETTING); + if ((response != null) && (response.what == RSP_GET_APNSETTING)) { + return rspApnSetting(response); + } else { + log("getApnSetting error response=" + response); + return null; + } + } + + /** + * Request the connections LinkProperties. + * Response {@link #rspLinkProperties} + */ + public void reqLinkProperties() { + sendMessage(REQ_GET_LINK_PROPERTIES); + if (DBG) log("reqLinkProperties"); + } + + /** + * Evaluate RSP_GET_LINK_PROPERTIES + * + * @param response + * @return LinkProperties, maybe null. + */ + public LinkProperties rspLinkProperties(Message response) { + LinkProperties retVal = (LinkProperties) response.obj; + if (DBG) log("rspLinkProperties=" + retVal); + return retVal; + } + + /** + * Get the connections LinkProperties. + * + * @return LinkProperties or null if an error + */ + public LinkProperties getLinkPropertiesSync() { + Message response = sendMessageSynchronously(REQ_GET_LINK_PROPERTIES); + if ((response != null) && (response.what == RSP_GET_LINK_PROPERTIES)) { + return rspLinkProperties(response); + } else { + log("getLinkProperties error response=" + response); + return null; + } + } + + /** + * Request setting the connections LinkProperties.HttpProxy. + * Response RSP_SET_LINK_PROPERTIES when complete. + */ + public void reqSetLinkPropertiesHttpProxy(ProxyProperties proxy) { + sendMessage(REQ_SET_LINK_PROPERTIES_HTTP_PROXY, proxy); + if (DBG) log("reqSetLinkPropertiesHttpProxy proxy=" + proxy); + } + + /** + * Set the connections LinkProperties.HttpProxy + */ + public void setLinkPropertiesHttpProxySync(ProxyProperties proxy) { + Message response = + sendMessageSynchronously(REQ_SET_LINK_PROPERTIES_HTTP_PROXY, proxy); + if ((response != null) && (response.what == RSP_SET_LINK_PROPERTIES_HTTP_PROXY)) { + if (DBG) log("setLinkPropertiesHttpPoxy ok"); + } else { + log("setLinkPropertiesHttpPoxy error response=" + response); + } + } + + /** + * Request update LinkProperties from DataCallState + * Response {@link #rspUpdateLinkPropertiesDataCallState} + */ + public void reqUpdateLinkPropertiesDataCallState(DataCallState newState) { + sendMessage(REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE, newState); + if (DBG) log("reqUpdateLinkPropertiesDataCallState"); + } + + public UpdateLinkPropertyResult rspUpdateLinkPropertiesDataCallState(Message response) { + UpdateLinkPropertyResult retVal = (UpdateLinkPropertyResult)response.obj; + if (DBG) log("rspUpdateLinkPropertiesState: retVal=" + retVal); + return retVal; + } + + /** + * Update link properties in the data connection + * + * @return the removed and added addresses. + */ + public UpdateLinkPropertyResult updateLinkPropertiesDataCallStateSync(DataCallState newState) { + Message response = + sendMessageSynchronously(REQ_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE, newState); + if ((response != null) && + (response.what == RSP_UPDATE_LINK_PROPERTIES_DATA_CALL_STATE)) { + return rspUpdateLinkPropertiesDataCallState(response); + } else { + log("getLinkProperties error response=" + response); + return new UpdateLinkPropertyResult(new LinkProperties()); + } + } + + /** + * Request the connections LinkCapabilities. + * Response {@link #rspLinkCapabilities} + */ + public void reqLinkCapabilities() { + sendMessage(REQ_GET_LINK_CAPABILITIES); + if (DBG) log("reqLinkCapabilities"); + } + + /** + * Evaluate RSP_GET_LINK_CAPABILITIES + * + * @param response + * @return LinkCapabilites, maybe null. + */ + public LinkCapabilities rspLinkCapabilities(Message response) { + LinkCapabilities retVal = (LinkCapabilities) response.obj; + if (DBG) log("rspLinkCapabilities=" + retVal); + return retVal; + } + + /** + * Get the connections LinkCapabilities. + * + * @return LinkCapabilities or null if an error + */ + public LinkCapabilities getLinkCapabilitiesSync() { + Message response = sendMessageSynchronously(REQ_GET_LINK_CAPABILITIES); + if ((response != null) && (response.what == RSP_GET_LINK_CAPABILITIES)) { + return rspLinkCapabilities(response); + } else { + log("getLinkCapabilities error response=" + response); + return null; + } + } + + /** + * Request the connections LinkCapabilities. + * Response RSP_RESET when complete + */ + public void reqReset() { + sendMessage(REQ_RESET); + if (DBG) log("reqReset"); + } + + /** + * Reset the connection and wait for it to complete. + */ + public void resetSync() { + Message response = sendMessageSynchronously(REQ_RESET); + if ((response != null) && (response.what == RSP_RESET)) { + if (DBG) log("restSync ok"); + } else { + log("restSync error response=" + response); + } + } + + /** + * Request to add ApnContext association. + * Response RSP_ADD_APNCONTEXT when complete. + */ + public void reqAddApnContext(ApnContext apnContext) { + Message response = sendMessageSynchronously(REQ_ADD_APNCONTEXT, apnContext); + if (DBG) log("reqAddApnContext"); + } + + /** + * Add ApnContext association synchronoulsy. + * + * @param ApnContext to associate + */ + public void addApnContextSync(ApnContext apnContext) { + Message response = sendMessageSynchronously(REQ_ADD_APNCONTEXT, apnContext); + if ((response != null) && (response.what == RSP_ADD_APNCONTEXT)) { + if (DBG) log("addApnContext ok"); + } else { + log("addApnContext error response=" + response); + } + } + + /** + * Request to remove ApnContext association. + * Response RSP_REMOVE_APNCONTEXT when complete. + */ + public void reqRemomveApnContext(ApnContext apnContext) { + Message response = sendMessageSynchronously(REQ_REMOVE_APNCONTEXT, apnContext); + if (DBG) log("reqRemomveApnContext"); + } + + /** + * Remove ApnContext associateion. + * + * @param ApnContext to dissociate + */ + public void removeApnContextSync(ApnContext apnContext) { + Message response = sendMessageSynchronously(REQ_REMOVE_APNCONTEXT, apnContext); + if ((response != null) && (response.what == RSP_REMOVE_APNCONTEXT)) { + if (DBG) log("removeApnContext ok"); + } else { + log("removeApnContext error response=" + response); + } + } + + /** + * Request to retrive ApnContext List associated with DC. + * Response RSP_GET_APNCONTEXT_LIST when complete. + */ + public void reqGetApnList(ApnContext apnContext) { + Message response = sendMessageSynchronously(REQ_GET_APNCONTEXT_LIST); + if (DBG) log("reqGetApnList"); + } + + /** + * Retrieve Collection of ApnContext from the response message. + * + * @param Message sent from DC in response to REQ_GET_APNCONTEXT_LIST. + * @return Collection of ApnContext + */ + public Collection<ApnContext> rspApnList(Message response) { + Collection<ApnContext> retVal = (Collection<ApnContext>)response.obj; + if (retVal == null) retVal = new ArrayList<ApnContext>(); + return retVal; + } + + /** + * Retrieve collection of ApnContext currently associated with + * the DataConnectionA synchronously. + * + * @return Collection of ApnContext + */ + public Collection<ApnContext> getApnListSync() { + Message response = sendMessageSynchronously(REQ_GET_APNCONTEXT_LIST); + if ((response != null) && (response.what == RSP_GET_APNCONTEXT_LIST)) { + if (DBG) log("getApnList ok"); + return rspApnList(response); + } else { + log("getApnList error response=" + response); + // return dummy list with no entry + return new ArrayList<ApnContext>(); + } + } + + /** + * Request to set Pending ReconnectIntent to DC. + * Response RSP_SET_RECONNECT_INTENT when complete. + */ + public void reqSetReconnectIntent(PendingIntent intent) { + Message response = sendMessageSynchronously(REQ_SET_RECONNECT_INTENT, intent); + if (DBG) log("reqSetReconnectIntent"); + } + + /** + * Set pending reconnect intent to DC synchronously. + * + * @param PendingIntent to set. + */ + public void setReconnectIntentSync(PendingIntent intent) { + Message response = sendMessageSynchronously(REQ_SET_RECONNECT_INTENT, intent); + if ((response != null) && (response.what == RSP_SET_RECONNECT_INTENT)) { + if (DBG) log("setReconnectIntent ok"); + } else { + log("setReconnectIntent error response=" + response); + } + } + + /** + * Request to get Pending ReconnectIntent to DC. + * Response RSP_GET_RECONNECT_INTENT when complete. + */ + public void reqGetReconnectIntent() { + Message response = sendMessageSynchronously(REQ_GET_RECONNECT_INTENT); + if (DBG) log("reqGetReconnectIntent"); + } + + /** + * Retrieve reconnect intent from response message from DC. + * + * @param Message which contains the reconnect intent. + * @return PendingIntent from the response. + */ + public PendingIntent rspReconnectIntent(Message response) { + PendingIntent retVal = (PendingIntent) response.obj; + return retVal; + } + + /** + * Retrieve reconnect intent currently set in DC synchronously. + * + * @return PendingIntent reconnect intent current ly set in DC + */ + public PendingIntent getReconnectIntentSync() { + Message response = sendMessageSynchronously(REQ_GET_RECONNECT_INTENT); + if ((response != null) && (response.what == RSP_GET_RECONNECT_INTENT)) { + if (DBG) log("getReconnectIntent ok"); + return rspReconnectIntent(response); + } else { + log("getReconnectIntent error response=" + response); + return null; + } + } + + @Override + public String toString() { + return dataConnection.getName(); + } + + private void log(String s) { + android.util.Log.d(mLogTag, "DataConnectionAc " + s); + } +} diff --git a/src/java/com/android/internal/telephony/DataConnectionTracker.java b/src/java/com/android/internal/telephony/DataConnectionTracker.java new file mode 100644 index 0000000..89a02d3 --- /dev/null +++ b/src/java/com/android/internal/telephony/DataConnectionTracker.java @@ -0,0 +1,1203 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.net.NetworkInfo; +import android.net.TrafficStats; +import android.net.wifi.WifiManager; +import android.os.AsyncResult; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Messenger; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.telephony.DataConnection.FailCause; +import com.android.internal.telephony.DctConstants; +import com.android.internal.util.AsyncChannel; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * {@hide} + */ +public abstract class DataConnectionTracker extends Handler { + protected static final boolean DBG = true; + protected static final boolean VDBG = false; + + + /** Delay between APN attempts. + Note the property override mechanism is there just for testing purpose only. */ + protected static final int APN_DELAY_MILLIS = + SystemProperties.getInt("persist.radio.apn_delay", 5000); + + protected Object mDataEnabledLock = new Object(); + + // responds to the setInternalDataEnabled call - used internally to turn off data + // for example during emergency calls + protected boolean mInternalDataEnabled = true; + + // responds to public (user) API to enable/disable data use + // independent of mInternalDataEnabled and requests for APN access + // persisted + protected boolean mUserDataEnabled = true; + + // TODO: move away from static state once 5587429 is fixed. + protected static boolean sPolicyDataEnabled = true; + + private boolean[] dataEnabled = new boolean[DctConstants.APN_NUM_TYPES]; + + private int enabledCount = 0; + + /* Currently requested APN type (TODO: This should probably be a parameter not a member) */ + protected String mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT; + + /** Retry configuration: A doubling of retry times from 5secs to 30minutes */ + protected static final String DEFAULT_DATA_RETRY_CONFIG = "default_randomization=2000," + + "5000,10000,20000,40000,80000:5000,160000:5000," + + "320000:5000,640000:5000,1280000:5000,1800000:5000"; + + /** Retry configuration for secondary networks: 4 tries in 20 sec */ + protected static final String SECONDARY_DATA_RETRY_CONFIG = + "max_retries=3, 5000, 5000, 5000"; + + /** Slow poll when attempting connection recovery. */ + protected static final int POLL_NETSTAT_SLOW_MILLIS = 5000; + /** Default max failure count before attempting to network re-registration. */ + protected static final int DEFAULT_MAX_PDP_RESET_FAIL = 3; + + /** + * After detecting a potential connection problem, this is the max number + * of subsequent polls before attempting recovery. + */ + protected static final int NO_RECV_POLL_LIMIT = 24; + // 1 sec. default polling interval when screen is on. + protected static final int POLL_NETSTAT_MILLIS = 1000; + // 10 min. default polling interval when screen is off. + protected static final int POLL_NETSTAT_SCREEN_OFF_MILLIS = 1000*60*10; + // 2 min for round trip time + protected static final int POLL_LONGEST_RTT = 120 * 1000; + // Default sent packets without ack which triggers initial recovery steps + protected static final int NUMBER_SENT_PACKETS_OF_HANG = 10; + // how long to wait before switching back to default APN + protected static final int RESTORE_DEFAULT_APN_DELAY = 1 * 60 * 1000; + // system property that can override the above value + protected static final String APN_RESTORE_DELAY_PROP_NAME = "android.telephony.apn-restore"; + // represents an invalid IP address + protected static final String NULL_IP = "0.0.0.0"; + + // Default for the data stall alarm while non-aggressive stall detection + protected static final int DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS_DEFAULT = 1000 * 60 * 6; + // Default for the data stall alarm for aggressive stall detection + protected static final int DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS_DEFAULT = 1000 * 60; + // If attempt is less than this value we're doing first level recovery + protected static final int DATA_STALL_NO_RECV_POLL_LIMIT = 1; + // Tag for tracking stale alarms + protected static final String DATA_STALL_ALARM_TAG_EXTRA = "data.stall.alram.tag"; + + // TODO: See if we can remove INTENT_RECONNECT_ALARM + // having to have different values for GSM and + // CDMA. If so we can then remove the need for + // getActionIntentReconnectAlarm. + protected static final String INTENT_RECONNECT_ALARM_EXTRA_REASON = + "reconnect_alarm_extra_reason"; + + // Used for debugging. Send the INTENT with an optional counter value with the number + // of times the setup is to fail before succeeding. If the counter isn't passed the + // setup will fail once. Example fail two times with FailCause.SIGNAL_LOST(-3) + // adb shell am broadcast \ + // -a com.android.internal.telephony.dataconnectiontracker.intent_set_fail_data_setup_counter \ + // --ei fail_data_setup_counter 3 --ei fail_data_setup_fail_cause -3 + protected static final String INTENT_SET_FAIL_DATA_SETUP_COUNTER = + "com.android.internal.telephony.dataconnectiontracker.intent_set_fail_data_setup_counter"; + protected static final String FAIL_DATA_SETUP_COUNTER = "fail_data_setup_counter"; + protected int mFailDataSetupCounter = 0; + protected static final String FAIL_DATA_SETUP_FAIL_CAUSE = "fail_data_setup_fail_cause"; + protected FailCause mFailDataSetupFailCause = FailCause.ERROR_UNSPECIFIED; + + protected static final String DEFALUT_DATA_ON_BOOT_PROP = "net.def_data_on_boot"; + + // member variables + protected PhoneBase mPhone; + protected DctConstants.Activity mActivity = DctConstants.Activity.NONE; + protected DctConstants.State mState = DctConstants.State.IDLE; + protected Handler mDataConnectionTracker = null; + + + protected long mTxPkts; + protected long mRxPkts; + protected int mNetStatPollPeriod; + protected boolean mNetStatPollEnabled = false; + + protected TxRxSum mDataStallTxRxSum = new TxRxSum(0, 0); + // Used to track stale data stall alarms. + protected int mDataStallAlarmTag = (int) SystemClock.elapsedRealtime(); + // The current data stall alarm intent + protected PendingIntent mDataStallAlarmIntent = null; + // Number of packets sent since the last received packet + protected long mSentSinceLastRecv; + // Controls when a simple recovery attempt it to be tried + protected int mNoRecvPollCount = 0; + + // wifi connection status will be updated by sticky intent + protected boolean mIsWifiConnected = false; + + /** Intent sent when the reconnect alarm fires. */ + protected PendingIntent mReconnectIntent = null; + + /** CID of active data connection */ + protected int mCidActive; + + // When false we will not auto attach and manually attaching is required. + protected boolean mAutoAttachOnCreation = false; + + // State of screen + // (TODO: Reconsider tying directly to screen, maybe this is + // really a lower power mode") + protected boolean mIsScreenOn = true; + + /** Allows the generation of unique Id's for DataConnection objects */ + protected AtomicInteger mUniqueIdGenerator = new AtomicInteger(0); + + /** The data connections. */ + protected HashMap<Integer, DataConnection> mDataConnections = + new HashMap<Integer, DataConnection>(); + + /** The data connection async channels */ + protected HashMap<Integer, DataConnectionAc> mDataConnectionAsyncChannels = + new HashMap<Integer, DataConnectionAc>(); + + /** Convert an ApnType string to Id (TODO: Use "enumeration" instead of String for ApnType) */ + protected HashMap<String, Integer> mApnToDataConnectionId = + new HashMap<String, Integer>(); + + /** Phone.APN_TYPE_* ===> ApnContext */ + protected ConcurrentHashMap<String, ApnContext> mApnContexts = + new ConcurrentHashMap<String, ApnContext>(); + + /* Currently active APN */ + protected ApnSetting mActiveApn; + + /** allApns holds all apns */ + protected ArrayList<ApnSetting> mAllApns = null; + + /** preferred apn */ + protected ApnSetting mPreferredApn = null; + + /** Is packet service restricted by network */ + protected boolean mIsPsRestricted = false; + + /* Once disposed dont handle any messages */ + protected boolean mIsDisposed = false; + + protected BroadcastReceiver mIntentReceiver = new BroadcastReceiver () + { + @Override + public void onReceive(Context context, Intent intent) + { + String action = intent.getAction(); + if (DBG) log("onReceive: action=" + action); + if (action.equals(Intent.ACTION_SCREEN_ON)) { + mIsScreenOn = true; + stopNetStatPoll(); + startNetStatPoll(); + restartDataStallAlarm(); + } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { + mIsScreenOn = false; + stopNetStatPoll(); + startNetStatPoll(); + restartDataStallAlarm(); + } else if (action.startsWith(getActionIntentReconnectAlarm())) { + log("Reconnect alarm. Previous state was " + mState); + onActionIntentReconnectAlarm(intent); + } else if (action.equals(getActionIntentDataStallAlarm())) { + onActionIntentDataStallAlarm(intent); + } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { + final android.net.NetworkInfo networkInfo = (NetworkInfo) + intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); + mIsWifiConnected = (networkInfo != null && networkInfo.isConnected()); + } else if (action.equals(WifiManager.WIFI_STATE_CHANGED_ACTION)) { + final boolean enabled = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, + WifiManager.WIFI_STATE_UNKNOWN) == WifiManager.WIFI_STATE_ENABLED; + + if (!enabled) { + // when WiFi got disabled, the NETWORK_STATE_CHANGED_ACTION + // quit and won't report disconnected until next enabling. + mIsWifiConnected = false; + } + } else if (action.equals(INTENT_SET_FAIL_DATA_SETUP_COUNTER)) { + mFailDataSetupCounter = intent.getIntExtra(FAIL_DATA_SETUP_COUNTER, 1); + mFailDataSetupFailCause = FailCause.fromInt( + intent.getIntExtra(FAIL_DATA_SETUP_FAIL_CAUSE, + FailCause.ERROR_UNSPECIFIED.getErrorCode())); + if (DBG) log("set mFailDataSetupCounter=" + mFailDataSetupCounter + + " mFailDataSetupFailCause=" + mFailDataSetupFailCause); + } + } + }; + + private final DataRoamingSettingObserver mDataRoamingSettingObserver; + + private class DataRoamingSettingObserver extends ContentObserver { + public DataRoamingSettingObserver(Handler handler) { + super(handler); + } + + public void register(Context context) { + final ContentResolver resolver = context.getContentResolver(); + resolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.DATA_ROAMING), false, this); + } + + public void unregister(Context context) { + final ContentResolver resolver = context.getContentResolver(); + resolver.unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange) { + // already running on mPhone handler thread + handleDataOnRoamingChange(); + } + } + + /** + * Maintian the sum of transmit and receive packets. + * + * The packet counts are initizlied and reset to -1 and + * remain -1 until they can be updated. + */ + public class TxRxSum { + public long txPkts; + public long rxPkts; + + public TxRxSum() { + reset(); + } + + public TxRxSum(long txPkts, long rxPkts) { + this.txPkts = txPkts; + this.rxPkts = rxPkts; + } + + public TxRxSum(TxRxSum sum) { + txPkts = sum.txPkts; + rxPkts = sum.rxPkts; + } + + public void reset() { + txPkts = -1; + rxPkts = -1; + } + + public String toString() { + return "{txSum=" + txPkts + " rxSum=" + rxPkts + "}"; + } + + public void updateTxRxSum() { + boolean txUpdated = false, rxUpdated = false; + long txSum = 0, rxSum = 0; + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.getState() == DctConstants.State.CONNECTED) { + DataConnectionAc dcac = apnContext.getDataConnectionAc(); + if (dcac == null) continue; + + LinkProperties linkProp = dcac.getLinkPropertiesSync(); + if (linkProp == null) continue; + + String iface = linkProp.getInterfaceName(); + + if (iface != null) { + long stats = TrafficStats.getTxPackets(iface); + if (stats > 0) { + txUpdated = true; + txSum += stats; + } + stats = TrafficStats.getRxPackets(iface); + if (stats > 0) { + rxUpdated = true; + rxSum += stats; + } + } + } + } + if (txUpdated) this.txPkts = txSum; + if (rxUpdated) this.rxPkts = rxSum; + } + } + + protected boolean isDataSetupCompleteOk(AsyncResult ar) { + if (ar.exception != null) { + if (DBG) log("isDataSetupCompleteOk return false, ar.result=" + ar.result); + return false; + } + if (mFailDataSetupCounter <= 0) { + if (DBG) log("isDataSetupCompleteOk return true"); + return true; + } + ar.result = mFailDataSetupFailCause; + if (DBG) { + log("isDataSetupCompleteOk return false" + + " mFailDataSetupCounter=" + mFailDataSetupCounter + + " mFailDataSetupFailCause=" + mFailDataSetupFailCause); + } + mFailDataSetupCounter -= 1; + return false; + } + + protected void onActionIntentReconnectAlarm(Intent intent) { + String reason = intent.getStringExtra(INTENT_RECONNECT_ALARM_EXTRA_REASON); + if (mState == DctConstants.State.FAILED) { + Message msg = obtainMessage(DctConstants.EVENT_CLEAN_UP_CONNECTION); + msg.arg1 = 0; // tearDown is false + msg.arg2 = 0; + msg.obj = reason; + sendMessage(msg); + } + sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA)); + } + + protected void onActionIntentDataStallAlarm(Intent intent) { + if (VDBG) log("onActionIntentDataStallAlarm: action=" + intent.getAction()); + Message msg = obtainMessage(DctConstants.EVENT_DATA_STALL_ALARM, + intent.getAction()); + msg.arg1 = intent.getIntExtra(DATA_STALL_ALARM_TAG_EXTRA, 0); + sendMessage(msg); + } + + /** + * Default constructor + */ + protected DataConnectionTracker(PhoneBase phone) { + super(); + if (DBG) log("DCT.constructor"); + mPhone = phone; + + IntentFilter filter = new IntentFilter(); + filter.addAction(getActionIntentReconnectAlarm()); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); + filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); + filter.addAction(INTENT_SET_FAIL_DATA_SETUP_COUNTER); + + mUserDataEnabled = Settings.Secure.getInt( + mPhone.getContext().getContentResolver(), Settings.Secure.MOBILE_DATA, 1) == 1; + + // TODO: Why is this registering the phone as the receiver of the intent + // and not its own handler? + mPhone.getContext().registerReceiver(mIntentReceiver, filter, null, mPhone); + + // This preference tells us 1) initial condition for "dataEnabled", + // and 2) whether the RIL will setup the baseband to auto-PS attach. + + dataEnabled[DctConstants.APN_DEFAULT_ID] = + SystemProperties.getBoolean(DEFALUT_DATA_ON_BOOT_PROP,true); + if (dataEnabled[DctConstants.APN_DEFAULT_ID]) { + enabledCount++; + } + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mPhone.getContext()); + mAutoAttachOnCreation = sp.getBoolean(PhoneBase.DATA_DISABLED_ON_BOOT_KEY, false); + + // watch for changes to Settings.Secure.DATA_ROAMING + mDataRoamingSettingObserver = new DataRoamingSettingObserver(mPhone); + mDataRoamingSettingObserver.register(mPhone.getContext()); + } + + public void dispose() { + if (DBG) log("DCT.dispose"); + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + dcac.disconnect(); + } + mDataConnectionAsyncChannels.clear(); + mIsDisposed = true; + mPhone.getContext().unregisterReceiver(this.mIntentReceiver); + mDataRoamingSettingObserver.unregister(mPhone.getContext()); + } + + protected void broadcastMessenger() { + Intent intent = new Intent(DctConstants.ACTION_DATA_CONNECTION_TRACKER_MESSENGER); + intent.putExtra(DctConstants.EXTRA_MESSENGER, new Messenger(this)); + mPhone.getContext().sendBroadcast(intent); + } + + public DctConstants.Activity getActivity() { + return mActivity; + } + + public boolean isApnTypeActive(String type) { + // TODO: support simultaneous with List instead + if (PhoneConstants.APN_TYPE_DUN.equals(type)) { + ApnSetting dunApn = fetchDunApn(); + if (dunApn != null) { + return ((mActiveApn != null) && (dunApn.toString().equals(mActiveApn.toString()))); + } + } + return mActiveApn != null && mActiveApn.canHandleType(type); + } + + protected ApnSetting fetchDunApn() { + if (SystemProperties.getBoolean("net.tethering.noprovisioning", false)) { + log("fetchDunApn: net.tethering.noprovisioning=true ret: null"); + return null; + } + Context c = mPhone.getContext(); + String apnData = Settings.Secure.getString(c.getContentResolver(), + Settings.Secure.TETHER_DUN_APN); + ApnSetting dunSetting = ApnSetting.fromString(apnData); + if (dunSetting != null) { + if (VDBG) log("fetchDunApn: secure TETHER_DUN_APN dunSetting=" + dunSetting); + return dunSetting; + } + + apnData = c.getResources().getString(R.string.config_tether_apndata); + dunSetting = ApnSetting.fromString(apnData); + if (VDBG) log("fetchDunApn: config_tether_apndata dunSetting=" + dunSetting); + return dunSetting; + } + + public String[] getActiveApnTypes() { + String[] result; + if (mActiveApn != null) { + result = mActiveApn.types; + } else { + result = new String[1]; + result[0] = PhoneConstants.APN_TYPE_DEFAULT; + } + return result; + } + + /** TODO: See if we can remove */ + public String getActiveApnString(String apnType) { + String result = null; + if (mActiveApn != null) { + result = mActiveApn.apn; + } + return result; + } + + /** + * Modify {@link Settings.Secure#DATA_ROAMING} value. + */ + public void setDataOnRoamingEnabled(boolean enabled) { + if (getDataOnRoamingEnabled() != enabled) { + final ContentResolver resolver = mPhone.getContext().getContentResolver(); + Settings.Secure.putInt(resolver, Settings.Secure.DATA_ROAMING, enabled ? 1 : 0); + // will trigger handleDataOnRoamingChange() through observer + } + } + + /** + * Return current {@link Settings.Secure#DATA_ROAMING} value. + */ + public boolean getDataOnRoamingEnabled() { + try { + final ContentResolver resolver = mPhone.getContext().getContentResolver(); + return Settings.Secure.getInt(resolver, Settings.Secure.DATA_ROAMING) != 0; + } catch (SettingNotFoundException snfe) { + return false; + } + } + + private void handleDataOnRoamingChange() { + if (mPhone.getServiceState().getRoaming()) { + if (getDataOnRoamingEnabled()) { + resetAllRetryCounts(); + } + sendMessage(obtainMessage(DctConstants.EVENT_ROAMING_ON)); + } + } + + // abstract methods + protected abstract String getActionIntentReconnectAlarm(); + protected abstract String getActionIntentDataStallAlarm(); + protected abstract void startNetStatPoll(); + protected abstract void stopNetStatPoll(); + protected abstract void restartDataStallAlarm(); + protected abstract void restartRadio(); + protected abstract void log(String s); + protected abstract void loge(String s); + protected abstract boolean isDataAllowed(); + protected abstract boolean isApnTypeAvailable(String type); + public abstract DctConstants.State getState(String apnType); + protected abstract void setState(DctConstants.State s); + protected abstract void gotoIdleAndNotifyDataConnection(String reason); + + protected abstract boolean onTrySetupData(String reason); + protected abstract void onRoamingOff(); + protected abstract void onRoamingOn(); + protected abstract void onRadioAvailable(); + protected abstract void onRadioOffOrNotAvailable(); + protected abstract void onDataSetupComplete(AsyncResult ar); + protected abstract void onDisconnectDone(int connId, AsyncResult ar); + protected abstract void onVoiceCallStarted(); + protected abstract void onVoiceCallEnded(); + protected abstract void onCleanUpConnection(boolean tearDown, int apnId, String reason); + protected abstract void onCleanUpAllConnections(String cause); + protected abstract boolean isDataPossible(String apnType); + + protected void onDataStallAlarm(int tag) { + loge("onDataStallAlarm: not impleted tag=" + tag); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_DISCONNECTED: { + log("DISCONNECTED_CONNECTED: msg=" + msg); + DataConnectionAc dcac = (DataConnectionAc) msg.obj; + mDataConnectionAsyncChannels.remove(dcac.dataConnection.getDataConnectionId()); + dcac.disconnected(); + break; + } + case DctConstants.EVENT_ENABLE_NEW_APN: + onEnableApn(msg.arg1, msg.arg2); + break; + + case DctConstants.EVENT_TRY_SETUP_DATA: + String reason = null; + if (msg.obj instanceof String) { + reason = (String) msg.obj; + } + onTrySetupData(reason); + break; + + case DctConstants.EVENT_DATA_STALL_ALARM: + onDataStallAlarm(msg.arg1); + break; + + case DctConstants.EVENT_ROAMING_OFF: + if (getDataOnRoamingEnabled() == false) { + resetAllRetryCounts(); + } + onRoamingOff(); + break; + + case DctConstants.EVENT_ROAMING_ON: + onRoamingOn(); + break; + + case DctConstants.EVENT_RADIO_AVAILABLE: + onRadioAvailable(); + break; + + case DctConstants.EVENT_RADIO_OFF_OR_NOT_AVAILABLE: + onRadioOffOrNotAvailable(); + break; + + case DctConstants.EVENT_DATA_SETUP_COMPLETE: + mCidActive = msg.arg1; + onDataSetupComplete((AsyncResult) msg.obj); + break; + + case DctConstants.EVENT_DISCONNECT_DONE: + log("DataConnectoinTracker.handleMessage: EVENT_DISCONNECT_DONE msg=" + msg); + onDisconnectDone(msg.arg1, (AsyncResult) msg.obj); + break; + + case DctConstants.EVENT_VOICE_CALL_STARTED: + onVoiceCallStarted(); + break; + + case DctConstants.EVENT_VOICE_CALL_ENDED: + onVoiceCallEnded(); + break; + + case DctConstants.EVENT_CLEAN_UP_ALL_CONNECTIONS: { + onCleanUpAllConnections((String) msg.obj); + break; + } + case DctConstants.EVENT_CLEAN_UP_CONNECTION: { + boolean tearDown = (msg.arg1 == 0) ? false : true; + onCleanUpConnection(tearDown, msg.arg2, (String) msg.obj); + break; + } + case DctConstants.EVENT_SET_INTERNAL_DATA_ENABLE: { + boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false; + onSetInternalDataEnabled(enabled); + break; + } + case DctConstants.EVENT_RESET_DONE: { + if (DBG) log("EVENT_RESET_DONE"); + onResetDone((AsyncResult) msg.obj); + break; + } + case DctConstants.CMD_SET_USER_DATA_ENABLE: { + final boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false; + if (DBG) log("CMD_SET_USER_DATA_ENABLE enabled=" + enabled); + onSetUserDataEnabled(enabled); + break; + } + case DctConstants.CMD_SET_DEPENDENCY_MET: { + boolean met = (msg.arg1 == DctConstants.ENABLED) ? true : false; + if (DBG) log("CMD_SET_DEPENDENCY_MET met=" + met); + Bundle bundle = msg.getData(); + if (bundle != null) { + String apnType = (String)bundle.get(DctConstants.APN_TYPE_KEY); + if (apnType != null) { + onSetDependencyMet(apnType, met); + } + } + break; + } + case DctConstants.CMD_SET_POLICY_DATA_ENABLE: { + final boolean enabled = (msg.arg1 == DctConstants.ENABLED) ? true : false; + onSetPolicyDataEnabled(enabled); + break; + } + default: + Log.e("DATA", "Unidentified event msg=" + msg); + break; + } + } + + /** + * Report on whether data connectivity is enabled + * + * @return {@code false} if data connectivity has been explicitly disabled, + * {@code true} otherwise. + */ + public boolean getAnyDataEnabled() { + final boolean result; + synchronized (mDataEnabledLock) { + result = (mInternalDataEnabled && mUserDataEnabled && sPolicyDataEnabled + && (enabledCount != 0)); + } + if (!result && DBG) log("getAnyDataEnabled " + result); + return result; + } + + protected boolean isEmergency() { + final boolean result; + synchronized (mDataEnabledLock) { + result = mPhone.isInEcm() || mPhone.isInEmergencyCall(); + } + log("isEmergency: result=" + result); + return result; + } + + protected int apnTypeToId(String type) { + if (TextUtils.equals(type, PhoneConstants.APN_TYPE_DEFAULT)) { + return DctConstants.APN_DEFAULT_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_MMS)) { + return DctConstants.APN_MMS_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_SUPL)) { + return DctConstants.APN_SUPL_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_DUN)) { + return DctConstants.APN_DUN_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_HIPRI)) { + return DctConstants.APN_HIPRI_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_IMS)) { + return DctConstants.APN_IMS_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_FOTA)) { + return DctConstants.APN_FOTA_ID; + } else if (TextUtils.equals(type, PhoneConstants.APN_TYPE_CBS)) { + return DctConstants.APN_CBS_ID; + } else { + return DctConstants.APN_INVALID_ID; + } + } + + protected String apnIdToType(int id) { + switch (id) { + case DctConstants.APN_DEFAULT_ID: + return PhoneConstants.APN_TYPE_DEFAULT; + case DctConstants.APN_MMS_ID: + return PhoneConstants.APN_TYPE_MMS; + case DctConstants.APN_SUPL_ID: + return PhoneConstants.APN_TYPE_SUPL; + case DctConstants.APN_DUN_ID: + return PhoneConstants.APN_TYPE_DUN; + case DctConstants.APN_HIPRI_ID: + return PhoneConstants.APN_TYPE_HIPRI; + case DctConstants.APN_IMS_ID: + return PhoneConstants.APN_TYPE_IMS; + case DctConstants.APN_FOTA_ID: + return PhoneConstants.APN_TYPE_FOTA; + case DctConstants.APN_CBS_ID: + return PhoneConstants.APN_TYPE_CBS; + default: + log("Unknown id (" + id + ") in apnIdToType"); + return PhoneConstants.APN_TYPE_DEFAULT; + } + } + + protected LinkProperties getLinkProperties(String apnType) { + int id = apnTypeToId(apnType); + + if (isApnIdEnabled(id)) { + // TODO - remove this cdma-only hack and support multiple DCs. + DataConnectionAc dcac = mDataConnectionAsyncChannels.get(0); + return dcac.getLinkPropertiesSync(); + } else { + return new LinkProperties(); + } + } + + protected LinkCapabilities getLinkCapabilities(String apnType) { + int id = apnTypeToId(apnType); + if (isApnIdEnabled(id)) { + // TODO - remove this cdma-only hack and support multiple DCs. + DataConnectionAc dcac = mDataConnectionAsyncChannels.get(0); + return dcac.getLinkCapabilitiesSync(); + } else { + return new LinkCapabilities(); + } + } + + // tell all active apns of the current condition + protected void notifyDataConnection(String reason) { + for (int id = 0; id < DctConstants.APN_NUM_TYPES; id++) { + if (dataEnabled[id]) { + mPhone.notifyDataConnection(reason, apnIdToType(id)); + } + } + notifyOffApnsOfAvailability(reason); + } + + // a new APN has gone active and needs to send events to catch up with the + // current condition + private void notifyApnIdUpToCurrent(String reason, int apnId) { + switch (mState) { + case IDLE: + case INITING: + break; + case CONNECTING: + case SCANNING: + mPhone.notifyDataConnection(reason, apnIdToType(apnId), + PhoneConstants.DataState.CONNECTING); + break; + case CONNECTED: + case DISCONNECTING: + mPhone.notifyDataConnection(reason, apnIdToType(apnId), + PhoneConstants.DataState.CONNECTING); + mPhone.notifyDataConnection(reason, apnIdToType(apnId), + PhoneConstants.DataState.CONNECTED); + break; + } + } + + // since we normally don't send info to a disconnected APN, we need to do this specially + private void notifyApnIdDisconnected(String reason, int apnId) { + mPhone.notifyDataConnection(reason, apnIdToType(apnId), + PhoneConstants.DataState.DISCONNECTED); + } + + // disabled apn's still need avail/unavail notificiations - send them out + protected void notifyOffApnsOfAvailability(String reason) { + if (DBG) log("notifyOffApnsOfAvailability - reason= " + reason); + for (int id = 0; id < DctConstants.APN_NUM_TYPES; id++) { + if (!isApnIdEnabled(id)) { + notifyApnIdDisconnected(reason, id); + } + } + } + + public boolean isApnTypeEnabled(String apnType) { + if (apnType == null) { + return false; + } else { + return isApnIdEnabled(apnTypeToId(apnType)); + } + } + + protected synchronized boolean isApnIdEnabled(int id) { + if (id != DctConstants.APN_INVALID_ID) { + return dataEnabled[id]; + } + return false; + } + + /** + * Ensure that we are connected to an APN of the specified type. + * + * @param type the APN type (currently the only valid values are + * {@link Phone#APN_TYPE_MMS} and {@link Phone#APN_TYPE_SUPL}) + * @return Success is indicated by {@code Phone.APN_ALREADY_ACTIVE} or + * {@code Phone.APN_REQUEST_STARTED}. In the latter case, a + * broadcast will be sent by the ConnectivityManager when a + * connection to the APN has been established. + */ + public synchronized int enableApnType(String type) { + int id = apnTypeToId(type); + if (id == DctConstants.APN_INVALID_ID) { + return PhoneConstants.APN_REQUEST_FAILED; + } + + if (DBG) { + log("enableApnType(" + type + "), isApnTypeActive = " + isApnTypeActive(type) + + ", isApnIdEnabled =" + isApnIdEnabled(id) + " and state = " + mState); + } + + if (!isApnTypeAvailable(type)) { + if (DBG) log("type not available"); + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } + + if (isApnIdEnabled(id)) { + return PhoneConstants.APN_ALREADY_ACTIVE; + } else { + setEnabled(id, true); + } + return PhoneConstants.APN_REQUEST_STARTED; + } + + /** + * The APN of the specified type is no longer needed. Ensure that if use of + * the default APN has not been explicitly disabled, we are connected to the + * default APN. + * + * @param type the APN type. The only valid values are currently + * {@link Phone#APN_TYPE_MMS} and {@link Phone#APN_TYPE_SUPL}. + * @return Success is indicated by {@code PhoneConstants.APN_ALREADY_ACTIVE} or + * {@code PhoneConstants.APN_REQUEST_STARTED}. In the latter case, a + * broadcast will be sent by the ConnectivityManager when a + * connection to the APN has been disconnected. A {@code + * PhoneConstants.APN_REQUEST_FAILED} is returned if the type parameter is + * invalid or if the apn wasn't enabled. + */ + public synchronized int disableApnType(String type) { + if (DBG) log("disableApnType(" + type + ")"); + int id = apnTypeToId(type); + if (id == DctConstants.APN_INVALID_ID) { + return PhoneConstants.APN_REQUEST_FAILED; + } + if (isApnIdEnabled(id)) { + setEnabled(id, false); + if (isApnTypeActive(PhoneConstants.APN_TYPE_DEFAULT)) { + if (dataEnabled[DctConstants.APN_DEFAULT_ID]) { + return PhoneConstants.APN_ALREADY_ACTIVE; + } else { + return PhoneConstants.APN_REQUEST_STARTED; + } + } else { + return PhoneConstants.APN_REQUEST_STARTED; + } + } else { + return PhoneConstants.APN_REQUEST_FAILED; + } + } + + protected void setEnabled(int id, boolean enable) { + if (DBG) { + log("setEnabled(" + id + ", " + enable + ") with old state = " + dataEnabled[id] + + " and enabledCount = " + enabledCount); + } + Message msg = obtainMessage(DctConstants.EVENT_ENABLE_NEW_APN); + msg.arg1 = id; + msg.arg2 = (enable ? DctConstants.ENABLED : DctConstants.DISABLED); + sendMessage(msg); + } + + protected void onEnableApn(int apnId, int enabled) { + if (DBG) { + log("EVENT_APN_ENABLE_REQUEST apnId=" + apnId + ", apnType=" + apnIdToType(apnId) + + ", enabled=" + enabled + ", dataEnabled = " + dataEnabled[apnId] + + ", enabledCount = " + enabledCount + ", isApnTypeActive = " + + isApnTypeActive(apnIdToType(apnId))); + } + if (enabled == DctConstants.ENABLED) { + synchronized (this) { + if (!dataEnabled[apnId]) { + dataEnabled[apnId] = true; + enabledCount++; + } + } + String type = apnIdToType(apnId); + if (!isApnTypeActive(type)) { + mRequestedApnType = type; + onEnableNewApn(); + } else { + notifyApnIdUpToCurrent(Phone.REASON_APN_SWITCHED, apnId); + } + } else { + // disable + boolean didDisable = false; + synchronized (this) { + if (dataEnabled[apnId]) { + dataEnabled[apnId] = false; + enabledCount--; + didDisable = true; + } + } + if (didDisable) { + if ((enabledCount == 0) || (apnId == DctConstants.APN_DUN_ID)) { + mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT; + onCleanUpConnection(true, apnId, Phone.REASON_DATA_DISABLED); + } + + // send the disconnect msg manually, since the normal route wont send + // it (it's not enabled) + notifyApnIdDisconnected(Phone.REASON_DATA_DISABLED, apnId); + if (dataEnabled[DctConstants.APN_DEFAULT_ID] == true + && !isApnTypeActive(PhoneConstants.APN_TYPE_DEFAULT)) { + // TODO - this is an ugly way to restore the default conn - should be done + // by a real contention manager and policy that disconnects the lower pri + // stuff as enable requests come in and pops them back on as we disable back + // down to the lower pri stuff + mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT; + onEnableNewApn(); + } + } + } + } + + /** + * Called when we switch APNs. + * + * mRequestedApnType is set prior to call + * To be overridden. + */ + protected void onEnableNewApn() { + } + + /** + * Called when EVENT_RESET_DONE is received so goto + * IDLE state and send notifications to those interested. + * + * TODO - currently unused. Needs to be hooked into DataConnection cleanup + * TODO - needs to pass some notion of which connection is reset.. + */ + protected void onResetDone(AsyncResult ar) { + if (DBG) log("EVENT_RESET_DONE"); + String reason = null; + if (ar.userObj instanceof String) { + reason = (String) ar.userObj; + } + gotoIdleAndNotifyDataConnection(reason); + } + + /** + * Prevent mobile data connections from being established, or once again + * allow mobile data connections. If the state toggles, then either tear + * down or set up data, as appropriate to match the new state. + * + * @param enable indicates whether to enable ({@code true}) or disable ( + * {@code false}) data + * @return {@code true} if the operation succeeded + */ + public boolean setInternalDataEnabled(boolean enable) { + if (DBG) + log("setInternalDataEnabled(" + enable + ")"); + + Message msg = obtainMessage(DctConstants.EVENT_SET_INTERNAL_DATA_ENABLE); + msg.arg1 = (enable ? DctConstants.ENABLED : DctConstants.DISABLED); + sendMessage(msg); + return true; + } + + protected void onSetInternalDataEnabled(boolean enabled) { + synchronized (mDataEnabledLock) { + mInternalDataEnabled = enabled; + if (enabled) { + log("onSetInternalDataEnabled: changed to enabled, try to setup data call"); + resetAllRetryCounts(); + onTrySetupData(Phone.REASON_DATA_ENABLED); + } else { + log("onSetInternalDataEnabled: changed to disabled, cleanUpAllConnections"); + cleanUpAllConnections(null); + } + } + } + + public void cleanUpAllConnections(String cause) { + Message msg = obtainMessage(DctConstants.EVENT_CLEAN_UP_ALL_CONNECTIONS); + msg.obj = cause; + sendMessage(msg); + } + + public abstract boolean isDisconnected(); + + protected void onSetUserDataEnabled(boolean enabled) { + synchronized (mDataEnabledLock) { + final boolean prevEnabled = getAnyDataEnabled(); + if (mUserDataEnabled != enabled) { + mUserDataEnabled = enabled; + Settings.Secure.putInt(mPhone.getContext().getContentResolver(), + Settings.Secure.MOBILE_DATA, enabled ? 1 : 0); + if (getDataOnRoamingEnabled() == false && + mPhone.getServiceState().getRoaming() == true) { + if (enabled) { + notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON); + } else { + notifyOffApnsOfAvailability(Phone.REASON_DATA_DISABLED); + } + } + if (prevEnabled != getAnyDataEnabled()) { + if (!prevEnabled) { + resetAllRetryCounts(); + onTrySetupData(Phone.REASON_DATA_ENABLED); + } else { + onCleanUpAllConnections(Phone.REASON_DATA_DISABLED); + } + } + } + } + } + + protected void onSetDependencyMet(String apnType, boolean met) { + } + + protected void onSetPolicyDataEnabled(boolean enabled) { + synchronized (mDataEnabledLock) { + final boolean prevEnabled = getAnyDataEnabled(); + if (sPolicyDataEnabled != enabled) { + sPolicyDataEnabled = enabled; + if (prevEnabled != getAnyDataEnabled()) { + if (!prevEnabled) { + resetAllRetryCounts(); + onTrySetupData(Phone.REASON_DATA_ENABLED); + } else { + onCleanUpAllConnections(Phone.REASON_DATA_DISABLED); + } + } + } + } + } + + protected String getReryConfig(boolean forDefault) { + int nt = mPhone.getServiceState().getNetworkType(); + + if ((nt == TelephonyManager.NETWORK_TYPE_CDMA) || + (nt == TelephonyManager.NETWORK_TYPE_1xRTT) || + (nt == TelephonyManager.NETWORK_TYPE_EVDO_0) || + (nt == TelephonyManager.NETWORK_TYPE_EVDO_A) || + (nt == TelephonyManager.NETWORK_TYPE_EVDO_B) || + (nt == TelephonyManager.NETWORK_TYPE_EHRPD)) { + // CDMA variant + return SystemProperties.get("ro.cdma.data_retry_config"); + } else { + // Use GSM varient for all others. + if (forDefault) { + return SystemProperties.get("ro.gsm.data_retry_config"); + } else { + return SystemProperties.get("ro.gsm.2nd_data_retry_config"); + } + } + } + + protected void resetAllRetryCounts() { + for (ApnContext ac : mApnContexts.values()) { + ac.setRetryCount(0); + } + for (DataConnection dc : mDataConnections.values()) { + dc.resetRetryCount(); + } + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("DataConnectionTracker:"); + pw.println(" mInternalDataEnabled=" + mInternalDataEnabled); + pw.println(" mUserDataEnabled=" + mUserDataEnabled); + pw.println(" sPolicyDataEnabed=" + sPolicyDataEnabled); + pw.println(" dataEnabled:"); + for(int i=0; i < dataEnabled.length; i++) { + pw.printf(" dataEnabled[%d]=%b\n", i, dataEnabled[i]); + } + pw.flush(); + pw.println(" enabledCount=" + enabledCount); + pw.println(" mRequestedApnType=" + mRequestedApnType); + pw.println(" mPhone=" + mPhone.getPhoneName()); + pw.println(" mActivity=" + mActivity); + pw.println(" mState=" + mState); + pw.println(" mTxPkts=" + mTxPkts); + pw.println(" mRxPkts=" + mRxPkts); + pw.println(" mNetStatPollPeriod=" + mNetStatPollPeriod); + pw.println(" mNetStatPollEnabled=" + mNetStatPollEnabled); + pw.println(" mDataStallTxRxSum=" + mDataStallTxRxSum); + pw.println(" mDataStallAlarmTag=" + mDataStallAlarmTag); + pw.println(" mSentSinceLastRecv=" + mSentSinceLastRecv); + pw.println(" mNoRecvPollCount=" + mNoRecvPollCount); + pw.println(" mIsWifiConnected=" + mIsWifiConnected); + pw.println(" mReconnectIntent=" + mReconnectIntent); + pw.println(" mCidActive=" + mCidActive); + pw.println(" mAutoAttachOnCreation=" + mAutoAttachOnCreation); + pw.println(" mIsScreenOn=" + mIsScreenOn); + pw.println(" mUniqueIdGenerator=" + mUniqueIdGenerator); + pw.flush(); + pw.println(" ***************************************"); + Set<Entry<Integer, DataConnection> > mDcSet = mDataConnections.entrySet(); + pw.println(" mDataConnections: count=" + mDcSet.size()); + for (Entry<Integer, DataConnection> entry : mDcSet) { + pw.printf(" *** mDataConnection[%d] \n", entry.getKey()); + entry.getValue().dump(fd, pw, args); + } + pw.println(" ***************************************"); + pw.flush(); + Set<Entry<String, Integer>> mApnToDcIdSet = mApnToDataConnectionId.entrySet(); + pw.println(" mApnToDataConnectonId size=" + mApnToDcIdSet.size()); + for (Entry<String, Integer> entry : mApnToDcIdSet) { + pw.printf(" mApnToDataConnectonId[%s]=%d\n", entry.getKey(), entry.getValue()); + } + pw.println(" ***************************************"); + pw.flush(); + if (mApnContexts != null) { + Set<Entry<String, ApnContext>> mApnContextsSet = mApnContexts.entrySet(); + pw.println(" mApnContexts size=" + mApnContextsSet.size()); + for (Entry<String, ApnContext> entry : mApnContextsSet) { + entry.getValue().dump(fd, pw, args); + } + pw.println(" ***************************************"); + } else { + pw.println(" mApnContexts=null"); + } + pw.flush(); + pw.println(" mActiveApn=" + mActiveApn); + if (mAllApns != null) { + pw.println(" mAllApns size=" + mAllApns.size()); + for (int i=0; i < mAllApns.size(); i++) { + pw.printf(" mAllApns[%d]: %s\n", i, mAllApns.get(i)); + } + pw.flush(); + } else { + pw.println(" mAllApns=null"); + } + pw.println(" mPreferredApn=" + mPreferredApn); + pw.println(" mIsPsRestricted=" + mIsPsRestricted); + pw.println(" mIsDisposed=" + mIsDisposed); + pw.println(" mIntentReceiver=" + mIntentReceiver); + pw.println(" mDataRoamingSettingObserver=" + mDataRoamingSettingObserver); + pw.flush(); + } +} diff --git a/src/java/com/android/internal/telephony/DebugService.java b/src/java/com/android/internal/telephony/DebugService.java new file mode 100644 index 0000000..29fea6e --- /dev/null +++ b/src/java/com/android/internal/telephony/DebugService.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2012 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.internal.telephony; + +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * A debug service that will dump telephony's state + * + * Currently this "Service" has a proxy in the phone app + * com.android.phone.TelephonyDebugService which actually + * invokes the dump method. + */ +public class DebugService { + private static String TAG = "DebugService"; + + /** Constructor */ + public DebugService() { + log("DebugService:"); + } + + /** + * Dump the state of various objects, add calls to other objects as desired. + */ + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + log("dump: +"); + PhoneProxy phoneProxy = null; + PhoneBase phoneBase = null; + + try { + phoneProxy = (PhoneProxy) PhoneFactory.getDefaultPhone(); + } catch (Exception e) { + pw.println("Telephony DebugService: Could not getDefaultPhone e=" + e); + return; + } + try { + phoneBase = (PhoneBase)phoneProxy.getActivePhone(); + } catch (Exception e) { + pw.println("Telephony DebugService: Could not PhoneBase e=" + e); + return; + } + + /** + * Surround each of the sub dump's with try/catch so even + * if one fails we'll be able to dump the next ones. + */ + pw.println(); + pw.println("++++++++++++++++++++++++++++++++"); + pw.flush(); + try { + phoneBase.dump(fd, pw, args); + } catch (Exception e) { + e.printStackTrace(); + } + pw.flush(); + pw.println("++++++++++++++++++++++++++++++++"); + try { + phoneBase.mDataConnectionTracker.dump(fd, pw, args); + } catch (Exception e) { + e.printStackTrace(); + } + pw.flush(); + pw.println("++++++++++++++++++++++++++++++++"); + try { + phoneBase.getServiceStateTracker().dump(fd, pw, args); + } catch (Exception e) { + e.printStackTrace(); + } + pw.flush(); + pw.println("++++++++++++++++++++++++++++++++"); + try { + phoneBase.getCallTracker().dump(fd, pw, args); + } catch (Exception e) { + e.printStackTrace(); + } + pw.flush(); + pw.println("++++++++++++++++++++++++++++++++"); + try { + ((RIL)phoneBase.mCM).dump(fd, pw, args); + } catch (Exception e) { + e.printStackTrace(); + } + pw.flush(); + pw.println("++++++++++++++++++++++++++++++++"); + log("dump: -"); + } + + private static void log(String s) { + Log.d(TAG, "DebugService " + s); + } +} diff --git a/src/java/com/android/internal/telephony/DefaultPhoneNotifier.java b/src/java/com/android/internal/telephony/DefaultPhoneNotifier.java new file mode 100644 index 0000000..4d16443 --- /dev/null +++ b/src/java/com/android/internal/telephony/DefaultPhoneNotifier.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.telephony.CellInfo; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.util.Log; + +import com.android.internal.telephony.ITelephonyRegistry; + +/** + * broadcast intents + */ +public class DefaultPhoneNotifier implements PhoneNotifier { + + static final String LOG_TAG = "GSM"; + private static final boolean DBG = true; + private ITelephonyRegistry mRegistry; + + /*package*/ + DefaultPhoneNotifier() { + mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService( + "telephony.registry")); + } + + public void notifyPhoneState(Phone sender) { + Call ringingCall = sender.getRingingCall(); + String incomingNumber = ""; + if (ringingCall != null && ringingCall.getEarliestConnection() != null){ + incomingNumber = ringingCall.getEarliestConnection().getAddress(); + } + try { + mRegistry.notifyCallState(convertCallState(sender.getState()), incomingNumber); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyServiceState(Phone sender) { + ServiceState ss = sender.getServiceState(); + if (ss == null) { + ss = new ServiceState(); + ss.setStateOutOfService(); + } + try { + mRegistry.notifyServiceState(ss); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifySignalStrength(Phone sender) { + try { + mRegistry.notifySignalStrength(sender.getSignalStrength()); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyMessageWaitingChanged(Phone sender) { + try { + mRegistry.notifyMessageWaitingChanged(sender.getMessageWaitingIndicator()); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyCallForwardingChanged(Phone sender) { + try { + mRegistry.notifyCallForwardingChanged(sender.getCallForwardingIndicator()); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyDataActivity(Phone sender) { + try { + mRegistry.notifyDataActivity(convertDataActivityState(sender.getDataActivityState())); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyDataConnection(Phone sender, String reason, String apnType, + PhoneConstants.DataState state) { + doNotifyDataConnection(sender, reason, apnType, state); + } + + private void doNotifyDataConnection(Phone sender, String reason, String apnType, + PhoneConstants.DataState state) { + // TODO + // use apnType as the key to which connection we're talking about. + // pass apnType back up to fetch particular for this one. + TelephonyManager telephony = TelephonyManager.getDefault(); + LinkProperties linkProperties = null; + LinkCapabilities linkCapabilities = null; + boolean roaming = false; + + if (state == PhoneConstants.DataState.CONNECTED) { + linkProperties = sender.getLinkProperties(apnType); + linkCapabilities = sender.getLinkCapabilities(apnType); + } + ServiceState ss = sender.getServiceState(); + if (ss != null) roaming = ss.getRoaming(); + + try { + mRegistry.notifyDataConnection( + convertDataState(state), + sender.isDataConnectivityPossible(apnType), reason, + sender.getActiveApnHost(apnType), + apnType, + linkProperties, + linkCapabilities, + ((telephony!=null) ? telephony.getNetworkType() : + TelephonyManager.NETWORK_TYPE_UNKNOWN), + roaming); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyDataConnectionFailed(Phone sender, String reason, String apnType) { + try { + mRegistry.notifyDataConnectionFailed(reason, apnType); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyCellLocation(Phone sender) { + Bundle data = new Bundle(); + sender.getCellLocation().fillInNotifierBundle(data); + try { + mRegistry.notifyCellLocation(data); + } catch (RemoteException ex) { + // system process is dead + } + } + + public void notifyCellInfo(Phone sender, CellInfo cellInfo) { + try { + mRegistry.notifyCellInfo(cellInfo); + } catch (RemoteException ex) { + + } + } + + public void notifyOtaspChanged(Phone sender, int otaspMode) { + try { + mRegistry.notifyOtaspChanged(otaspMode); + } catch (RemoteException ex) { + // system process is dead + } + } + + private void log(String s) { + Log.d(LOG_TAG, "[PhoneNotifier] " + s); + } + + /** + * Convert the {@link State} enum into the TelephonyManager.CALL_STATE_* constants + * for the public API. + */ + public static int convertCallState(PhoneConstants.State state) { + switch (state) { + case RINGING: + return TelephonyManager.CALL_STATE_RINGING; + case OFFHOOK: + return TelephonyManager.CALL_STATE_OFFHOOK; + default: + return TelephonyManager.CALL_STATE_IDLE; + } + } + + /** + * Convert the TelephonyManager.CALL_STATE_* constants into the {@link State} enum + * for the public API. + */ + public static PhoneConstants.State convertCallState(int state) { + switch (state) { + case TelephonyManager.CALL_STATE_RINGING: + return PhoneConstants.State.RINGING; + case TelephonyManager.CALL_STATE_OFFHOOK: + return PhoneConstants.State.OFFHOOK; + default: + return PhoneConstants.State.IDLE; + } + } + + /** + * Convert the {@link DataState} enum into the TelephonyManager.DATA_* constants + * for the public API. + */ + public static int convertDataState(PhoneConstants.DataState state) { + switch (state) { + case CONNECTING: + return TelephonyManager.DATA_CONNECTING; + case CONNECTED: + return TelephonyManager.DATA_CONNECTED; + case SUSPENDED: + return TelephonyManager.DATA_SUSPENDED; + default: + return TelephonyManager.DATA_DISCONNECTED; + } + } + + /** + * Convert the TelephonyManager.DATA_* constants into {@link DataState} enum + * for the public API. + */ + public static PhoneConstants.DataState convertDataState(int state) { + switch (state) { + case TelephonyManager.DATA_CONNECTING: + return PhoneConstants.DataState.CONNECTING; + case TelephonyManager.DATA_CONNECTED: + return PhoneConstants.DataState.CONNECTED; + case TelephonyManager.DATA_SUSPENDED: + return PhoneConstants.DataState.SUSPENDED; + default: + return PhoneConstants.DataState.DISCONNECTED; + } + } + + /** + * Convert the {@link DataState} enum into the TelephonyManager.DATA_* constants + * for the public API. + */ + public static int convertDataActivityState(Phone.DataActivityState state) { + switch (state) { + case DATAIN: + return TelephonyManager.DATA_ACTIVITY_IN; + case DATAOUT: + return TelephonyManager.DATA_ACTIVITY_OUT; + case DATAINANDOUT: + return TelephonyManager.DATA_ACTIVITY_INOUT; + case DORMANT: + return TelephonyManager.DATA_ACTIVITY_DORMANT; + default: + return TelephonyManager.DATA_ACTIVITY_NONE; + } + } + + /** + * Convert the TelephonyManager.DATA_* constants into the {@link DataState} enum + * for the public API. + */ + public static Phone.DataActivityState convertDataActivityState(int state) { + switch (state) { + case TelephonyManager.DATA_ACTIVITY_IN: + return Phone.DataActivityState.DATAIN; + case TelephonyManager.DATA_ACTIVITY_OUT: + return Phone.DataActivityState.DATAOUT; + case TelephonyManager.DATA_ACTIVITY_INOUT: + return Phone.DataActivityState.DATAINANDOUT; + case TelephonyManager.DATA_ACTIVITY_DORMANT: + return Phone.DataActivityState.DORMANT; + default: + return Phone.DataActivityState.NONE; + } + } +} diff --git a/src/java/com/android/internal/telephony/DriverCall.java b/src/java/com/android/internal/telephony/DriverCall.java new file mode 100644 index 0000000..b1e63ae --- /dev/null +++ b/src/java/com/android/internal/telephony/DriverCall.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2006 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.internal.telephony; +//import com.android.internal.telephony.*; +import android.util.Log; +import java.lang.Comparable; +import android.telephony.PhoneNumberUtils; + +/** + * {@hide} + */ +public class DriverCall implements Comparable { + static final String LOG_TAG = "RILB"; + + public enum State { + ACTIVE, + HOLDING, + DIALING, // MO call only + ALERTING, // MO call only + INCOMING, // MT call only + WAITING; // MT call only + // If you add a state, make sure to look for the switch() + // statements that use this enum + } + + public int index; + public boolean isMT; + public State state; // May be null if unavail + public boolean isMpty; + public String number; + public int TOA; + public boolean isVoice; + public boolean isVoicePrivacy; + public int als; + public int numberPresentation; + public String name; + public int namePresentation; + public UUSInfo uusInfo; + + /** returns null on error */ + static DriverCall + fromCLCCLine(String line) { + DriverCall ret = new DriverCall(); + + //+CLCC: 1,0,2,0,0,\"+18005551212\",145 + // index,isMT,state,mode,isMpty(,number,TOA)? + ATResponseParser p = new ATResponseParser(line); + + try { + ret.index = p.nextInt(); + ret.isMT = p.nextBoolean(); + ret.state = stateFromCLCC(p.nextInt()); + + ret.isVoice = (0 == p.nextInt()); + ret.isMpty = p.nextBoolean(); + + // use ALLOWED as default presentation while parsing CLCC + ret.numberPresentation = PhoneConstants.PRESENTATION_ALLOWED; + + if (p.hasMore()) { + // Some lame implementations return strings + // like "NOT AVAILABLE" in the CLCC line + ret.number = PhoneNumberUtils.extractNetworkPortionAlt(p.nextString()); + + if (ret.number.length() == 0) { + ret.number = null; + } + + ret.TOA = p.nextInt(); + + // Make sure there's a leading + on addresses with a TOA + // of 145 + + ret.number = PhoneNumberUtils.stringFromStringAndTOA( + ret.number, ret.TOA); + + } + } catch (ATParseEx ex) { + Log.e(LOG_TAG,"Invalid CLCC line: '" + line + "'"); + return null; + } + + return ret; + } + + public + DriverCall() { + } + + public String + toString() { + return "id=" + index + "," + + state + "," + + "toa=" + TOA + "," + + (isMpty ? "conf" : "norm") + "," + + (isMT ? "mt" : "mo") + "," + + als + "," + + (isVoice ? "voc" : "nonvoc") + "," + + (isVoicePrivacy ? "evp" : "noevp") + "," + /*+ "number=" + number */ + ",cli=" + numberPresentation + "," + /*+ "name="+ name */ + "," + namePresentation; + } + + public static State + stateFromCLCC(int state) throws ATParseEx { + switch(state) { + case 0: return State.ACTIVE; + case 1: return State.HOLDING; + case 2: return State.DIALING; + case 3: return State.ALERTING; + case 4: return State.INCOMING; + case 5: return State.WAITING; + default: + throw new ATParseEx("illegal call state " + state); + } + } + + public static int + presentationFromCLIP(int cli) throws ATParseEx + { + switch(cli) { + case 0: return PhoneConstants.PRESENTATION_ALLOWED; + case 1: return PhoneConstants.PRESENTATION_RESTRICTED; + case 2: return PhoneConstants.PRESENTATION_UNKNOWN; + case 3: return PhoneConstants.PRESENTATION_PAYPHONE; + default: + throw new ATParseEx("illegal presentation " + cli); + } + } + + //***** Comparable Implementation + + /** For sorting by index */ + public int + compareTo (Object o) { + DriverCall dc; + + dc = (DriverCall)o; + + if (index < dc.index) { + return -1; + } else if (index == dc.index) { + return 0; + } else { /*index > dc.index*/ + return 1; + } + } +} diff --git a/src/java/com/android/internal/telephony/EventLogTags.logtags b/src/java/com/android/internal/telephony/EventLogTags.logtags new file mode 100644 index 0000000..427e5da --- /dev/null +++ b/src/java/com/android/internal/telephony/EventLogTags.logtags @@ -0,0 +1,73 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package com.android.internal.telephony; + +# PDP Context has a bad DNS address +50100 pdp_bad_dns_address (dns_address|3) + +# For data connection on PDP context, reached the data-out-without-data-in +# packet count that triggers a countdown to radio restart +50101 pdp_radio_reset_countdown_triggered (out_packet_count|1|1) + +# Radio restart - timed out with no incoming packets. +50102 pdp_radio_reset (out_packet_count|1|1) + +# PDP context reset - timed out with no incoming packets. +50103 pdp_context_reset (out_packet_count|1|1) + +# Reregister to data network - timed out with no incoming packets. +50104 pdp_reregister_network (out_packet_count|1|1) + +# PDP Setup failures +50105 pdp_setup_fail (cause|1|5), (cid|1|5), (network_type|1|5) + +# Call drops +50106 call_drop (cause|1|5), (cid|1|5), (network_type|1|5) + +# Data network registration failed after successful voice registration +50107 data_network_registration_fail (op_numeric|1|5), (cid|1|5) + +# Suspicious status of data connection while radio poweroff +50108 data_network_status_on_radio_off (dc_state|3), (enable|1|5) + +# PDP drop caused by network +50109 pdp_network_drop (cid|1|5), (network_type|1|5) + +# CDMA data network setup failure +50110 cdma_data_setup_failed (cause|1|5), (cid|1|5), (network_type|1|5) + +# CDMA data network drop +50111 cdma_data_drop (cid|1|5), (network_type|1|5) + +# GSM radio access technology switched +50112 gsm_rat_switched (cid|1|5), (network_from|1|5), (network_to|1|5) + +# GSM data connection state transition +50113 gsm_data_state_change (oldState|3), (newState|3) + +# GSM service state transition +50114 gsm_service_state_change (oldState|1|5), (oldGprsState|1|5), (newState|1|5), (newGprsState|1|5) + +# CDMA data connection state transition +50115 cdma_data_state_change (oldState|3), (newState|3) + +# CDMA service state transition +50116 cdma_service_state_change (oldState|1|5), (oldDataState|1|5), (newState|1|5), (newDataState|1|5) + +# Bad IP address +50117 bad_ip_address (ip_address|3) + +# Data Stall Recovery mode DATA_STALL_RECOVERY_GET_DATA_CALL_LIST +50118 data_stall_recovery_get_data_call_list (out_packet_count|1|1) + +# Data Stall Recovery mode DATA_STALL_RECOVERY_CLEANUP +50119 data_stall_recovery_cleanup (out_packet_count|1|1) + +# Data Stall Recovery mode DATA_STALL_RECOVERY_REREGISTER +50120 data_stall_recovery_reregister (out_packet_count|1|1) + +# Data Stall Recovery mode DATA_STALL_RECOVERY_RADIO_RESTART +50121 data_stall_recovery_radio_restart (out_packet_count|1|1) + +# Data Stall Recovery mode DATA_STALL_RECOVERY_RADIO_RESTART_WITH_PROP +50122 data_stall_recovery_radio_restart_with_prop (out_packet_count|1|1) diff --git a/src/java/com/android/internal/telephony/IIccPhoneBook.aidl b/src/java/com/android/internal/telephony/IIccPhoneBook.aidl new file mode 100644 index 0000000..f700dfe --- /dev/null +++ b/src/java/com/android/internal/telephony/IIccPhoneBook.aidl @@ -0,0 +1,101 @@ +/* +** Copyright 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 com.android.internal.telephony; + +import com.android.internal.telephony.AdnRecord; + + + +/** Interface for applications to access the ICC phone book. + * + * <p>The following code snippet demonstrates a static method to + * retrieve the IIccPhoneBook interface from Android:</p> + * <pre>private static IIccPhoneBook getSimPhoneBookInterface() + throws DeadObjectException { + IServiceManager sm = ServiceManagerNative.getDefault(); + IIccPhoneBook spb; + spb = IIccPhoneBook.Stub.asInterface(sm.getService("iccphonebook")); + return spb; +} + * </pre> + */ + +interface IIccPhoneBook { + + /** + * Loads the AdnRecords in efid and returns them as a + * List of AdnRecords + * + * @param efid the EF id of a ADN-like SIM + * @return List of AdnRecord + */ + List<AdnRecord> getAdnRecordsInEf(int efid); + + /** + * Replace oldAdn with newAdn in ADN-like record in EF + * + * getAdnRecordsInEf must be called at least once before this function, + * otherwise an error will be returned + * + * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN + * @param oldTag adn tag to be replaced + * @param oldPhoneNumber adn number to be replaced + * Set both oldTag and oldPhoneNubmer to "" means to replace an + * empty record, aka, insert new record + * @param newTag adn tag to be stored + * @param newPhoneNumber adn number ot be stored + * Set both newTag and newPhoneNubmer to "" means to replace the old + * record with empty one, aka, delete old record + * @param pin2 required to update EF_FDN, otherwise must be null + * @return true for success + */ + boolean updateAdnRecordsInEfBySearch(int efid, + String oldTag, String oldPhoneNumber, + String newTag, String newPhoneNumber, + String pin2); + + /** + * Update an ADN-like EF record by record index + * + * This is useful for iteration the whole ADN file, such as write the whole + * phone book or erase/format the whole phonebook + * + * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN + * @param newTag adn tag to be stored + * @param newPhoneNumber adn number to be stored + * Set both newTag and newPhoneNubmer to "" means to replace the old + * record with empty one, aka, delete old record + * @param index is 1-based adn record index to be updated + * @param pin2 required to update EF_FDN, otherwise must be null + * @return true for success + */ + boolean updateAdnRecordsInEfByIndex(int efid, String newTag, + String newPhoneNumber, int index, + String pin2); + + /** + * Get the max munber of records in efid + * + * @param efid the EF id of a ADN-like SIM + * @return int[3] array + * recordSizes[0] is the single record length + * recordSizes[1] is the total length of the EF file + * recordSizes[2] is the number of records in the EF file + */ + int[] getAdnRecordsSize(int efid); + +} diff --git a/src/java/com/android/internal/telephony/ISms.aidl b/src/java/com/android/internal/telephony/ISms.aidl new file mode 100644 index 0000000..735f986 --- /dev/null +++ b/src/java/com/android/internal/telephony/ISms.aidl @@ -0,0 +1,201 @@ +/* +** Copyright 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 com.android.internal.telephony; + +import android.app.PendingIntent; +import com.android.internal.telephony.SmsRawData; + +/** Interface for applications to access the ICC phone book. + * + * <p>The following code snippet demonstrates a static method to + * retrieve the ISms interface from Android:</p> + * <pre>private static ISms getSmsInterface() + throws DeadObjectException { + IServiceManager sm = ServiceManagerNative.getDefault(); + ISms ss; + ss = ISms.Stub.asInterface(sm.getService("isms")); + return ss; +} + * </pre> + */ + +interface ISms { + /** + * Retrieves all messages currently stored on ICC. + * + * @return list of SmsRawData of all sms on ICC + */ + List<SmsRawData> getAllMessagesFromIccEf(); + + /** + * Update the specified message on the ICC. + * + * @param messageIndex record index of message to update + * @param newStatus new message status (STATUS_ON_ICC_READ, + * STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT, + * STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE) + * @param pdu the raw PDU to store + * @return success or not + * + */ + boolean updateMessageOnIccEf(int messageIndex, int newStatus, + in byte[] pdu); + + /** + * Copy a raw SMS PDU to the ICC. + * + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD, + * STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT) + * @return success or not + * + */ + boolean copyMessageToIccEf(int status, in byte[] pdu, in byte[] smsc); + + /** + * Send a data SMS. + * + * @param smsc the SMSC to send the message through, or NULL for the + * default SMSC + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is sucessfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applicaitons, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + void sendData(in String destAddr, in String scAddr, in int destPort, + in byte[] data, in PendingIntent sentIntent, in PendingIntent deliveryIntent); + + /** + * Send an SMS. + * + * @param smsc the SMSC to send the message through, or NULL for the + * default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is sucessfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + void sendText(in String destAddr, in String scAddr, in String text, + in PendingIntent sentIntent, in PendingIntent deliveryIntent); + + /** + * Send a multi-part text based SMS. + * + * @param destinationAddress the address to send the message to + * @param scAddress is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code>. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + */ + void sendMultipartText(in String destinationAddress, in String scAddress, + in List<String> parts, in List<PendingIntent> sentIntents, + in List<PendingIntent> deliveryIntents); + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #disableCellBroadcast(int) + */ + boolean enableCellBroadcast(int messageIdentifier); + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier. Note that if two different clients enable the same + * message identifier, they must both disable it for the device to stop + * receiving those messages. + * + * @param messageIdentifier Message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #enableCellBroadcast(int) + */ + boolean disableCellBroadcast(int messageIdentifier); + + /** + * Enable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range. Note that if two different clients enable + * a message identifier range, they must both disable it for the device + * to stop receiving those messages. + * + * @param startMessageId first message identifier as specified in TS 23.041 + * @param endMessageId last message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #disableCellBroadcastRange(int, int) + */ + boolean enableCellBroadcastRange(int startMessageId, int endMessageId); + + /** + * Disable reception of cell broadcast (SMS-CB) messages with the given + * message identifier range. Note that if two different clients enable + * a message identifier range, they must both disable it for the device + * to stop receiving those messages. + * + * @param startMessageId first message identifier as specified in TS 23.041 + * @param endMessageId last message identifier as specified in TS 23.041 + * @return true if successful, false otherwise + * + * @see #enableCellBroadcastRange(int, int) + */ + boolean disableCellBroadcastRange(int startMessageId, int endMessageId); + +} diff --git a/src/java/com/android/internal/telephony/IccCard.java b/src/java/com/android/internal/telephony/IccCard.java new file mode 100644 index 0000000..740292c --- /dev/null +++ b/src/java/com/android/internal/telephony/IccCard.java @@ -0,0 +1,958 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import static android.Manifest.permission.READ_PHONE_STATE; +import android.app.ActivityManagerNative; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.Registrant; +import android.os.RegistrantList; +import android.util.Log; +import android.view.WindowManager; + +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.CommandsInterface.RadioState; +import com.android.internal.telephony.gsm.SIMFileHandler; +import com.android.internal.telephony.gsm.SIMRecords; +import com.android.internal.telephony.cat.CatService; +import com.android.internal.telephony.cdma.CDMALTEPhone; +import com.android.internal.telephony.cdma.CdmaLteUiccFileHandler; +import com.android.internal.telephony.cdma.CdmaLteUiccRecords; +import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager; +import com.android.internal.telephony.cdma.RuimFileHandler; +import com.android.internal.telephony.cdma.RuimRecords; + +import com.android.internal.R; + +/** + * {@hide} + */ +public class IccCard { + protected String mLogTag; + protected boolean mDbg; + + protected IccCardStatus mIccCardStatus = null; + protected IccCardConstants.State mState = null; + private final Object mStateMonitor = new Object(); + + protected boolean is3gpp = true; + protected boolean isSubscriptionFromIccCard = true; + protected CdmaSubscriptionSourceManager mCdmaSSM = null; + protected PhoneBase mPhone; + private IccRecords mIccRecords; + private IccFileHandler mIccFileHandler; + private CatService mCatService; + + private RegistrantList mAbsentRegistrants = new RegistrantList(); + private RegistrantList mPinLockedRegistrants = new RegistrantList(); + private RegistrantList mNetworkLockedRegistrants = new RegistrantList(); + protected RegistrantList mReadyRegistrants = new RegistrantList(); + protected RegistrantList mRuimReadyRegistrants = new RegistrantList(); + + private boolean mDesiredPinLocked; + private boolean mDesiredFdnEnabled; + private boolean mIccPinLocked = true; // Default to locked + private boolean mIccFdnEnabled = false; // Default to disabled. + // Will be updated when SIM_READY. + + /* Parameter is3gpp's values to be passed to constructor */ + public final static boolean CARD_IS_3GPP = true; + public final static boolean CARD_IS_NOT_3GPP = false; + + protected static final int EVENT_ICC_LOCKED = 1; + private static final int EVENT_GET_ICC_STATUS_DONE = 2; + protected static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = 3; + private static final int EVENT_PINPUK_DONE = 4; + private static final int EVENT_REPOLL_STATUS_DONE = 5; + protected static final int EVENT_ICC_READY = 6; + private static final int EVENT_QUERY_FACILITY_LOCK_DONE = 7; + private static final int EVENT_CHANGE_FACILITY_LOCK_DONE = 8; + private static final int EVENT_CHANGE_ICC_PASSWORD_DONE = 9; + private static final int EVENT_QUERY_FACILITY_FDN_DONE = 10; + private static final int EVENT_CHANGE_FACILITY_FDN_DONE = 11; + private static final int EVENT_ICC_STATUS_CHANGED = 12; + private static final int EVENT_CARD_REMOVED = 13; + private static final int EVENT_CARD_ADDED = 14; + protected static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 15; + protected static final int EVENT_RADIO_ON = 16; + + public IccCardConstants.State getState() { + if (mState == null) { + switch(mPhone.mCM.getRadioState()) { + /* This switch block must not return anything in + * IccCardConstants.State.isLocked() or IccCardConstants.State.ABSENT. + * If it does, handleSimStatus() may break + */ + case RADIO_OFF: + case RADIO_UNAVAILABLE: + return IccCardConstants.State.UNKNOWN; + default: + if (!is3gpp && !isSubscriptionFromIccCard) { + // CDMA can get subscription from NV. In that case, + // subscription is ready as soon as Radio is ON. + return IccCardConstants.State.READY; + } + } + } else { + return mState; + } + + return IccCardConstants.State.UNKNOWN; + } + + public IccCard(PhoneBase phone, String logTag, Boolean is3gpp, Boolean dbg) { + mLogTag = logTag; + mDbg = dbg; + if (mDbg) log("[IccCard] Creating card type " + (is3gpp ? "3gpp" : "3gpp2")); + mPhone = phone; + this.is3gpp = is3gpp; + mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(mPhone.getContext(), + mPhone.mCM, mHandler, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null); + if (phone.mCM.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE + && phone instanceof CDMALTEPhone) { + mIccFileHandler = new CdmaLteUiccFileHandler(this, "", mPhone.mCM); + mIccRecords = new CdmaLteUiccRecords(this, mPhone.mContext, mPhone.mCM); + } else { + // Correct aid will be set later (when GET_SIM_STATUS returns) + mIccFileHandler = is3gpp ? new SIMFileHandler(this, "", mPhone.mCM) : + new RuimFileHandler(this, "", mPhone.mCM); + mIccRecords = is3gpp ? new SIMRecords(this, mPhone.mContext, mPhone.mCM) : + new RuimRecords(this, mPhone.mContext, mPhone.mCM); + } + mCatService = CatService.getInstance(mPhone.mCM, mIccRecords, + mPhone.mContext, mIccFileHandler, this); + mPhone.mCM.registerForOffOrNotAvailable(mHandler, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + mPhone.mCM.registerForOn(mHandler, EVENT_RADIO_ON, null); + mPhone.mCM.registerForIccStatusChanged(mHandler, EVENT_ICC_STATUS_CHANGED, null); + } + + public void dispose() { + if (mDbg) log("[IccCard] Disposing card type " + (is3gpp ? "3gpp" : "3gpp2")); + mPhone.mCM.unregisterForIccStatusChanged(mHandler); + mPhone.mCM.unregisterForOffOrNotAvailable(mHandler); + mPhone.mCM.unregisterForOn(mHandler); + mCatService.dispose(); + mCdmaSSM.dispose(mHandler); + mIccRecords.dispose(); + mIccFileHandler.dispose(); + } + + protected void finalize() { + if (mDbg) log("[IccCard] Finalized card type " + (is3gpp ? "3gpp" : "3gpp2")); + } + + public IccRecords getIccRecords() { + return mIccRecords; + } + + public IccFileHandler getIccFileHandler() { + return mIccFileHandler; + } + + /** + * Notifies handler of any transition into IccCardConstants.State.ABSENT + */ + public void registerForAbsent(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mAbsentRegistrants.add(r); + + if (getState() == IccCardConstants.State.ABSENT) { + r.notifyRegistrant(); + } + } + + public void unregisterForAbsent(Handler h) { + mAbsentRegistrants.remove(h); + } + + /** + * Notifies handler of any transition into IccCardConstants.State.NETWORK_LOCKED + */ + public void registerForNetworkLocked(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mNetworkLockedRegistrants.add(r); + + if (getState() == IccCardConstants.State.NETWORK_LOCKED) { + r.notifyRegistrant(); + } + } + + public void unregisterForNetworkLocked(Handler h) { + mNetworkLockedRegistrants.remove(h); + } + + /** + * Notifies handler of any transition into IccCardConstants.State.isPinLocked() + */ + public void registerForLocked(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + mPinLockedRegistrants.add(r); + + if (getState().isPinLocked()) { + r.notifyRegistrant(); + } + } + + public void unregisterForLocked(Handler h) { + mPinLockedRegistrants.remove(h); + } + + public void registerForReady(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mReadyRegistrants.add(r); + + if (getState() == IccCardConstants.State.READY) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + + public void unregisterForReady(Handler h) { + synchronized (mStateMonitor) { + mReadyRegistrants.remove(h); + } + } + + public IccCardConstants.State getRuimState() { + if(mIccCardStatus != null) { + return getAppState(mIccCardStatus.getCdmaSubscriptionAppIndex()); + } else { + return IccCardConstants.State.UNKNOWN; + } + } + + public void registerForRuimReady(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + + synchronized (mStateMonitor) { + mRuimReadyRegistrants.add(r); + + if (getState() == IccCardConstants.State.READY && + getRuimState() == IccCardConstants.State.READY ) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + } + + public void unregisterForRuimReady(Handler h) { + synchronized (mStateMonitor) { + mRuimReadyRegistrants.remove(h); + } + } + + /** + * Supply the ICC PIN to the ICC + * + * When the operation is complete, onComplete will be sent to its + * Handler. + * + * onComplete.obj will be an AsyncResult + * + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + * + * If the supplied PIN is incorrect: + * ((AsyncResult)onComplete.obj).exception != null + * && ((AsyncResult)onComplete.obj).exception + * instanceof com.android.internal.telephony.gsm.CommandException) + * && ((CommandException)(((AsyncResult)onComplete.obj).exception)) + * .getCommandError() == CommandException.Error.PASSWORD_INCORRECT + * + * + */ + + public void supplyPin (String pin, Message onComplete) { + mPhone.mCM.supplyIccPin(pin, mHandler.obtainMessage(EVENT_PINPUK_DONE, onComplete)); + } + + public void supplyPuk (String puk, String newPin, Message onComplete) { + mPhone.mCM.supplyIccPuk(puk, newPin, + mHandler.obtainMessage(EVENT_PINPUK_DONE, onComplete)); + } + + public void supplyPin2 (String pin2, Message onComplete) { + mPhone.mCM.supplyIccPin2(pin2, + mHandler.obtainMessage(EVENT_PINPUK_DONE, onComplete)); + } + + public void supplyPuk2 (String puk2, String newPin2, Message onComplete) { + mPhone.mCM.supplyIccPuk2(puk2, newPin2, + mHandler.obtainMessage(EVENT_PINPUK_DONE, onComplete)); + } + + public void supplyNetworkDepersonalization (String pin, Message onComplete) { + mPhone.mCM.supplyNetworkDepersonalization(pin, + mHandler.obtainMessage(EVENT_PINPUK_DONE, onComplete)); + } + + /** + * Check whether ICC pin lock is enabled + * This is a sync call which returns the cached pin enabled state + * + * @return true for ICC locked enabled + * false for ICC locked disabled + */ + public boolean getIccLockEnabled() { + return mIccPinLocked; + } + + /** + * Check whether ICC fdn (fixed dialing number) is enabled + * This is a sync call which returns the cached pin enabled state + * + * @return true for ICC fdn enabled + * false for ICC fdn disabled + */ + public boolean getIccFdnEnabled() { + return mIccFdnEnabled; + } + + /** + * Set the ICC pin lock enabled or disabled + * When the operation is complete, onComplete will be sent to its handler + * + * @param enabled "true" for locked "false" for unlocked. + * @param password needed to change the ICC pin state, aka. Pin1 + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void setIccLockEnabled (boolean enabled, + String password, Message onComplete) { + int serviceClassX; + serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE + + CommandsInterface.SERVICE_CLASS_DATA + + CommandsInterface.SERVICE_CLASS_FAX; + + mDesiredPinLocked = enabled; + + mPhone.mCM.setFacilityLock(CommandsInterface.CB_FACILITY_BA_SIM, + enabled, password, serviceClassX, + mHandler.obtainMessage(EVENT_CHANGE_FACILITY_LOCK_DONE, onComplete)); + } + + /** + * Set the ICC fdn enabled or disabled + * When the operation is complete, onComplete will be sent to its handler + * + * @param enabled "true" for locked "false" for unlocked. + * @param password needed to change the ICC fdn enable, aka Pin2 + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void setIccFdnEnabled (boolean enabled, + String password, Message onComplete) { + int serviceClassX; + serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE + + CommandsInterface.SERVICE_CLASS_DATA + + CommandsInterface.SERVICE_CLASS_FAX + + CommandsInterface.SERVICE_CLASS_SMS; + + mDesiredFdnEnabled = enabled; + + mPhone.mCM.setFacilityLock(CommandsInterface.CB_FACILITY_BA_FD, + enabled, password, serviceClassX, + mHandler.obtainMessage(EVENT_CHANGE_FACILITY_FDN_DONE, onComplete)); + } + + /** + * Change the ICC password used in ICC pin lock + * When the operation is complete, onComplete will be sent to its handler + * + * @param oldPassword is the old password + * @param newPassword is the new password + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void changeIccLockPassword(String oldPassword, String newPassword, + Message onComplete) { + mPhone.mCM.changeIccPin(oldPassword, newPassword, + mHandler.obtainMessage(EVENT_CHANGE_ICC_PASSWORD_DONE, onComplete)); + + } + + /** + * Change the ICC password used in ICC fdn enable + * When the operation is complete, onComplete will be sent to its handler + * + * @param oldPassword is the old password + * @param newPassword is the new password + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void changeIccFdnPassword(String oldPassword, String newPassword, + Message onComplete) { + mPhone.mCM.changeIccPin2(oldPassword, newPassword, + mHandler.obtainMessage(EVENT_CHANGE_ICC_PASSWORD_DONE, onComplete)); + + } + + + /** + * Returns service provider name stored in ICC card. + * If there is no service provider name associated or the record is not + * yet available, null will be returned <p> + * + * Please use this value when display Service Provider Name in idle mode <p> + * + * Usage of this provider name in the UI is a common carrier requirement. + * + * Also available via Android property "gsm.sim.operator.alpha" + * + * @return Service Provider Name stored in ICC card + * null if no service provider name associated or the record is not + * yet available + * + */ + public String getServiceProviderName () { + return mPhone.mIccRecords.getServiceProviderName(); + } + + protected void updateStateProperty() { + mPhone.setSystemProperty(TelephonyProperties.PROPERTY_SIM_STATE, getState().toString()); + } + + private void getIccCardStatusDone(AsyncResult ar) { + if (ar.exception != null) { + Log.e(mLogTag,"Error getting ICC status. " + + "RIL_REQUEST_GET_ICC_STATUS should " + + "never return an error", ar.exception); + return; + } + handleIccCardStatus((IccCardStatus) ar.result); + } + + private void handleIccCardStatus(IccCardStatus newCardStatus) { + boolean transitionedIntoPinLocked; + boolean transitionedIntoAbsent; + boolean transitionedIntoNetworkLocked; + boolean transitionedIntoPermBlocked; + boolean isIccCardRemoved; + boolean isIccCardAdded; + + IccCardConstants.State oldState, newState; + IccCardConstants.State oldRuimState = getRuimState(); + + oldState = mState; + mIccCardStatus = newCardStatus; + newState = getIccCardState(); + + synchronized (mStateMonitor) { + mState = newState; + updateStateProperty(); + if (oldState != IccCardConstants.State.READY && + newState == IccCardConstants.State.READY) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_ICC_READY)); + mReadyRegistrants.notifyRegistrants(); + } else if (newState.isPinLocked()) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_ICC_LOCKED)); + } + if (oldRuimState != IccCardConstants.State.READY && + getRuimState() == IccCardConstants.State.READY) { + mRuimReadyRegistrants.notifyRegistrants(); + } + } + + transitionedIntoPinLocked = ( + (oldState != IccCardConstants.State.PIN_REQUIRED && + newState == IccCardConstants.State.PIN_REQUIRED) + || (oldState != IccCardConstants.State.PUK_REQUIRED && + newState == IccCardConstants.State.PUK_REQUIRED)); + transitionedIntoAbsent = (oldState != IccCardConstants.State.ABSENT && + newState == IccCardConstants.State.ABSENT); + transitionedIntoNetworkLocked = (oldState != IccCardConstants.State.NETWORK_LOCKED + && newState == IccCardConstants.State.NETWORK_LOCKED); + transitionedIntoPermBlocked = (oldState != IccCardConstants.State.PERM_DISABLED + && newState == IccCardConstants.State.PERM_DISABLED); + isIccCardRemoved = (oldState != null && oldState.iccCardExist() && + newState == IccCardConstants.State.ABSENT); + isIccCardAdded = (oldState == IccCardConstants.State.ABSENT && + newState != null && newState.iccCardExist()); + + if (transitionedIntoPinLocked) { + if (mDbg) log("Notify SIM pin or puk locked."); + mPinLockedRegistrants.notifyRegistrants(); + broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_LOCKED, + (newState == IccCardConstants.State.PIN_REQUIRED) ? + IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN : + IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK); + } else if (transitionedIntoAbsent) { + if (mDbg) log("Notify SIM missing."); + mAbsentRegistrants.notifyRegistrants(); + broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_ABSENT, null); + } else if (transitionedIntoNetworkLocked) { + if (mDbg) log("Notify SIM network locked."); + mNetworkLockedRegistrants.notifyRegistrants(); + broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_LOCKED, + IccCardConstants.INTENT_VALUE_LOCKED_NETWORK); + } else if (transitionedIntoPermBlocked) { + if (mDbg) log("Notify SIM permanently disabled."); + broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_ABSENT, + IccCardConstants.INTENT_VALUE_ABSENT_ON_PERM_DISABLED); + } + + if (isIccCardRemoved) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_REMOVED, null)); + } else if (isIccCardAdded) { + mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARD_ADDED, null)); + } + + // Call onReady Record(s) on the IccCard becomes ready (not NV) + if (oldState != IccCardConstants.State.READY && newState == IccCardConstants.State.READY && + (is3gpp || isSubscriptionFromIccCard)) { + if (!(mIccFileHandler instanceof CdmaLteUiccFileHandler)) { + // CdmaLteUicc File Handler deals with both USIM and CSIM. + // Do not lock onto one AID for now. + mIccFileHandler.setAid(getAid()); + } + mIccRecords.onReady(); + } + } + + private void onIccSwap(boolean isAdded) { + // TODO: Here we assume the device can't handle SIM hot-swap + // and has to reboot. We may want to add a property, + // e.g. REBOOT_ON_SIM_SWAP, to indicate if modem support + // hot-swap. + DialogInterface.OnClickListener listener = null; + + + // TODO: SimRecords is not reset while SIM ABSENT (only reset while + // Radio_off_or_not_available). Have to reset in both both + // added or removed situation. + listener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + if (mDbg) log("Reboot due to SIM swap"); + PowerManager pm = (PowerManager) mPhone.getContext() + .getSystemService(Context.POWER_SERVICE); + pm.reboot("SIM is added."); + } + } + + }; + + Resources r = Resources.getSystem(); + + String title = (isAdded) ? r.getString(R.string.sim_added_title) : + r.getString(R.string.sim_removed_title); + String message = (isAdded) ? r.getString(R.string.sim_added_message) : + r.getString(R.string.sim_removed_message); + String buttonTxt = r.getString(R.string.sim_restart_button); + + AlertDialog dialog = new AlertDialog.Builder(mPhone.getContext()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(buttonTxt, listener) + .create(); + dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.show(); + } + + /** + * Interperate EVENT_QUERY_FACILITY_LOCK_DONE + * @param ar is asyncResult of Query_Facility_Locked + */ + private void onQueryFdnEnabled(AsyncResult ar) { + if(ar.exception != null) { + if(mDbg) log("Error in querying facility lock:" + ar.exception); + return; + } + + int[] ints = (int[])ar.result; + if(ints.length != 0) { + mIccFdnEnabled = (0!=ints[0]); + if(mDbg) log("Query facility lock : " + mIccFdnEnabled); + } else { + Log.e(mLogTag, "[IccCard] Bogus facility lock response"); + } + } + + /** + * Interperate EVENT_QUERY_FACILITY_LOCK_DONE + * @param ar is asyncResult of Query_Facility_Locked + */ + private void onQueryFacilityLock(AsyncResult ar) { + if(ar.exception != null) { + if (mDbg) log("Error in querying facility lock:" + ar.exception); + return; + } + + int[] ints = (int[])ar.result; + if(ints.length != 0) { + mIccPinLocked = (0!=ints[0]); + if(mDbg) log("Query facility lock : " + mIccPinLocked); + } else { + Log.e(mLogTag, "[IccCard] Bogus facility lock response"); + } + } + + public void broadcastIccStateChangedIntent(String value, String reason) { + Intent intent = new Intent(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra(PhoneConstants.PHONE_NAME_KEY, mPhone.getPhoneName()); + intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value); + intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason); + if(mDbg) log("Broadcasting intent ACTION_SIM_STATE_CHANGED " + value + + " reason " + reason); + ActivityManagerNative.broadcastStickyIntent(intent, READ_PHONE_STATE); + } + + protected Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg){ + AsyncResult ar; + int serviceClassX; + + serviceClassX = CommandsInterface.SERVICE_CLASS_VOICE + + CommandsInterface.SERVICE_CLASS_DATA + + CommandsInterface.SERVICE_CLASS_FAX; + + if (!mPhone.mIsTheCurrentActivePhone) { + Log.e(mLogTag, "Received message " + msg + "[" + msg.what + + "] while being destroyed. Ignoring."); + return; + } + + switch (msg.what) { + case EVENT_RADIO_OFF_OR_NOT_AVAILABLE: + mState = null; + updateStateProperty(); + broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_NOT_READY, + null); + break; + case EVENT_RADIO_ON: + if (!is3gpp) { + handleCdmaSubscriptionSource(); + } + mPhone.mCM.getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE)); + break; + case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED: + handleCdmaSubscriptionSource(); + break; + case EVENT_ICC_READY: + if(isSubscriptionFromIccCard) { + mPhone.mCM.queryFacilityLock ( + CommandsInterface.CB_FACILITY_BA_SIM, "", serviceClassX, + obtainMessage(EVENT_QUERY_FACILITY_LOCK_DONE)); + mPhone.mCM.queryFacilityLock ( + CommandsInterface.CB_FACILITY_BA_FD, "", serviceClassX, + obtainMessage(EVENT_QUERY_FACILITY_FDN_DONE)); + } + break; + case EVENT_ICC_LOCKED: + mPhone.mCM.queryFacilityLock ( + CommandsInterface.CB_FACILITY_BA_SIM, "", serviceClassX, + obtainMessage(EVENT_QUERY_FACILITY_LOCK_DONE)); + break; + case EVENT_GET_ICC_STATUS_DONE: + ar = (AsyncResult)msg.obj; + + getIccCardStatusDone(ar); + break; + case EVENT_PINPUK_DONE: + // a PIN/PUK/PIN2/PUK2/Network Personalization + // request has completed. ar.userObj is the response Message + // Repoll before returning + ar = (AsyncResult)msg.obj; + // TODO should abstract these exceptions + AsyncResult.forMessage(((Message)ar.userObj)).exception + = ar.exception; + mPhone.mCM.getIccCardStatus( + obtainMessage(EVENT_REPOLL_STATUS_DONE, ar.userObj)); + break; + case EVENT_REPOLL_STATUS_DONE: + // Finished repolling status after PIN operation + // ar.userObj is the response messaeg + // ar.userObj.obj is already an AsyncResult with an + // appropriate exception filled in if applicable + + ar = (AsyncResult)msg.obj; + getIccCardStatusDone(ar); + ((Message)ar.userObj).sendToTarget(); + break; + case EVENT_QUERY_FACILITY_LOCK_DONE: + ar = (AsyncResult)msg.obj; + onQueryFacilityLock(ar); + break; + case EVENT_QUERY_FACILITY_FDN_DONE: + ar = (AsyncResult)msg.obj; + onQueryFdnEnabled(ar); + break; + case EVENT_CHANGE_FACILITY_LOCK_DONE: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + mIccPinLocked = mDesiredPinLocked; + if (mDbg) log( "EVENT_CHANGE_FACILITY_LOCK_DONE: " + + "mIccPinLocked= " + mIccPinLocked); + } else { + Log.e(mLogTag, "Error change facility lock with exception " + + ar.exception); + } + AsyncResult.forMessage(((Message)ar.userObj)).exception + = ar.exception; + ((Message)ar.userObj).sendToTarget(); + break; + case EVENT_CHANGE_FACILITY_FDN_DONE: + ar = (AsyncResult)msg.obj; + + if (ar.exception == null) { + mIccFdnEnabled = mDesiredFdnEnabled; + if (mDbg) log("EVENT_CHANGE_FACILITY_FDN_DONE: " + + "mIccFdnEnabled=" + mIccFdnEnabled); + } else { + Log.e(mLogTag, "Error change facility fdn with exception " + + ar.exception); + } + AsyncResult.forMessage(((Message)ar.userObj)).exception + = ar.exception; + ((Message)ar.userObj).sendToTarget(); + break; + case EVENT_CHANGE_ICC_PASSWORD_DONE: + ar = (AsyncResult)msg.obj; + if(ar.exception != null) { + Log.e(mLogTag, "Error in change sim password with exception" + + ar.exception); + } + AsyncResult.forMessage(((Message)ar.userObj)).exception + = ar.exception; + ((Message)ar.userObj).sendToTarget(); + break; + case EVENT_ICC_STATUS_CHANGED: + Log.d(mLogTag, "Received Event EVENT_ICC_STATUS_CHANGED"); + mPhone.mCM.getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE)); + break; + case EVENT_CARD_REMOVED: + onIccSwap(false); + break; + case EVENT_CARD_ADDED: + onIccSwap(true); + break; + default: + Log.e(mLogTag, "[IccCard] Unknown Event " + msg.what); + } + } + }; + + private void handleCdmaSubscriptionSource() { + if(mCdmaSSM != null) { + int newSubscriptionSource = mCdmaSSM.getCdmaSubscriptionSource(); + + Log.d(mLogTag, "Received Cdma subscription source: " + newSubscriptionSource); + + boolean isNewSubFromRuim = + (newSubscriptionSource == CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM); + + if (isNewSubFromRuim != isSubscriptionFromIccCard) { + isSubscriptionFromIccCard = isNewSubFromRuim; + // Parse the Stored IccCardStatus Message to set mState correctly. + handleIccCardStatus(mIccCardStatus); + } + } + } + + public IccCardConstants.State getIccCardState() { + if(!is3gpp && !isSubscriptionFromIccCard) { + // CDMA can get subscription from NV. In that case, + // subscription is ready as soon as Radio is ON. + return IccCardConstants.State.READY; + } + + if (mIccCardStatus == null) { + Log.e(mLogTag, "[IccCard] IccCardStatus is null"); + return IccCardConstants.State.ABSENT; + } + + // this is common for all radio technologies + if (!mIccCardStatus.getCardState().isCardPresent()) { + return IccCardConstants.State.ABSENT; + } + + RadioState currentRadioState = mPhone.mCM.getRadioState(); + // check radio technology + if( currentRadioState == RadioState.RADIO_OFF || + currentRadioState == RadioState.RADIO_UNAVAILABLE) { + return IccCardConstants.State.NOT_READY; + } + + if( currentRadioState == RadioState.RADIO_ON ) { + IccCardConstants.State csimState = + getAppState(mIccCardStatus.getCdmaSubscriptionAppIndex()); + IccCardConstants.State usimState = + getAppState(mIccCardStatus.getGsmUmtsSubscriptionAppIndex()); + + if(mDbg) log("USIM=" + usimState + " CSIM=" + csimState); + + if (mPhone.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE) { + // UICC card contains both USIM and CSIM + // Return consolidated status + return getConsolidatedState(csimState, usimState, csimState); + } + + // check for CDMA radio technology + if (!is3gpp) { + return csimState; + } + return usimState; + } + + return IccCardConstants.State.ABSENT; + } + + private IccCardConstants.State getAppState(int appIndex) { + IccCardApplication app; + if (appIndex >= 0 && appIndex < IccCardStatus.CARD_MAX_APPS) { + app = mIccCardStatus.getApplication(appIndex); + } else { + Log.e(mLogTag, "[IccCard] Invalid Subscription Application index:" + appIndex); + return IccCardConstants.State.ABSENT; + } + + if (app == null) { + Log.e(mLogTag, "[IccCard] Subscription Application in not present"); + return IccCardConstants.State.ABSENT; + } + + // check if PIN required + if (app.pin1.isPermBlocked()) { + return IccCardConstants.State.PERM_DISABLED; + } + if (app.app_state.isPinRequired()) { + return IccCardConstants.State.PIN_REQUIRED; + } + if (app.app_state.isPukRequired()) { + return IccCardConstants.State.PUK_REQUIRED; + } + if (app.app_state.isSubscriptionPersoEnabled()) { + return IccCardConstants.State.NETWORK_LOCKED; + } + if (app.app_state.isAppReady()) { + return IccCardConstants.State.READY; + } + if (app.app_state.isAppNotReady()) { + return IccCardConstants.State.NOT_READY; + } + return IccCardConstants.State.NOT_READY; + } + + private IccCardConstants.State getConsolidatedState(IccCardConstants.State left, + IccCardConstants.State right, IccCardConstants.State preferredState) { + // Check if either is absent. + if (right == IccCardConstants.State.ABSENT) return left; + if (left == IccCardConstants.State.ABSENT) return right; + + // Only if both are ready, return ready + if ((left == IccCardConstants.State.READY) && (right == IccCardConstants.State.READY)) { + return IccCardConstants.State.READY; + } + + // Case one is ready, but the other is not. + if (((right == IccCardConstants.State.NOT_READY) && + (left == IccCardConstants.State.READY)) || + ((left == IccCardConstants.State.NOT_READY) && + (right == IccCardConstants.State.READY))) { + return IccCardConstants.State.NOT_READY; + } + + // At this point, the other state is assumed to be one of locked state + if (right == IccCardConstants.State.NOT_READY) return left; + if (left == IccCardConstants.State.NOT_READY) return right; + + // At this point, FW currently just assumes the status will be + // consistent across the applications... + return preferredState; + } + + public boolean isApplicationOnIcc(IccCardApplication.AppType type) { + if (mIccCardStatus == null) return false; + + for (int i = 0 ; i < mIccCardStatus.getNumApplications(); i++) { + IccCardApplication app = mIccCardStatus.getApplication(i); + if (app != null && app.app_type == type) { + return true; + } + } + return false; + } + + /** + * @return true if a ICC card is present + */ + public boolean hasIccCard() { + if (mIccCardStatus == null) { + return false; + } else { + // Returns ICC card status for both GSM and CDMA mode + return mIccCardStatus.getCardState().isCardPresent(); + } + } + + private void log(String msg) { + Log.d(mLogTag, "[IccCard] " + msg); + } + + protected int getCurrentApplicationIndex() { + if (is3gpp) { + return mIccCardStatus.getGsmUmtsSubscriptionAppIndex(); + } else { + return mIccCardStatus.getCdmaSubscriptionAppIndex(); + } + } + + public String getAid() { + String aid = ""; + if (mIccCardStatus == null) { + return aid; + } + + int appIndex = getCurrentApplicationIndex(); + + if (appIndex >= 0 && appIndex < IccCardStatus.CARD_MAX_APPS) { + IccCardApplication app = mIccCardStatus.getApplication(appIndex); + if (app != null) { + aid = app.aid; + } else { + Log.e(mLogTag, "[IccCard] getAid: no current application index=" + appIndex); + } + } else { + Log.e(mLogTag, "[IccCard] getAid: Invalid Subscription Application index=" + appIndex); + } + + return aid; + } +} diff --git a/src/java/com/android/internal/telephony/IccCardApplication.java b/src/java/com/android/internal/telephony/IccCardApplication.java new file mode 100644 index 0000000..abb740e --- /dev/null +++ b/src/java/com/android/internal/telephony/IccCardApplication.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.IccCardStatus.PinState; + + +/** + * See also RIL_AppStatus in include/telephony/ril.h + * + * {@hide} + */ +public class IccCardApplication { + public enum AppType{ + APPTYPE_UNKNOWN, + APPTYPE_SIM, + APPTYPE_USIM, + APPTYPE_RUIM, + APPTYPE_CSIM, + APPTYPE_ISIM + }; + + public enum AppState{ + APPSTATE_UNKNOWN, + APPSTATE_DETECTED, + APPSTATE_PIN, + APPSTATE_PUK, + APPSTATE_SUBSCRIPTION_PERSO, + APPSTATE_READY; + + boolean isPinRequired() { + return this == APPSTATE_PIN; + } + + boolean isPukRequired() { + return this == APPSTATE_PUK; + } + + boolean isSubscriptionPersoEnabled() { + return this == APPSTATE_SUBSCRIPTION_PERSO; + } + + boolean isAppReady() { + return this == APPSTATE_READY; + } + + boolean isAppNotReady() { + return this == APPSTATE_UNKNOWN || + this == APPSTATE_DETECTED; + } + }; + + public enum PersoSubState{ + PERSOSUBSTATE_UNKNOWN, + PERSOSUBSTATE_IN_PROGRESS, + PERSOSUBSTATE_READY, + PERSOSUBSTATE_SIM_NETWORK, + PERSOSUBSTATE_SIM_NETWORK_SUBSET, + PERSOSUBSTATE_SIM_CORPORATE, + PERSOSUBSTATE_SIM_SERVICE_PROVIDER, + PERSOSUBSTATE_SIM_SIM, + PERSOSUBSTATE_SIM_NETWORK_PUK, + PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK, + PERSOSUBSTATE_SIM_CORPORATE_PUK, + PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK, + PERSOSUBSTATE_SIM_SIM_PUK, + PERSOSUBSTATE_RUIM_NETWORK1, + PERSOSUBSTATE_RUIM_NETWORK2, + PERSOSUBSTATE_RUIM_HRPD, + PERSOSUBSTATE_RUIM_CORPORATE, + PERSOSUBSTATE_RUIM_SERVICE_PROVIDER, + PERSOSUBSTATE_RUIM_RUIM, + PERSOSUBSTATE_RUIM_NETWORK1_PUK, + PERSOSUBSTATE_RUIM_NETWORK2_PUK, + PERSOSUBSTATE_RUIM_HRPD_PUK, + PERSOSUBSTATE_RUIM_CORPORATE_PUK, + PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK, + PERSOSUBSTATE_RUIM_RUIM_PUK; + + boolean isPersoSubStateUnknown() { + return this == PERSOSUBSTATE_UNKNOWN; + } + }; + + public AppType app_type; + public AppState app_state; + // applicable only if app_state == RIL_APPSTATE_SUBSCRIPTION_PERSO + public PersoSubState perso_substate; + // null terminated string, e.g., from 0xA0, 0x00 -> 0x41, 0x30, 0x30, 0x30 */ + public String aid; + // null terminated string + public String app_label; + // applicable to USIM and CSIM + public int pin1_replaced; + public PinState pin1; + public PinState pin2; + + AppType AppTypeFromRILInt(int type) { + AppType newType; + /* RIL_AppType ril.h */ + switch(type) { + case 0: newType = AppType.APPTYPE_UNKNOWN; break; + case 1: newType = AppType.APPTYPE_SIM; break; + case 2: newType = AppType.APPTYPE_USIM; break; + case 3: newType = AppType.APPTYPE_RUIM; break; + case 4: newType = AppType.APPTYPE_CSIM; break; + case 5: newType = AppType.APPTYPE_ISIM; break; + default: + throw new RuntimeException( + "Unrecognized RIL_AppType: " +type); + } + return newType; + } + + AppState AppStateFromRILInt(int state) { + AppState newState; + /* RIL_AppState ril.h */ + switch(state) { + case 0: newState = AppState.APPSTATE_UNKNOWN; break; + case 1: newState = AppState.APPSTATE_DETECTED; break; + case 2: newState = AppState.APPSTATE_PIN; break; + case 3: newState = AppState.APPSTATE_PUK; break; + case 4: newState = AppState.APPSTATE_SUBSCRIPTION_PERSO; break; + case 5: newState = AppState.APPSTATE_READY; break; + default: + throw new RuntimeException( + "Unrecognized RIL_AppState: " +state); + } + return newState; + } + + PersoSubState PersoSubstateFromRILInt(int substate) { + PersoSubState newSubState; + /* RIL_PeroSubstate ril.h */ + switch(substate) { + case 0: newSubState = PersoSubState.PERSOSUBSTATE_UNKNOWN; break; + case 1: newSubState = PersoSubState.PERSOSUBSTATE_IN_PROGRESS; break; + case 2: newSubState = PersoSubState.PERSOSUBSTATE_READY; break; + case 3: newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK; break; + case 4: newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_SUBSET; break; + case 5: newSubState = PersoSubState.PERSOSUBSTATE_SIM_CORPORATE; break; + case 6: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SERVICE_PROVIDER; break; + case 7: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SIM; break; + case 8: newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_PUK; break; + case 9: newSubState = PersoSubState.PERSOSUBSTATE_SIM_NETWORK_SUBSET_PUK; break; + case 10: newSubState = PersoSubState.PERSOSUBSTATE_SIM_CORPORATE_PUK; break; + case 11: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SERVICE_PROVIDER_PUK; break; + case 12: newSubState = PersoSubState.PERSOSUBSTATE_SIM_SIM_PUK; break; + case 13: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK1; break; + case 14: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK2; break; + case 15: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_HRPD; break; + case 16: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_CORPORATE; break; + case 17: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_SERVICE_PROVIDER; break; + case 18: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_RUIM; break; + case 19: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK1_PUK; break; + case 20: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_NETWORK2_PUK; break; + case 21: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_HRPD_PUK ; break; + case 22: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_CORPORATE_PUK; break; + case 23: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_SERVICE_PROVIDER_PUK; break; + case 24: newSubState = PersoSubState.PERSOSUBSTATE_RUIM_RUIM_PUK; break; + default: + throw new RuntimeException( + "Unrecognized RIL_PersoSubstate: " +substate); + } + return newSubState; + } + + PinState PinStateFromRILInt(int state) { + PinState newPinState; + switch(state) { + case 0: + newPinState = PinState.PINSTATE_UNKNOWN; + break; + case 1: + newPinState = PinState.PINSTATE_ENABLED_NOT_VERIFIED; + break; + case 2: + newPinState = PinState.PINSTATE_ENABLED_VERIFIED; + break; + case 3: + newPinState = PinState.PINSTATE_DISABLED; + break; + case 4: + newPinState = PinState.PINSTATE_ENABLED_BLOCKED; + break; + case 5: + newPinState = PinState.PINSTATE_ENABLED_PERM_BLOCKED; + break; + default: + throw new RuntimeException("Unrecognized RIL_PinState: " + state); + } + return newPinState; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append("{").append(app_type).append(",").append(app_state); + if (app_state == AppState.APPSTATE_SUBSCRIPTION_PERSO) { + sb.append(",").append(perso_substate); + } + if (app_type == AppType.APPTYPE_CSIM || + app_type == AppType.APPTYPE_USIM || + app_type == AppType.APPTYPE_ISIM) { + sb.append(",pin1=").append(pin1); + sb.append(",pin2=").append(pin2); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/java/com/android/internal/telephony/IccCardStatus.java b/src/java/com/android/internal/telephony/IccCardStatus.java new file mode 100644 index 0000000..a3bdd76 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccCardStatus.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import java.util.ArrayList; + +/** + * See also RIL_CardStatus in include/telephony/ril.h + * + * {@hide} + */ +public class IccCardStatus { + public static final int CARD_MAX_APPS = 8; + + public enum CardState { + CARDSTATE_ABSENT, + CARDSTATE_PRESENT, + CARDSTATE_ERROR; + + boolean isCardPresent() { + return this == CARDSTATE_PRESENT; + } + } + + public enum PinState { + PINSTATE_UNKNOWN, + PINSTATE_ENABLED_NOT_VERIFIED, + PINSTATE_ENABLED_VERIFIED, + PINSTATE_DISABLED, + PINSTATE_ENABLED_BLOCKED, + PINSTATE_ENABLED_PERM_BLOCKED; + + boolean isPermBlocked() { + return this == PINSTATE_ENABLED_PERM_BLOCKED; + } + + boolean isPinRequired() { + return this == PINSTATE_ENABLED_NOT_VERIFIED; + } + + boolean isPukRequired() { + return this == PINSTATE_ENABLED_BLOCKED; + } + } + + private CardState mCardState; + private PinState mUniversalPinState; + private int mGsmUmtsSubscriptionAppIndex; + private int mCdmaSubscriptionAppIndex; + private int mImsSubscriptionAppIndex; + private int mNumApplications; + + private ArrayList<IccCardApplication> mApplications = + new ArrayList<IccCardApplication>(CARD_MAX_APPS); + + public CardState getCardState() { + return mCardState; + } + + public void setCardState(int state) { + switch(state) { + case 0: + mCardState = CardState.CARDSTATE_ABSENT; + break; + case 1: + mCardState = CardState.CARDSTATE_PRESENT; + break; + case 2: + mCardState = CardState.CARDSTATE_ERROR; + break; + default: + throw new RuntimeException("Unrecognized RIL_CardState: " + state); + } + } + + public PinState getUniversalPinState() { + return mUniversalPinState; + } + + public void setUniversalPinState(int state) { + switch(state) { + case 0: + mUniversalPinState = PinState.PINSTATE_UNKNOWN; + break; + case 1: + mUniversalPinState = PinState.PINSTATE_ENABLED_NOT_VERIFIED; + break; + case 2: + mUniversalPinState = PinState.PINSTATE_ENABLED_VERIFIED; + break; + case 3: + mUniversalPinState = PinState.PINSTATE_DISABLED; + break; + case 4: + mUniversalPinState = PinState.PINSTATE_ENABLED_BLOCKED; + break; + case 5: + mUniversalPinState = PinState.PINSTATE_ENABLED_PERM_BLOCKED; + break; + default: + throw new RuntimeException("Unrecognized RIL_PinState: " + state); + } + } + + public int getGsmUmtsSubscriptionAppIndex() { + return mGsmUmtsSubscriptionAppIndex; + } + + public void setGsmUmtsSubscriptionAppIndex(int gsmUmtsSubscriptionAppIndex) { + mGsmUmtsSubscriptionAppIndex = gsmUmtsSubscriptionAppIndex; + } + + public int getCdmaSubscriptionAppIndex() { + return mCdmaSubscriptionAppIndex; + } + + public void setCdmaSubscriptionAppIndex(int cdmaSubscriptionAppIndex) { + mCdmaSubscriptionAppIndex = cdmaSubscriptionAppIndex; + } + + public int getImsSubscriptionAppIndex() { + return mImsSubscriptionAppIndex; + } + + public void setImsSubscriptionAppIndex(int imsSubscriptionAppIndex) { + mImsSubscriptionAppIndex = imsSubscriptionAppIndex; + } + + public int getNumApplications() { + return mNumApplications; + } + + public void setNumApplications(int numApplications) { + mNumApplications = numApplications; + } + + public void addApplication(IccCardApplication application) { + mApplications.add(application); + } + + public IccCardApplication getApplication(int index) { + return mApplications.get(index); + } + + @Override + public String toString() { + IccCardApplication app; + + StringBuilder sb = new StringBuilder(); + sb.append("IccCardState {").append(mCardState).append(",") + .append(mUniversalPinState) + .append(",num_apps=").append(mNumApplications) + .append(",gsm_id=").append(mGsmUmtsSubscriptionAppIndex); + if (mGsmUmtsSubscriptionAppIndex >=0 + && mGsmUmtsSubscriptionAppIndex <CARD_MAX_APPS) { + app = getApplication(mGsmUmtsSubscriptionAppIndex); + sb.append(app == null ? "null" : app); + } + + sb.append(",cmda_id=").append(mCdmaSubscriptionAppIndex); + if (mCdmaSubscriptionAppIndex >=0 + && mCdmaSubscriptionAppIndex <CARD_MAX_APPS) { + app = getApplication(mCdmaSubscriptionAppIndex); + sb.append(app == null ? "null" : app); + } + + sb.append(",ism_id=").append(mImsSubscriptionAppIndex); + + sb.append("}"); + + return sb.toString(); + } + +} diff --git a/src/java/com/android/internal/telephony/IccConstants.java b/src/java/com/android/internal/telephony/IccConstants.java new file mode 100644 index 0000000..1ba6dfe --- /dev/null +++ b/src/java/com/android/internal/telephony/IccConstants.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public interface IccConstants { + // GSM SIM file ids from TS 51.011 + static final int EF_ADN = 0x6F3A; + static final int EF_FDN = 0x6F3B; + static final int EF_SDN = 0x6F49; + static final int EF_EXT1 = 0x6F4A; + static final int EF_EXT2 = 0x6F4B; + static final int EF_EXT3 = 0x6F4C; + static final int EF_EXT6 = 0x6fc8; // Ext record for EF[MBDN] + static final int EF_MWIS = 0x6FCA; + static final int EF_MBDN = 0x6fc7; + static final int EF_PNN = 0x6fc5; + static final int EF_SPN = 0x6F46; + static final int EF_SMS = 0x6F3C; + static final int EF_ICCID = 0x2fe2; + static final int EF_AD = 0x6FAD; + static final int EF_MBI = 0x6fc9; + static final int EF_MSISDN = 0x6f40; + static final int EF_SPDI = 0x6fcd; + static final int EF_SST = 0x6f38; + static final int EF_CFIS = 0x6FCB; + static final int EF_IMG = 0x4f20; + + // USIM SIM file ids from TS 31.102 + public static final int EF_PBR = 0x4F30; + + // GSM SIM file ids from CPHS (phase 2, version 4.2) CPHS4_2.WW6 + static final int EF_MAILBOX_CPHS = 0x6F17; + static final int EF_VOICE_MAIL_INDICATOR_CPHS = 0x6F11; + static final int EF_CFF_CPHS = 0x6F13; + static final int EF_SPN_CPHS = 0x6f14; + static final int EF_SPN_SHORT_CPHS = 0x6f18; + static final int EF_INFO_CPHS = 0x6f16; + static final int EF_CSP_CPHS = 0x6f15; + + // CDMA RUIM file ids from 3GPP2 C.S0023-0 + static final int EF_CST = 0x6f32; + static final int EF_RUIM_SPN =0x6F41; + + // ETSI TS.102.221 + static final int EF_PL = 0x2F05; + // 3GPP2 C.S0065 + static final int EF_CSIM_LI = 0x6F3A; + static final int EF_CSIM_SPN =0x6F41; + static final int EF_CSIM_MDN = 0x6F44; + static final int EF_CSIM_IMSIM = 0x6F22; + static final int EF_CSIM_CDMAHOME = 0x6F28; + static final int EF_CSIM_EPRL = 0x6F5A; + + //ISIM access + static final int EF_IMPU = 0x6f04; + static final int EF_IMPI = 0x6f02; + static final int EF_DOMAIN = 0x6f03; + static final int EF_IST = 0x6f07; + static final int EF_PCSCF = 0x6f09; + + // SMS record length from TS 51.011 10.5.3 + static public final int SMS_RECORD_LENGTH = 176; + + static final String MF_SIM = "3F00"; + static final String DF_TELECOM = "7F10"; + static final String DF_PHONEBOOK = "5F3A"; + static final String DF_GRAPHICS = "5F50"; + static final String DF_GSM = "7F20"; + static final String DF_CDMA = "7F25"; + + //ISIM access + static final String DF_ADFISIM = "7FFF"; +} diff --git a/src/java/com/android/internal/telephony/IccException.java b/src/java/com/android/internal/telephony/IccException.java new file mode 100644 index 0000000..1659a4e --- /dev/null +++ b/src/java/com/android/internal/telephony/IccException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class IccException extends Exception { + public IccException() { + + } + + public IccException(String s) { + super(s); + } +} diff --git a/src/java/com/android/internal/telephony/IccFileHandler.java b/src/java/com/android/internal/telephony/IccFileHandler.java new file mode 100644 index 0000000..70d8f7a --- /dev/null +++ b/src/java/com/android/internal/telephony/IccFileHandler.java @@ -0,0 +1,551 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.os.*; +import android.util.Log; +import java.util.ArrayList; + +/** + * {@hide} + */ +public abstract class IccFileHandler extends Handler implements IccConstants { + + //from TS 11.11 9.1 or elsewhere + static protected final int COMMAND_READ_BINARY = 0xb0; + static protected final int COMMAND_UPDATE_BINARY = 0xd6; + static protected final int COMMAND_READ_RECORD = 0xb2; + static protected final int COMMAND_UPDATE_RECORD = 0xdc; + static protected final int COMMAND_SEEK = 0xa2; + static protected final int COMMAND_GET_RESPONSE = 0xc0; + + // from TS 11.11 9.2.5 + static protected final int READ_RECORD_MODE_ABSOLUTE = 4; + + //***** types of files TS 11.11 9.3 + static protected final int EF_TYPE_TRANSPARENT = 0; + static protected final int EF_TYPE_LINEAR_FIXED = 1; + static protected final int EF_TYPE_CYCLIC = 3; + + //***** types of files TS 11.11 9.3 + static protected final int TYPE_RFU = 0; + static protected final int TYPE_MF = 1; + static protected final int TYPE_DF = 2; + static protected final int TYPE_EF = 4; + + // size of GET_RESPONSE for EF's + static protected final int GET_RESPONSE_EF_SIZE_BYTES = 15; + static protected final int GET_RESPONSE_EF_IMG_SIZE_BYTES = 10; + + // Byte order received in response to COMMAND_GET_RESPONSE + // Refer TS 51.011 Section 9.2.1 + static protected final int RESPONSE_DATA_RFU_1 = 0; + static protected final int RESPONSE_DATA_RFU_2 = 1; + + static protected final int RESPONSE_DATA_FILE_SIZE_1 = 2; + static protected final int RESPONSE_DATA_FILE_SIZE_2 = 3; + + static protected final int RESPONSE_DATA_FILE_ID_1 = 4; + static protected final int RESPONSE_DATA_FILE_ID_2 = 5; + static protected final int RESPONSE_DATA_FILE_TYPE = 6; + static protected final int RESPONSE_DATA_RFU_3 = 7; + static protected final int RESPONSE_DATA_ACCESS_CONDITION_1 = 8; + static protected final int RESPONSE_DATA_ACCESS_CONDITION_2 = 9; + static protected final int RESPONSE_DATA_ACCESS_CONDITION_3 = 10; + static protected final int RESPONSE_DATA_FILE_STATUS = 11; + static protected final int RESPONSE_DATA_LENGTH = 12; + static protected final int RESPONSE_DATA_STRUCTURE = 13; + static protected final int RESPONSE_DATA_RECORD_LENGTH = 14; + + + //***** Events + + /** Finished retrieving size of transparent EF; start loading. */ + static protected final int EVENT_GET_BINARY_SIZE_DONE = 4; + /** Finished loading contents of transparent EF; post result. */ + static protected final int EVENT_READ_BINARY_DONE = 5; + /** Finished retrieving size of records for linear-fixed EF; now load. */ + static protected final int EVENT_GET_RECORD_SIZE_DONE = 6; + /** Finished loading single record from a linear-fixed EF; post result. */ + static protected final int EVENT_READ_RECORD_DONE = 7; + /** Finished retrieving record size; post result. */ + static protected final int EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE = 8; + /** Finished retrieving image instance record; post result. */ + static protected final int EVENT_READ_IMG_DONE = 9; + /** Finished retrieving icon data; post result. */ + static protected final int EVENT_READ_ICON_DONE = 10; + + // member variables + protected final CommandsInterface mCi; + protected final IccCard mParentCard; + protected String mAid; + + static class LoadLinearFixedContext { + + int efid; + int recordNum, recordSize, countRecords; + boolean loadAll; + + Message onLoaded; + + ArrayList<byte[]> results; + + LoadLinearFixedContext(int efid, int recordNum, Message onLoaded) { + this.efid = efid; + this.recordNum = recordNum; + this.onLoaded = onLoaded; + this.loadAll = false; + } + + LoadLinearFixedContext(int efid, Message onLoaded) { + this.efid = efid; + this.recordNum = 1; + this.loadAll = true; + this.onLoaded = onLoaded; + } + } + + /** + * Default constructor + */ + protected IccFileHandler(IccCard card, String aid, CommandsInterface ci) { + mParentCard = card; + mAid = aid; + mCi = ci; + } + + public void dispose() { + } + + //***** Public Methods + + /** + * Load a record from a SIM Linear Fixed EF + * + * @param fileid EF id + * @param recordNum 1-based (not 0-based) record number + * @param onLoaded + * + * ((AsyncResult)(onLoaded.obj)).result is the byte[] + * + */ + public void loadEFLinearFixed(int fileid, int recordNum, Message onLoaded) { + Message response + = obtainMessage(EVENT_GET_RECORD_SIZE_DONE, + new LoadLinearFixedContext(fileid, recordNum, onLoaded)); + + mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(fileid), + 0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response); + } + + /** + * Load a image instance record from a SIM Linear Fixed EF-IMG + * + * @param recordNum 1-based (not 0-based) record number + * @param onLoaded + * + * ((AsyncResult)(onLoaded.obj)).result is the byte[] + * + */ + public void loadEFImgLinearFixed(int recordNum, Message onLoaded) { + Message response = obtainMessage(EVENT_READ_IMG_DONE, + new LoadLinearFixedContext(IccConstants.EF_IMG, recordNum, + onLoaded)); + + // TODO(): Verify when path changes are done. + mCi.iccIOForApp(COMMAND_GET_RESPONSE, IccConstants.EF_IMG, "img", + recordNum, READ_RECORD_MODE_ABSOLUTE, + GET_RESPONSE_EF_IMG_SIZE_BYTES, null, null, mAid, response); + } + + /** + * get record size for a linear fixed EF + * + * @param fileid EF id + * @param onLoaded ((AsnyncResult)(onLoaded.obj)).result is the recordSize[] + * int[0] is the record length int[1] is the total length of the EF + * file int[3] is the number of records in the EF file So int[0] * + * int[3] = int[1] + */ + public void getEFLinearRecordSize(int fileid, Message onLoaded) { + Message response + = obtainMessage(EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE, + new LoadLinearFixedContext(fileid, onLoaded)); + mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(fileid), + 0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response); + } + + /** + * Load all records from a SIM Linear Fixed EF + * + * @param fileid EF id + * @param onLoaded + * + * ((AsyncResult)(onLoaded.obj)).result is an ArrayList<byte[]> + * + */ + public void loadEFLinearFixedAll(int fileid, Message onLoaded) { + Message response = obtainMessage(EVENT_GET_RECORD_SIZE_DONE, + new LoadLinearFixedContext(fileid,onLoaded)); + + mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(fileid), + 0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response); + } + + /** + * Load a SIM Transparent EF + * + * @param fileid EF id + * @param onLoaded + * + * ((AsyncResult)(onLoaded.obj)).result is the byte[] + * + */ + + public void loadEFTransparent(int fileid, Message onLoaded) { + Message response = obtainMessage(EVENT_GET_BINARY_SIZE_DONE, + fileid, 0, onLoaded); + + mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, getEFPath(fileid), + 0, 0, GET_RESPONSE_EF_SIZE_BYTES, null, null, mAid, response); + } + + /** + * Load a SIM Transparent EF-IMG. Used right after loadEFImgLinearFixed to + * retrive STK's icon data. + * + * @param fileid EF id + * @param onLoaded + * + * ((AsyncResult)(onLoaded.obj)).result is the byte[] + * + */ + public void loadEFImgTransparent(int fileid, int highOffset, int lowOffset, + int length, Message onLoaded) { + Message response = obtainMessage(EVENT_READ_ICON_DONE, fileid, 0, + onLoaded); + + mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, "img", highOffset, lowOffset, + length, null, null, mAid, response); + } + + /** + * Update a record in a linear fixed EF + * @param fileid EF id + * @param recordNum 1-based (not 0-based) record number + * @param data must be exactly as long as the record in the EF + * @param pin2 for CHV2 operations, otherwist must be null + * @param onComplete onComplete.obj will be an AsyncResult + * onComplete.obj.userObj will be a IccIoResult on success + */ + public void updateEFLinearFixed(int fileid, int recordNum, byte[] data, + String pin2, Message onComplete) { + mCi.iccIOForApp(COMMAND_UPDATE_RECORD, fileid, getEFPath(fileid), + recordNum, READ_RECORD_MODE_ABSOLUTE, data.length, + IccUtils.bytesToHexString(data), pin2, mAid, onComplete); + } + + /** + * Update a transparent EF + * @param fileid EF id + * @param data must be exactly as long as the EF + */ + public void updateEFTransparent(int fileid, byte[] data, Message onComplete) { + mCi.iccIOForApp(COMMAND_UPDATE_BINARY, fileid, getEFPath(fileid), + 0, 0, data.length, + IccUtils.bytesToHexString(data), null, mAid, onComplete); + } + + + //***** Abstract Methods + + + //***** Private Methods + + private void sendResult(Message response, Object result, Throwable ex) { + if (response == null) { + return; + } + + AsyncResult.forMessage(response, result, ex); + + response.sendToTarget(); + } + + //***** Overridden from Handler + + public void handleMessage(Message msg) { + AsyncResult ar; + IccIoResult result; + Message response = null; + String str; + LoadLinearFixedContext lc; + + IccException iccException; + byte data[]; + int size; + int fileid; + int recordNum; + int recordSize[]; + + try { + switch (msg.what) { + case EVENT_READ_IMG_DONE: + ar = (AsyncResult) msg.obj; + lc = (LoadLinearFixedContext) ar.userObj; + result = (IccIoResult) ar.result; + response = lc.onLoaded; + + iccException = result.getException(); + if (iccException != null) { + sendResult(response, result.payload, ar.exception); + } + break; + case EVENT_READ_ICON_DONE: + ar = (AsyncResult) msg.obj; + response = (Message) ar.userObj; + result = (IccIoResult) ar.result; + + iccException = result.getException(); + if (iccException != null) { + sendResult(response, result.payload, ar.exception); + } + break; + case EVENT_GET_EF_LINEAR_RECORD_SIZE_DONE: + ar = (AsyncResult)msg.obj; + lc = (LoadLinearFixedContext) ar.userObj; + result = (IccIoResult) ar.result; + response = lc.onLoaded; + + if (ar.exception != null) { + sendResult(response, null, ar.exception); + break; + } + + iccException = result.getException(); + if (iccException != null) { + sendResult(response, null, iccException); + break; + } + + data = result.payload; + + if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE] || + EF_TYPE_LINEAR_FIXED != data[RESPONSE_DATA_STRUCTURE]) { + throw new IccFileTypeMismatch(); + } + + recordSize = new int[3]; + recordSize[0] = data[RESPONSE_DATA_RECORD_LENGTH] & 0xFF; + recordSize[1] = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8) + + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff); + recordSize[2] = recordSize[1] / recordSize[0]; + + sendResult(response, recordSize, null); + break; + case EVENT_GET_RECORD_SIZE_DONE: + ar = (AsyncResult)msg.obj; + lc = (LoadLinearFixedContext) ar.userObj; + result = (IccIoResult) ar.result; + response = lc.onLoaded; + + if (ar.exception != null) { + sendResult(response, null, ar.exception); + break; + } + + iccException = result.getException(); + + if (iccException != null) { + sendResult(response, null, iccException); + break; + } + + data = result.payload; + fileid = lc.efid; + recordNum = lc.recordNum; + + if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE]) { + throw new IccFileTypeMismatch(); + } + + if (EF_TYPE_LINEAR_FIXED != data[RESPONSE_DATA_STRUCTURE]) { + throw new IccFileTypeMismatch(); + } + + lc.recordSize = data[RESPONSE_DATA_RECORD_LENGTH] & 0xFF; + + size = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8) + + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff); + + lc.countRecords = size / lc.recordSize; + + if (lc.loadAll) { + lc.results = new ArrayList<byte[]>(lc.countRecords); + } + + mCi.iccIOForApp(COMMAND_READ_RECORD, lc.efid, getEFPath(lc.efid), + lc.recordNum, + READ_RECORD_MODE_ABSOLUTE, + lc.recordSize, null, null, mAid, + obtainMessage(EVENT_READ_RECORD_DONE, lc)); + break; + case EVENT_GET_BINARY_SIZE_DONE: + ar = (AsyncResult)msg.obj; + response = (Message) ar.userObj; + result = (IccIoResult) ar.result; + + if (ar.exception != null) { + sendResult(response, null, ar.exception); + break; + } + + iccException = result.getException(); + + if (iccException != null) { + sendResult(response, null, iccException); + break; + } + + data = result.payload; + + fileid = msg.arg1; + + if (TYPE_EF != data[RESPONSE_DATA_FILE_TYPE]) { + throw new IccFileTypeMismatch(); + } + + if (EF_TYPE_TRANSPARENT != data[RESPONSE_DATA_STRUCTURE]) { + throw new IccFileTypeMismatch(); + } + + size = ((data[RESPONSE_DATA_FILE_SIZE_1] & 0xff) << 8) + + (data[RESPONSE_DATA_FILE_SIZE_2] & 0xff); + + mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, getEFPath(fileid), + 0, 0, size, null, null, mAid, + obtainMessage(EVENT_READ_BINARY_DONE, + fileid, 0, response)); + break; + + case EVENT_READ_RECORD_DONE: + + ar = (AsyncResult)msg.obj; + lc = (LoadLinearFixedContext) ar.userObj; + result = (IccIoResult) ar.result; + response = lc.onLoaded; + + if (ar.exception != null) { + sendResult(response, null, ar.exception); + break; + } + + iccException = result.getException(); + + if (iccException != null) { + sendResult(response, null, iccException); + break; + } + + if (!lc.loadAll) { + sendResult(response, result.payload, null); + } else { + lc.results.add(result.payload); + + lc.recordNum++; + + if (lc.recordNum > lc.countRecords) { + sendResult(response, lc.results, null); + } else { + mCi.iccIOForApp(COMMAND_READ_RECORD, lc.efid, getEFPath(lc.efid), + lc.recordNum, + READ_RECORD_MODE_ABSOLUTE, + lc.recordSize, null, null, mAid, + obtainMessage(EVENT_READ_RECORD_DONE, lc)); + } + } + + break; + + case EVENT_READ_BINARY_DONE: + ar = (AsyncResult)msg.obj; + response = (Message) ar.userObj; + result = (IccIoResult) ar.result; + + if (ar.exception != null) { + sendResult(response, null, ar.exception); + break; + } + + iccException = result.getException(); + + if (iccException != null) { + sendResult(response, null, iccException); + break; + } + + sendResult(response, result.payload, null); + break; + + }} catch (Exception exc) { + if (response != null) { + sendResult(response, null, exc); + } else { + loge("uncaught exception" + exc); + } + } + } + + /** + * Returns the root path of the EF file. + * i.e returns MasterFile + DFfile as a string. + * Ex: For EF_ADN on a SIM, it will return "3F007F10" + * This function handles only EFids that are common to + * RUIM, SIM, USIM and other types of Icc cards. + * + * @param efId + * @return root path of the file. + */ + protected String getCommonIccEFPath(int efid) { + switch(efid) { + case EF_ADN: + case EF_FDN: + case EF_MSISDN: + case EF_SDN: + case EF_EXT1: + case EF_EXT2: + case EF_EXT3: + return MF_SIM + DF_TELECOM; + + case EF_ICCID: + case EF_PL: + return MF_SIM; + case EF_IMG: + return MF_SIM + DF_TELECOM + DF_GRAPHICS; + } + return null; + } + + protected abstract String getEFPath(int efid); + protected abstract void logd(String s); + + protected abstract void loge(String s); + protected void setAid(String aid) { + mAid = aid; + } + +} diff --git a/src/java/com/android/internal/telephony/IccFileNotFound.java b/src/java/com/android/internal/telephony/IccFileNotFound.java new file mode 100644 index 0000000..915cea6 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccFileNotFound.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class IccFileNotFound extends IccException { + IccFileNotFound() { + + } + + IccFileNotFound(String s) { + super(s); + } + + IccFileNotFound(int ef) { + super("ICC EF Not Found 0x" + Integer.toHexString(ef)); + } +} diff --git a/src/java/com/android/internal/telephony/IccFileTypeMismatch.java b/src/java/com/android/internal/telephony/IccFileTypeMismatch.java new file mode 100644 index 0000000..66fcfa9 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccFileTypeMismatch.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class IccFileTypeMismatch extends IccException { + public IccFileTypeMismatch() { + + } + + public IccFileTypeMismatch(String s) { + super(s); + } +} diff --git a/src/java/com/android/internal/telephony/IccIoResult.java b/src/java/com/android/internal/telephony/IccIoResult.java new file mode 100644 index 0000000..7043da5 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccIoResult.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public class +IccIoResult { + public int sw1; + public int sw2; + + public byte[] payload; + + public IccIoResult(int sw1, int sw2, byte[] payload) { + this.sw1 = sw1; + this.sw2 = sw2; + this.payload = payload; + } + + public IccIoResult(int sw1, int sw2, String hexString) { + this(sw1, sw2, IccUtils.hexStringToBytes(hexString)); + } + + public String toString() { + return "IccIoResponse sw1:0x" + Integer.toHexString(sw1) + " sw2:0x" + + Integer.toHexString(sw2); + } + + /** + * true if this operation was successful + * See GSM 11.11 Section 9.4 + * (the fun stuff is absent in 51.011) + */ + public boolean success() { + return sw1 == 0x90 || sw1 == 0x91 || sw1 == 0x9e || sw1 == 0x9f; + } + + /** + * Returns exception on error or null if success + */ + public IccException getException() { + if (success()) return null; + + switch (sw1) { + case 0x94: + if (sw2 == 0x08) { + return new IccFileTypeMismatch(); + } else { + return new IccFileNotFound(); + } + default: + return new IccException("sw1:" + sw1 + " sw2:" + sw2); + } + } +} diff --git a/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManager.java b/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManager.java new file mode 100644 index 0000000..45562ca --- /dev/null +++ b/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManager.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.pm.PackageManager; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ServiceManager; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * SimPhoneBookInterfaceManager to provide an inter-process communication to + * access ADN-like SIM records. + */ +public abstract class IccPhoneBookInterfaceManager extends IIccPhoneBook.Stub { + protected static final boolean DBG = true; + + protected PhoneBase phone; + protected AdnRecordCache adnCache; + protected final Object mLock = new Object(); + protected int recordSize[]; + protected boolean success; + protected List<AdnRecord> records; + + protected static final boolean ALLOW_SIM_OP_IN_UI_THREAD = false; + + protected static final int EVENT_GET_SIZE_DONE = 1; + protected static final int EVENT_LOAD_DONE = 2; + protected static final int EVENT_UPDATE_DONE = 3; + + protected Handler mBaseHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_GET_SIZE_DONE: + ar = (AsyncResult) msg.obj; + synchronized (mLock) { + if (ar.exception == null) { + recordSize = (int[])ar.result; + // recordSize[0] is the record length + // recordSize[1] is the total length of the EF file + // recordSize[2] is the number of records in the EF file + logd("GET_RECORD_SIZE Size " + recordSize[0] + + " total " + recordSize[1] + + " #record " + recordSize[2]); + } + notifyPending(ar); + } + break; + case EVENT_UPDATE_DONE: + ar = (AsyncResult) msg.obj; + synchronized (mLock) { + success = (ar.exception == null); + notifyPending(ar); + } + break; + case EVENT_LOAD_DONE: + ar = (AsyncResult)msg.obj; + synchronized (mLock) { + if (ar.exception == null) { + records = (List<AdnRecord>) ar.result; + } else { + if(DBG) logd("Cannot load ADN records"); + if (records != null) { + records.clear(); + } + } + notifyPending(ar); + } + break; + } + } + + private void notifyPending(AsyncResult ar) { + if (ar.userObj == null) { + return; + } + AtomicBoolean status = (AtomicBoolean) ar.userObj; + status.set(true); + mLock.notifyAll(); + } + }; + + public IccPhoneBookInterfaceManager(PhoneBase phone) { + this.phone = phone; + } + + public void dispose() { + } + + protected void publish() { + //NOTE service "simphonebook" added by IccSmsInterfaceManagerProxy + ServiceManager.addService("simphonebook", this); + } + + protected abstract void logd(String msg); + + protected abstract void loge(String msg); + + /** + * Replace oldAdn with newAdn in ADN-like record in EF + * + * getAdnRecordsInEf must be called at least once before this function, + * otherwise an error will be returned. Currently the email field + * if set in the ADN record is ignored. + * throws SecurityException if no WRITE_CONTACTS permission + * + * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN + * @param oldTag adn tag to be replaced + * @param oldPhoneNumber adn number to be replaced + * Set both oldTag and oldPhoneNubmer to "" means to replace an + * empty record, aka, insert new record + * @param newTag adn tag to be stored + * @param newPhoneNumber adn number ot be stored + * Set both newTag and newPhoneNubmer to "" means to replace the old + * record with empty one, aka, delete old record + * @param pin2 required to update EF_FDN, otherwise must be null + * @return true for success + */ + public boolean + updateAdnRecordsInEfBySearch (int efid, + String oldTag, String oldPhoneNumber, + String newTag, String newPhoneNumber, String pin2) { + + + if (phone.getContext().checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Requires android.permission.WRITE_CONTACTS permission"); + } + + + if (DBG) logd("updateAdnRecordsInEfBySearch: efid=" + efid + + " ("+ oldTag + "," + oldPhoneNumber + ")"+ "==>" + + " ("+ newTag + "," + newPhoneNumber + ")"+ " pin2=" + pin2); + + efid = updateEfForIccType(efid); + + synchronized(mLock) { + checkThread(); + success = false; + AtomicBoolean status = new AtomicBoolean(false); + Message response = mBaseHandler.obtainMessage(EVENT_UPDATE_DONE, status); + AdnRecord oldAdn = new AdnRecord(oldTag, oldPhoneNumber); + AdnRecord newAdn = new AdnRecord(newTag, newPhoneNumber); + adnCache.updateAdnBySearch(efid, oldAdn, newAdn, pin2, response); + waitForResult(status); + } + return success; + } + + /** + * Update an ADN-like EF record by record index + * + * This is useful for iteration the whole ADN file, such as write the whole + * phone book or erase/format the whole phonebook. Currently the email field + * if set in the ADN record is ignored. + * throws SecurityException if no WRITE_CONTACTS permission + * + * @param efid must be one among EF_ADN, EF_FDN, and EF_SDN + * @param newTag adn tag to be stored + * @param newPhoneNumber adn number to be stored + * Set both newTag and newPhoneNubmer to "" means to replace the old + * record with empty one, aka, delete old record + * @param index is 1-based adn record index to be updated + * @param pin2 required to update EF_FDN, otherwise must be null + * @return true for success + */ + public boolean + updateAdnRecordsInEfByIndex(int efid, String newTag, + String newPhoneNumber, int index, String pin2) { + + if (phone.getContext().checkCallingOrSelfPermission( + android.Manifest.permission.WRITE_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Requires android.permission.WRITE_CONTACTS permission"); + } + + if (DBG) logd("updateAdnRecordsInEfByIndex: efid=" + efid + + " Index=" + index + " ==> " + + "("+ newTag + "," + newPhoneNumber + ")"+ " pin2=" + pin2); + synchronized(mLock) { + checkThread(); + success = false; + AtomicBoolean status = new AtomicBoolean(false); + Message response = mBaseHandler.obtainMessage(EVENT_UPDATE_DONE, status); + AdnRecord newAdn = new AdnRecord(newTag, newPhoneNumber); + adnCache.updateAdnByIndex(efid, newAdn, index, pin2, response); + waitForResult(status); + } + return success; + } + + /** + * Get the capacity of records in efid + * + * @param efid the EF id of a ADN-like ICC + * @return int[3] array + * recordSizes[0] is the single record length + * recordSizes[1] is the total length of the EF file + * recordSizes[2] is the number of records in the EF file + */ + public abstract int[] getAdnRecordsSize(int efid); + + /** + * Loads the AdnRecords in efid and returns them as a + * List of AdnRecords + * + * throws SecurityException if no READ_CONTACTS permission + * + * @param efid the EF id of a ADN-like ICC + * @return List of AdnRecord + */ + public List<AdnRecord> getAdnRecordsInEf(int efid) { + + if (phone.getContext().checkCallingOrSelfPermission( + android.Manifest.permission.READ_CONTACTS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Requires android.permission.READ_CONTACTS permission"); + } + + efid = updateEfForIccType(efid); + if (DBG) logd("getAdnRecordsInEF: efid=" + efid); + + synchronized(mLock) { + checkThread(); + AtomicBoolean status = new AtomicBoolean(false); + Message response = mBaseHandler.obtainMessage(EVENT_LOAD_DONE, status); + adnCache.requestLoadAllAdnLike(efid, adnCache.extensionEfForEf(efid), response); + waitForResult(status); + } + return records; + } + + protected void checkThread() { + if (!ALLOW_SIM_OP_IN_UI_THREAD) { + // Make sure this isn't the UI thread, since it will block + if (mBaseHandler.getLooper().equals(Looper.myLooper())) { + loge("query() called on the main UI thread!"); + throw new IllegalStateException( + "You cannot call query on this provder from the main UI thread."); + } + } + } + + protected void waitForResult(AtomicBoolean status) { + while (!status.get()) { + try { + mLock.wait(); + } catch (InterruptedException e) { + logd("interrupted while trying to update by search"); + } + } + } + + private int updateEfForIccType(int efid) { + // Check if we are trying to read ADN records + if (efid == IccConstants.EF_ADN) { + if (phone.getIccCard().isApplicationOnIcc(IccCardApplication.AppType.APPTYPE_USIM)) { + return IccConstants.EF_PBR; + } + } + return efid; + } +} + diff --git a/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManagerProxy.java b/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManagerProxy.java new file mode 100644 index 0000000..1c0fc52 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccPhoneBookInterfaceManagerProxy.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.content.pm.PackageManager; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.ServiceManager; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + + +/** + * SimPhoneBookInterfaceManager to provide an inter-process communication to + * access ADN-like SIM records. + */ +public class IccPhoneBookInterfaceManagerProxy extends IIccPhoneBook.Stub { + private IccPhoneBookInterfaceManager mIccPhoneBookInterfaceManager; + + public IccPhoneBookInterfaceManagerProxy(IccPhoneBookInterfaceManager + iccPhoneBookInterfaceManager) { + mIccPhoneBookInterfaceManager = iccPhoneBookInterfaceManager; + if(ServiceManager.getService("simphonebook") == null) { + ServiceManager.addService("simphonebook", this); + } + } + + public void setmIccPhoneBookInterfaceManager( + IccPhoneBookInterfaceManager iccPhoneBookInterfaceManager) { + this.mIccPhoneBookInterfaceManager = iccPhoneBookInterfaceManager; + } + + public boolean + updateAdnRecordsInEfBySearch (int efid, + String oldTag, String oldPhoneNumber, + String newTag, String newPhoneNumber, + String pin2) throws android.os.RemoteException { + return mIccPhoneBookInterfaceManager.updateAdnRecordsInEfBySearch( + efid, oldTag, oldPhoneNumber, newTag, newPhoneNumber, pin2); + } + + public boolean + updateAdnRecordsInEfByIndex(int efid, String newTag, + String newPhoneNumber, int index, String pin2) throws android.os.RemoteException { + return mIccPhoneBookInterfaceManager.updateAdnRecordsInEfByIndex(efid, + newTag, newPhoneNumber, index, pin2); + } + + public int[] getAdnRecordsSize(int efid) throws android.os.RemoteException { + return mIccPhoneBookInterfaceManager.getAdnRecordsSize(efid); + } + + public List<AdnRecord> getAdnRecordsInEf(int efid) throws android.os.RemoteException { + return mIccPhoneBookInterfaceManager.getAdnRecordsInEf(efid); + } +} diff --git a/src/java/com/android/internal/telephony/IccProvider.java b/src/java/com/android/internal/telephony/IccProvider.java new file mode 100644 index 0000000..a66e19d --- /dev/null +++ b/src/java/com/android/internal/telephony/IccProvider.java @@ -0,0 +1,431 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.ContentProvider; +import android.content.UriMatcher; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.TextUtils; +import android.util.Log; + +import java.util.List; + +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.AdnRecord; +import com.android.internal.telephony.IIccPhoneBook; + + +/** + * {@hide} + */ +public class IccProvider extends ContentProvider { + private static final String TAG = "IccProvider"; + private static final boolean DBG = false; + + + private static final String[] ADDRESS_BOOK_COLUMN_NAMES = new String[] { + "name", + "number", + "emails", + "_id" + }; + + private static final int ADN = 1; + private static final int FDN = 2; + private static final int SDN = 3; + + private static final String STR_TAG = "tag"; + private static final String STR_NUMBER = "number"; + private static final String STR_EMAILS = "emails"; + private static final String STR_PIN2 = "pin2"; + + private static final UriMatcher URL_MATCHER = + new UriMatcher(UriMatcher.NO_MATCH); + + static { + URL_MATCHER.addURI("icc", "adn", ADN); + URL_MATCHER.addURI("icc", "fdn", FDN); + URL_MATCHER.addURI("icc", "sdn", SDN); + } + + + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sort) { + switch (URL_MATCHER.match(url)) { + case ADN: + return loadFromEf(IccConstants.EF_ADN); + + case FDN: + return loadFromEf(IccConstants.EF_FDN); + + case SDN: + return loadFromEf(IccConstants.EF_SDN); + + default: + throw new IllegalArgumentException("Unknown URL " + url); + } + } + + @Override + public String getType(Uri url) { + switch (URL_MATCHER.match(url)) { + case ADN: + case FDN: + case SDN: + return "vnd.android.cursor.dir/sim-contact"; + + default: + throw new IllegalArgumentException("Unknown URL " + url); + } + } + + @Override + public Uri insert(Uri url, ContentValues initialValues) { + Uri resultUri; + int efType; + String pin2 = null; + + if (DBG) log("insert"); + + int match = URL_MATCHER.match(url); + switch (match) { + case ADN: + efType = IccConstants.EF_ADN; + break; + + case FDN: + efType = IccConstants.EF_FDN; + pin2 = initialValues.getAsString("pin2"); + break; + + default: + throw new UnsupportedOperationException( + "Cannot insert into URL: " + url); + } + + String tag = initialValues.getAsString("tag"); + String number = initialValues.getAsString("number"); + // TODO(): Read email instead of sending null. + boolean success = addIccRecordToEf(efType, tag, number, null, pin2); + + if (!success) { + return null; + } + + StringBuilder buf = new StringBuilder("content://icc/"); + switch (match) { + case ADN: + buf.append("adn/"); + break; + + case FDN: + buf.append("fdn/"); + break; + } + + // TODO: we need to find out the rowId for the newly added record + buf.append(0); + + resultUri = Uri.parse(buf.toString()); + + /* + // notify interested parties that an insertion happened + getContext().getContentResolver().notifyInsert( + resultUri, rowID, null); + */ + + return resultUri; + } + + private String normalizeValue(String inVal) { + int len = inVal.length(); + String retVal = inVal; + + if (inVal.charAt(0) == '\'' && inVal.charAt(len-1) == '\'') { + retVal = inVal.substring(1, len-1); + } + + return retVal; + } + + @Override + public int delete(Uri url, String where, String[] whereArgs) { + int efType; + + if (DBG) log("delete"); + + int match = URL_MATCHER.match(url); + switch (match) { + case ADN: + efType = IccConstants.EF_ADN; + break; + + case FDN: + efType = IccConstants.EF_FDN; + break; + + default: + throw new UnsupportedOperationException( + "Cannot insert into URL: " + url); + } + + // parse where clause + String tag = null; + String number = null; + String[] emails = null; + String pin2 = null; + + String[] tokens = where.split("AND"); + int n = tokens.length; + + while (--n >= 0) { + String param = tokens[n]; + if (DBG) log("parsing '" + param + "'"); + + String[] pair = param.split("="); + + if (pair.length != 2) { + Log.e(TAG, "resolve: bad whereClause parameter: " + param); + continue; + } + + String key = pair[0].trim(); + String val = pair[1].trim(); + + if (STR_TAG.equals(key)) { + tag = normalizeValue(val); + } else if (STR_NUMBER.equals(key)) { + number = normalizeValue(val); + } else if (STR_EMAILS.equals(key)) { + //TODO(): Email is null. + emails = null; + } else if (STR_PIN2.equals(key)) { + pin2 = normalizeValue(val); + } + } + + if (TextUtils.isEmpty(number)) { + return 0; + } + + if (efType == IccConstants.EF_FDN && TextUtils.isEmpty(pin2)) { + return 0; + } + + boolean success = deleteIccRecordFromEf(efType, tag, number, emails, pin2); + if (!success) { + return 0; + } + + return 1; + } + + @Override + public int update(Uri url, ContentValues values, String where, String[] whereArgs) { + int efType; + String pin2 = null; + + if (DBG) log("update"); + + int match = URL_MATCHER.match(url); + switch (match) { + case ADN: + efType = IccConstants.EF_ADN; + break; + + case FDN: + efType = IccConstants.EF_FDN; + pin2 = values.getAsString("pin2"); + break; + + default: + throw new UnsupportedOperationException( + "Cannot insert into URL: " + url); + } + + String tag = values.getAsString("tag"); + String number = values.getAsString("number"); + String[] emails = null; + String newTag = values.getAsString("newTag"); + String newNumber = values.getAsString("newNumber"); + String[] newEmails = null; + // TODO(): Update for email. + boolean success = updateIccRecordInEf(efType, tag, number, + newTag, newNumber, pin2); + + if (!success) { + return 0; + } + + return 1; + } + + private MatrixCursor loadFromEf(int efType) { + if (DBG) log("loadFromEf: efType=" + efType); + + List<AdnRecord> adnRecords = null; + try { + IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface( + ServiceManager.getService("simphonebook")); + if (iccIpb != null) { + adnRecords = iccIpb.getAdnRecordsInEf(efType); + } + } catch (RemoteException ex) { + // ignore it + } catch (SecurityException ex) { + if (DBG) log(ex.toString()); + } + + if (adnRecords != null) { + // Load the results + final int N = adnRecords.size(); + final MatrixCursor cursor = new MatrixCursor(ADDRESS_BOOK_COLUMN_NAMES, N); + if (DBG) log("adnRecords.size=" + N); + for (int i = 0; i < N ; i++) { + loadRecord(adnRecords.get(i), cursor, i); + } + return cursor; + } else { + // No results to load + Log.w(TAG, "Cannot load ADN records"); + return new MatrixCursor(ADDRESS_BOOK_COLUMN_NAMES); + } + } + + private boolean + addIccRecordToEf(int efType, String name, String number, String[] emails, String pin2) { + if (DBG) log("addIccRecordToEf: efType=" + efType + ", name=" + name + + ", number=" + number + ", emails=" + emails); + + boolean success = false; + + // TODO: do we need to call getAdnRecordsInEf() before calling + // updateAdnRecordsInEfBySearch()? In any case, we will leave + // the UI level logic to fill that prereq if necessary. But + // hopefully, we can remove this requirement. + + try { + IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface( + ServiceManager.getService("simphonebook")); + if (iccIpb != null) { + success = iccIpb.updateAdnRecordsInEfBySearch(efType, "", "", + name, number, pin2); + } + } catch (RemoteException ex) { + // ignore it + } catch (SecurityException ex) { + if (DBG) log(ex.toString()); + } + if (DBG) log("addIccRecordToEf: " + success); + return success; + } + + private boolean + updateIccRecordInEf(int efType, String oldName, String oldNumber, + String newName, String newNumber, String pin2) { + if (DBG) log("updateIccRecordInEf: efType=" + efType + + ", oldname=" + oldName + ", oldnumber=" + oldNumber + + ", newname=" + newName + ", newnumber=" + newNumber); + boolean success = false; + + try { + IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface( + ServiceManager.getService("simphonebook")); + if (iccIpb != null) { + success = iccIpb.updateAdnRecordsInEfBySearch(efType, + oldName, oldNumber, newName, newNumber, pin2); + } + } catch (RemoteException ex) { + // ignore it + } catch (SecurityException ex) { + if (DBG) log(ex.toString()); + } + if (DBG) log("updateIccRecordInEf: " + success); + return success; + } + + + private boolean deleteIccRecordFromEf(int efType, String name, String number, String[] emails, + String pin2) { + if (DBG) log("deleteIccRecordFromEf: efType=" + efType + + ", name=" + name + ", number=" + number + ", emails=" + emails + ", pin2=" + pin2); + + boolean success = false; + + try { + IIccPhoneBook iccIpb = IIccPhoneBook.Stub.asInterface( + ServiceManager.getService("simphonebook")); + if (iccIpb != null) { + success = iccIpb.updateAdnRecordsInEfBySearch(efType, + name, number, "", "", pin2); + } + } catch (RemoteException ex) { + // ignore it + } catch (SecurityException ex) { + if (DBG) log(ex.toString()); + } + if (DBG) log("deleteIccRecordFromEf: " + success); + return success; + } + + /** + * Loads an AdnRecord into a MatrixCursor. Must be called with mLock held. + * + * @param record the ADN record to load from + * @param cursor the cursor to receive the results + */ + private void loadRecord(AdnRecord record, MatrixCursor cursor, int id) { + if (!record.isEmpty()) { + Object[] contact = new Object[4]; + String alphaTag = record.getAlphaTag(); + String number = record.getNumber(); + + if (DBG) log("loadRecord: " + alphaTag + ", " + number + ","); + contact[0] = alphaTag; + contact[1] = number; + + String[] emails = record.getEmails(); + if (emails != null) { + StringBuilder emailString = new StringBuilder(); + for (String email: emails) { + if (DBG) log("Adding email:" + email); + emailString.append(email); + emailString.append(","); + } + contact[2] = emailString.toString(); + } + contact[3] = id; + cursor.addRow(contact); + } + } + + private void log(String msg) { + Log.d(TAG, "[IccProvider] " + msg); + } + +} diff --git a/src/java/com/android/internal/telephony/IccRecords.java b/src/java/com/android/internal/telephony/IccRecords.java new file mode 100644 index 0000000..41c9d5a --- /dev/null +++ b/src/java/com/android/internal/telephony/IccRecords.java @@ -0,0 +1,422 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; + +import com.android.internal.telephony.gsm.UsimServiceTable; +import com.android.internal.telephony.ims.IsimRecords; + +/** + * {@hide} + */ +public abstract class IccRecords extends Handler implements IccConstants { + + protected static final boolean DBG = true; + // ***** Instance Variables + protected boolean mDestroyed = false; // set to true once this object needs to be disposed of + protected Context mContext; + protected CommandsInterface mCi; + protected IccFileHandler mFh; + protected IccCard mParentCard; + + protected RegistrantList recordsLoadedRegistrants = new RegistrantList(); + protected RegistrantList mRecordsEventsRegistrants = new RegistrantList(); + protected RegistrantList mNewSmsRegistrants = new RegistrantList(); + protected RegistrantList mNetworkSelectionModeAutomaticRegistrants = new RegistrantList(); + + protected int recordsToLoad; // number of pending load requests + + protected AdnRecordCache adnCache; + + // ***** Cached SIM State; cleared on channel close + + protected boolean recordsRequested = false; // true if we've made requests for the sim records + + public String iccid; + protected String msisdn = null; // My mobile number + protected String msisdnTag = null; + protected String voiceMailNum = null; + protected String voiceMailTag = null; + protected String newVoiceMailNum = null; + protected String newVoiceMailTag = null; + protected boolean isVoiceMailFixed = false; + protected int countVoiceMessages = 0; + + protected int mncLength = UNINITIALIZED; + protected int mailboxIndex = 0; // 0 is no mailbox dailing number associated + + protected String spn; + + // ***** Constants + + // Markers for mncLength + protected static final int UNINITIALIZED = -1; + protected static final int UNKNOWN = 0; + + // Bitmasks for SPN display rules. + protected static final int SPN_RULE_SHOW_SPN = 0x01; + protected static final int SPN_RULE_SHOW_PLMN = 0x02; + + // ***** Event Constants + protected static final int EVENT_SET_MSISDN_DONE = 30; + public static final int EVENT_MWI = 0; + public static final int EVENT_CFI = 1; + public static final int EVENT_SPN = 2; + + public static final int EVENT_GET_ICC_RECORD_DONE = 100; + + /** + * Generic ICC record loaded callback. Subclasses can call EF load methods on + * {@link IccFileHandler} passing a Message for onLoaded with the what field set to + * {@link #EVENT_GET_ICC_RECORD_DONE} and the obj field set to an instance + * of this interface. The {@link #handleMessage} method in this class will print a + * log message using {@link #getEfName()} and decrement {@link #recordsToLoad}. + * + * If the record load was successful, {@link #onRecordLoaded} will be called with the result. + * Otherwise, an error log message will be output by {@link #handleMessage} and + * {@link #onRecordLoaded} will not be called. + */ + public interface IccRecordLoaded { + String getEfName(); + void onRecordLoaded(AsyncResult ar); + } + + // ***** Constructor + public IccRecords(IccCard card, Context c, CommandsInterface ci) { + mContext = c; + mCi = ci; + mFh = card.getIccFileHandler(); + mParentCard = card; + } + + /** + * Call when the IccRecords object is no longer going to be used. + */ + public void dispose() { + mDestroyed = true; + mParentCard = null; + mFh = null; + mCi = null; + mContext = null; + } + + protected abstract void onRadioOffOrNotAvailable(); + public abstract void onReady(); + + //***** Public Methods + public AdnRecordCache getAdnCache() { + return adnCache; + } + + public IccCard getIccCard() { + return mParentCard; + } + + public void registerForRecordsLoaded(Handler h, int what, Object obj) { + if (mDestroyed) { + return; + } + + Registrant r = new Registrant(h, what, obj); + recordsLoadedRegistrants.add(r); + + if (recordsToLoad == 0 && recordsRequested == true) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + public void unregisterForRecordsLoaded(Handler h) { + recordsLoadedRegistrants.remove(h); + } + + public void registerForRecordsEvents(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mRecordsEventsRegistrants.add(r); + } + public void unregisterForRecordsEvents(Handler h) { + mRecordsEventsRegistrants.remove(h); + } + + public void registerForNewSms(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mNewSmsRegistrants.add(r); + } + public void unregisterForNewSms(Handler h) { + mNewSmsRegistrants.remove(h); + } + + public void registerForNetworkSelectionModeAutomatic( + Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mNetworkSelectionModeAutomaticRegistrants.add(r); + } + public void unregisterForNetworkSelectionModeAutomatic(Handler h) { + mNetworkSelectionModeAutomaticRegistrants.remove(h); + } + + /** + * Get the International Mobile Subscriber ID (IMSI) on a SIM + * for GSM, UMTS and like networks. Default is null if IMSI is + * not supported or unavailable. + * + * @return null if SIM is not yet ready or unavailable + */ + public String getIMSI() { + return null; + } + + public String getMsisdnNumber() { + return msisdn; + } + + /** + * Set subscriber number to SIM record + * + * The subscriber number is stored in EF_MSISDN (TS 51.011) + * + * When the operation is complete, onComplete will be sent to its handler + * + * @param alphaTag alpha-tagging of the dailing nubmer (up to 10 characters) + * @param number dailing nubmer (up to 20 digits) + * if the number starts with '+', then set to international TOA + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void setMsisdnNumber(String alphaTag, String number, + Message onComplete) { + + msisdn = number; + msisdnTag = alphaTag; + + if(DBG) log("Set MSISDN: " + msisdnTag +" " + msisdn); + + + AdnRecord adn = new AdnRecord(msisdnTag, msisdn); + + new AdnRecordLoader(mFh).updateEF(adn, EF_MSISDN, EF_EXT1, 1, null, + obtainMessage(EVENT_SET_MSISDN_DONE, onComplete)); + } + + public String getMsisdnAlphaTag() { + return msisdnTag; + } + + public String getVoiceMailNumber() { + return voiceMailNum; + } + + /** + * Return Service Provider Name stored in SIM (EF_SPN=0x6F46) or in RUIM (EF_RUIM_SPN=0x6F41) + * @return null if SIM is not yet ready or no RUIM entry + */ + public String getServiceProviderName() { + return spn; + } + + /** + * Set voice mail number to SIM record + * + * The voice mail number can be stored either in EF_MBDN (TS 51.011) or + * EF_MAILBOX_CPHS (CPHS 4.2) + * + * If EF_MBDN is available, store the voice mail number to EF_MBDN + * + * If EF_MAILBOX_CPHS is enabled, store the voice mail number to EF_CHPS + * + * So the voice mail number will be stored in both EFs if both are available + * + * Return error only if both EF_MBDN and EF_MAILBOX_CPHS fail. + * + * When the operation is complete, onComplete will be sent to its handler + * + * @param alphaTag alpha-tagging of the dailing nubmer (upto 10 characters) + * @param voiceNumber dailing nubmer (upto 20 digits) + * if the number is start with '+', then set to international TOA + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public abstract void setVoiceMailNumber(String alphaTag, String voiceNumber, + Message onComplete); + + public String getVoiceMailAlphaTag() { + return voiceMailTag; + } + + /** + * Sets the SIM voice message waiting indicator records + * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported + * @param countWaiting The number of messages waiting, if known. Use + * -1 to indicate that an unknown number of + * messages are waiting + */ + public abstract void setVoiceMessageWaiting(int line, int countWaiting); + + /** @return true if there are messages waiting, false otherwise. */ + public boolean getVoiceMessageWaiting() { + return countVoiceMessages != 0; + } + + /** + * Returns number of voice messages waiting, if available + * If not available (eg, on an older CPHS SIM) -1 is returned if + * getVoiceMessageWaiting() is true + */ + public int getVoiceMessageCount() { + return countVoiceMessages; + } + + /** + * Called by STK Service when REFRESH is received. + * @param fileChanged indicates whether any files changed + * @param fileList if non-null, a list of EF files that changed + */ + public abstract void onRefresh(boolean fileChanged, int[] fileList); + + + public boolean getRecordsLoaded() { + if (recordsToLoad == 0 && recordsRequested == true) { + return true; + } else { + return false; + } + } + + //***** Overridden from Handler + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_GET_ICC_RECORD_DONE: + try { + AsyncResult ar = (AsyncResult) msg.obj; + IccRecordLoaded recordLoaded = (IccRecordLoaded) ar.userObj; + if (DBG) log(recordLoaded.getEfName() + " LOADED"); + + if (ar.exception != null) { + loge("Record Load Exception: " + ar.exception); + } else { + recordLoaded.onRecordLoaded(ar); + } + }catch (RuntimeException exc) { + // I don't want these exceptions to be fatal + loge("Exception parsing SIM record: " + exc); + } finally { + // Count up record load responses even if they are fails + onRecordLoaded(); + } + break; + + default: + super.handleMessage(msg); + } + } + + protected abstract void onRecordLoaded(); + + protected abstract void onAllRecordsLoaded(); + + /** + * Returns the SpnDisplayRule based on settings on the SIM and the + * specified plmn (currently-registered PLMN). See TS 22.101 Annex A + * and TS 51.011 10.3.11 for details. + * + * If the SPN is not found on the SIM, the rule is always PLMN_ONLY. + * Generally used for GSM/UMTS and the like SIMs. + */ + public abstract int getDisplayRule(String plmn); + + /** + * Return true if "Restriction of menu options for manual PLMN selection" + * bit is set or EF_CSP data is unavailable, return false otherwise. + * Generally used for GSM/UMTS and the like SIMs. + */ + public boolean isCspPlmnEnabled() { + return false; + } + + /** + * Returns the 5 or 6 digit MCC/MNC of the operator that + * provided the SIM card. Returns null of SIM is not yet ready + * or is not valid for the type of IccCard. Generally used for + * GSM/UMTS and the like SIMS + */ + public String getOperatorNumeric() { + return null; + } + + /** + * Get the current Voice call forwarding flag for GSM/UMTS and the like SIMs + * + * @return true if enabled + */ + public boolean getVoiceCallForwardingFlag() { + return false; + } + + /** + * Set the voice call forwarding flag for GSM/UMTS and the like SIMs + * + * @param line to enable/disable + * @param enable + */ + public void setVoiceCallForwardingFlag(int line, boolean enable) { + } + + /** + * Indicates wether SIM is in provisioned state or not. + * Overridden only if SIM can be dynamically provisioned via OTA. + * + * @return true if provisioned + */ + public boolean isProvisioned () { + return true; + } + + /** + * Write string to log file + * + * @param s is the string to write + */ + protected abstract void log(String s); + + /** + * Write error string to log file. + * + * @param s is the string to write + */ + protected abstract void loge(String s); + + /** + * Return an interface to retrieve the ISIM records for IMS, if available. + * @return the interface to retrieve the ISIM records, or null if not supported + */ + public IsimRecords getIsimRecords() { + return null; + } + + public UsimServiceTable getUsimServiceTable() { + return null; + } +} diff --git a/src/java/com/android/internal/telephony/IccRefreshResponse.java b/src/java/com/android/internal/telephony/IccRefreshResponse.java new file mode 100644 index 0000000..6806703 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccRefreshResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +/** + * See also RIL_SimRefresh in include/telephony/ril.h + * + * {@hide} + */ + +public class IccRefreshResponse { + + public static final int REFRESH_RESULT_FILE_UPDATE = 0; /* Single file was updated */ + public static final int REFRESH_RESULT_INIT = 1; /* The Icc has been initialized */ + public static final int REFRESH_RESULT_RESET = 2; /* The Icc was reset */ + + public int refreshResult; /* Sim Refresh result */ + public int efId; /* EFID */ + public String aid; /* null terminated string, e.g., + from 0xA0, 0x00 -> 0x41, + 0x30, 0x30, 0x30 */ + /* Example: a0000000871002f310ffff89080000ff */ + + @Override + public String toString() { + return "{" + refreshResult + ", " + aid +", " + efId + "}"; + } +} diff --git a/src/java/com/android/internal/telephony/IccServiceTable.java b/src/java/com/android/internal/telephony/IccServiceTable.java new file mode 100644 index 0000000..ed74a11 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccServiceTable.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import android.util.Log; + +/** + * Wrapper class for an ICC EF containing a bit field of enabled services. + */ +public abstract class IccServiceTable { + protected final byte[] mServiceTable; + + protected IccServiceTable(byte[] table) { + mServiceTable = table; + } + + // Get the class name to use for log strings + protected abstract String getTag(); + + // Get the array of enums to use for toString + protected abstract Object[] getValues(); + + /** + * Returns if the specified service is available. + * @param service the service number as a zero-based offset (the enum ordinal) + * @return true if the service is available; false otherwise + */ + protected boolean isAvailable(int service) { + int offset = service / 8; + if (offset >= mServiceTable.length) { + // Note: Enums are zero-based, but the TS service numbering is one-based + Log.e(getTag(), "isAvailable for service " + (service + 1) + " fails, max service is " + + (mServiceTable.length * 8)); + return false; + } + int bit = service % 8; + return (mServiceTable[offset] & (1 << bit)) != 0; + } + + public String toString() { + Object[] values = getValues(); + int numBytes = mServiceTable.length; + StringBuilder builder = new StringBuilder(getTag()).append('[') + .append(numBytes * 8).append("]={ "); + + boolean addComma = false; + for (int i = 0; i < numBytes; i++) { + byte currentByte = mServiceTable[i]; + for (int bit = 0; bit < 8; bit++) { + if ((currentByte & (1 << bit)) != 0) { + if (addComma) { + builder.append(", "); + } else { + addComma = true; + } + int ordinal = (i * 8) + bit; + if (ordinal < values.length) { + builder.append(values[ordinal]); + } else { + builder.append('#').append(ordinal + 1); // service number (one-based) + } + } + } + } + return builder.append(" }").toString(); + } +} diff --git a/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java b/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java new file mode 100644 index 0000000..5fef6de --- /dev/null +++ b/src/java/com/android/internal/telephony/IccSmsInterfaceManager.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.app.PendingIntent; +import android.content.Context; +import android.util.Log; + +import com.android.internal.util.HexDump; + +import java.util.ArrayList; +import java.util.List; + +import static android.telephony.SmsManager.STATUS_ON_ICC_FREE; + +/** + * IccSmsInterfaceManager to provide an inter-process communication to + * access Sms in Icc. + */ +public abstract class IccSmsInterfaceManager extends ISms.Stub { + protected PhoneBase mPhone; + protected Context mContext; + protected SMSDispatcher mDispatcher; + + protected IccSmsInterfaceManager(PhoneBase phone){ + mPhone = phone; + mContext = phone.getContext(); + } + + protected void enforceReceiveAndSend(String message) { + mContext.enforceCallingPermission( + "android.permission.RECEIVE_SMS", message); + mContext.enforceCallingPermission( + "android.permission.SEND_SMS", message); + } + + /** + * Send a data based SMS to a specific application port. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param destPort the port to deliver the message to + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + public void sendData(String destAddr, String scAddr, int destPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + mPhone.getContext().enforceCallingPermission( + "android.permission.SEND_SMS", + "Sending SMS message"); + if (Log.isLoggable("SMS", Log.VERBOSE)) { + log("sendData: destAddr=" + destAddr + " scAddr=" + scAddr + " destPort=" + + destPort + " data='"+ HexDump.toHexString(data) + "' sentIntent=" + + sentIntent + " deliveryIntent=" + deliveryIntent); + } + mDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent, deliveryIntent); + } + + /** + * Send a text based SMS. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + public void sendText(String destAddr, String scAddr, + String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { + mPhone.getContext().enforceCallingPermission( + "android.permission.SEND_SMS", + "Sending SMS message"); + if (Log.isLoggable("SMS", Log.VERBOSE)) { + log("sendText: destAddr=" + destAddr + " scAddr=" + scAddr + + " text='"+ text + "' sentIntent=" + + sentIntent + " deliveryIntent=" + deliveryIntent); + } + mDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent); + } + + /** + * Send a multi-part text based SMS. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + */ + public void sendMultipartText(String destAddr, String scAddr, List<String> parts, + List<PendingIntent> sentIntents, List<PendingIntent> deliveryIntents) { + mPhone.getContext().enforceCallingPermission( + "android.permission.SEND_SMS", + "Sending SMS message"); + if (Log.isLoggable("SMS", Log.VERBOSE)) { + int i = 0; + for (String part : parts) { + log("sendMultipartText: destAddr=" + destAddr + ", srAddr=" + scAddr + + ", part[" + (i++) + "]=" + part); + } + } + mDispatcher.sendMultipartText(destAddr, scAddr, (ArrayList<String>) parts, + (ArrayList<PendingIntent>) sentIntents, (ArrayList<PendingIntent>) deliveryIntents); + } + + /** + * create SmsRawData lists from all sms record byte[] + * Use null to indicate "free" record + * + * @param messages List of message records from EF_SMS. + * @return SmsRawData list of all in-used records + */ + protected ArrayList<SmsRawData> buildValidRawData(ArrayList<byte[]> messages) { + int count = messages.size(); + ArrayList<SmsRawData> ret; + + ret = new ArrayList<SmsRawData>(count); + + for (int i = 0; i < count; i++) { + byte[] ba = messages.get(i); + if (ba[0] == STATUS_ON_ICC_FREE) { + ret.add(null); + } else { + ret.add(new SmsRawData(messages.get(i))); + } + } + + return ret; + } + + /** + * Generates an EF_SMS record from status and raw PDU. + * + * @param status Message status. See TS 51.011 10.5.3. + * @param pdu Raw message PDU. + * @return byte array for the record. + */ + protected byte[] makeSmsRecordData(int status, byte[] pdu) { + byte[] data = new byte[IccConstants.SMS_RECORD_LENGTH]; + + // Status bits for this record. See TS 51.011 10.5.3 + data[0] = (byte)(status & 7); + + System.arraycopy(pdu, 0, data, 1, pdu.length); + + // Pad out with 0xFF's. + for (int j = pdu.length+1; j < IccConstants.SMS_RECORD_LENGTH; j++) { + data[j] = -1; + } + + return data; + } + + protected abstract void log(String msg); + +} diff --git a/src/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java b/src/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java new file mode 100644 index 0000000..54de508 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.app.PendingIntent; +import android.os.ServiceManager; + +import java.util.List; + +public class IccSmsInterfaceManagerProxy extends ISms.Stub { + private IccSmsInterfaceManager mIccSmsInterfaceManager; + + public IccSmsInterfaceManagerProxy(IccSmsInterfaceManager + iccSmsInterfaceManager) { + this.mIccSmsInterfaceManager = iccSmsInterfaceManager; + if(ServiceManager.getService("isms") == null) { + ServiceManager.addService("isms", this); + } + } + + public void setmIccSmsInterfaceManager(IccSmsInterfaceManager iccSmsInterfaceManager) { + this.mIccSmsInterfaceManager = iccSmsInterfaceManager; + } + + public boolean + updateMessageOnIccEf(int index, int status, byte[] pdu) throws android.os.RemoteException { + return mIccSmsInterfaceManager.updateMessageOnIccEf(index, status, pdu); + } + + public boolean copyMessageToIccEf(int status, byte[] pdu, + byte[] smsc) throws android.os.RemoteException { + return mIccSmsInterfaceManager.copyMessageToIccEf(status, pdu, smsc); + } + + public List<SmsRawData> getAllMessagesFromIccEf() throws android.os.RemoteException { + return mIccSmsInterfaceManager.getAllMessagesFromIccEf(); + } + + public void sendData(String destAddr, String scAddr, int destPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + mIccSmsInterfaceManager.sendData(destAddr, scAddr, destPort, data, + sentIntent, deliveryIntent); + } + + public void sendText(String destAddr, String scAddr, + String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { + mIccSmsInterfaceManager.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent); + } + + public void sendMultipartText(String destAddr, String scAddr, + List<String> parts, List<PendingIntent> sentIntents, + List<PendingIntent> deliveryIntents) throws android.os.RemoteException { + mIccSmsInterfaceManager.sendMultipartText(destAddr, scAddr, + parts, sentIntents, deliveryIntents); + } + + public boolean enableCellBroadcast(int messageIdentifier) throws android.os.RemoteException { + return mIccSmsInterfaceManager.enableCellBroadcast(messageIdentifier); + } + + public boolean disableCellBroadcast(int messageIdentifier) throws android.os.RemoteException { + return mIccSmsInterfaceManager.disableCellBroadcast(messageIdentifier); + } + + public boolean enableCellBroadcastRange(int startMessageId, int endMessageId) + throws android.os.RemoteException { + return mIccSmsInterfaceManager.enableCellBroadcastRange(startMessageId, endMessageId); + } + + public boolean disableCellBroadcastRange(int startMessageId, int endMessageId) + throws android.os.RemoteException { + return mIccSmsInterfaceManager.disableCellBroadcastRange(startMessageId, endMessageId); + } +} diff --git a/src/java/com/android/internal/telephony/IccUtils.java b/src/java/com/android/internal/telephony/IccUtils.java new file mode 100644 index 0000000..a966f76 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccUtils.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Various methods, useful for dealing with SIM data. + */ +public class IccUtils { + static final String LOG_TAG="IccUtils"; + + /** + * Many fields in GSM SIM's are stored as nibble-swizzled BCD + * + * Assumes left-justified field that may be padded right with 0xf + * values. + * + * Stops on invalid BCD value, returning string so far + */ + public static String + bcdToString(byte[] data, int offset, int length) { + StringBuilder ret = new StringBuilder(length*2); + + for (int i = offset ; i < offset + length ; i++) { + byte b; + int v; + + v = data[i] & 0xf; + if (v > 9) break; + ret.append((char)('0' + v)); + + v = (data[i] >> 4) & 0xf; + // Some PLMNs have 'f' as high nibble, ignore it + if (v == 0xf) continue; + if (v > 9) break; + ret.append((char)('0' + v)); + } + + return ret.toString(); + } + + /** + * Decode cdma byte into String. + */ + public static String + cdmaBcdToString(byte[] data, int offset, int length) { + StringBuilder ret = new StringBuilder(length); + + int count = 0; + for (int i = offset; count < length; i++) { + int v; + v = data[i] & 0xf; + if (v > 9) v = 0; + ret.append((char)('0' + v)); + + if (++count == length) break; + + v = (data[i] >> 4) & 0xf; + if (v > 9) v = 0; + ret.append((char)('0' + v)); + ++count; + } + return ret.toString(); + } + + /** + * Decodes a GSM-style BCD byte, returning an int ranging from 0-99. + * + * In GSM land, the least significant BCD digit is stored in the most + * significant nibble. + * + * Out-of-range digits are treated as 0 for the sake of the time stamp, + * because of this: + * + * TS 23.040 section 9.2.3.11 + * "if the MS receives a non-integer value in the SCTS, it shall + * assume the digit is set to 0 but shall store the entire field + * exactly as received" + */ + public static int + gsmBcdByteToInt(byte b) { + int ret = 0; + + // treat out-of-range BCD values as 0 + if ((b & 0xf0) <= 0x90) { + ret = (b >> 4) & 0xf; + } + + if ((b & 0x0f) <= 0x09) { + ret += (b & 0xf) * 10; + } + + return ret; + } + + /** + * Decodes a CDMA style BCD byte like {@link gsmBcdByteToInt}, but + * opposite nibble format. The least significant BCD digit + * is in the least significant nibble and the most significant + * is in the most significant nibble. + */ + public static int + cdmaBcdByteToInt(byte b) { + int ret = 0; + + // treat out-of-range BCD values as 0 + if ((b & 0xf0) <= 0x90) { + ret = ((b >> 4) & 0xf) * 10; + } + + if ((b & 0x0f) <= 0x09) { + ret += (b & 0xf); + } + + return ret; + } + + /** + * Decodes a string field that's formatted like the EF[ADN] alpha + * identifier + * + * From TS 51.011 10.5.1: + * Coding: + * this alpha tagging shall use either + * - the SMS default 7 bit coded alphabet as defined in + * TS 23.038 [12] with bit 8 set to 0. The alpha identifier + * shall be left justified. Unused bytes shall be set to 'FF'; or + * - one of the UCS2 coded options as defined in annex B. + * + * Annex B from TS 11.11 V8.13.0: + * 1) If the first octet in the alpha string is '80', then the + * remaining octets are 16 bit UCS2 characters ... + * 2) if the first octet in the alpha string is '81', then the + * second octet contains a value indicating the number of + * characters in the string, and the third octet contains an + * 8 bit number which defines bits 15 to 8 of a 16 bit + * base pointer, where bit 16 is set to zero and bits 7 to 1 + * are also set to zero. These sixteen bits constitute a + * base pointer to a "half page" in the UCS2 code space, to be + * used with some or all of the remaining octets in the string. + * The fourth and subsequent octets contain codings as follows: + * If bit 8 of the octet is set to zero, the remaining 7 bits + * of the octet contain a GSM Default Alphabet character, + * whereas if bit 8 of the octet is set to one, then the + * remaining seven bits are an offset value added to the + * 16 bit base pointer defined earlier... + * 3) If the first octet of the alpha string is set to '82', then + * the second octet contains a value indicating the number of + * characters in the string, and the third and fourth octets + * contain a 16 bit number which defines the complete 16 bit + * base pointer to a "half page" in the UCS2 code space... + */ + public static String + adnStringFieldToString(byte[] data, int offset, int length) { + if (length == 0) { + return ""; + } + if (length >= 1) { + if (data[offset] == (byte) 0x80) { + int ucslen = (length - 1) / 2; + String ret = null; + + try { + ret = new String(data, offset + 1, ucslen * 2, "utf-16be"); + } catch (UnsupportedEncodingException ex) { + Log.e(LOG_TAG, "implausible UnsupportedEncodingException", + ex); + } + + if (ret != null) { + // trim off trailing FFFF characters + + ucslen = ret.length(); + while (ucslen > 0 && ret.charAt(ucslen - 1) == '\uFFFF') + ucslen--; + + return ret.substring(0, ucslen); + } + } + } + + boolean isucs2 = false; + char base = '\0'; + int len = 0; + + if (length >= 3 && data[offset] == (byte) 0x81) { + len = data[offset + 1] & 0xFF; + if (len > length - 3) + len = length - 3; + + base = (char) ((data[offset + 2] & 0xFF) << 7); + offset += 3; + isucs2 = true; + } else if (length >= 4 && data[offset] == (byte) 0x82) { + len = data[offset + 1] & 0xFF; + if (len > length - 4) + len = length - 4; + + base = (char) (((data[offset + 2] & 0xFF) << 8) | + (data[offset + 3] & 0xFF)); + offset += 4; + isucs2 = true; + } + + if (isucs2) { + StringBuilder ret = new StringBuilder(); + + while (len > 0) { + // UCS2 subset case + + if (data[offset] < 0) { + ret.append((char) (base + (data[offset] & 0x7F))); + offset++; + len--; + } + + // GSM character set case + + int count = 0; + while (count < len && data[offset + count] >= 0) + count++; + + ret.append(GsmAlphabet.gsm8BitUnpackedToString(data, + offset, count)); + + offset += count; + len -= count; + } + + return ret.toString(); + } + + Resources resource = Resources.getSystem(); + String defaultCharset = ""; + try { + defaultCharset = + resource.getString(com.android.internal.R.string.gsm_alphabet_default_charset); + } catch (NotFoundException e) { + // Ignore Exception and defaultCharset is set to a empty string. + } + return GsmAlphabet.gsm8BitUnpackedToString(data, offset, length, defaultCharset.trim()); + } + + static int + hexCharToInt(char c) { + if (c >= '0' && c <= '9') return (c - '0'); + if (c >= 'A' && c <= 'F') return (c - 'A' + 10); + if (c >= 'a' && c <= 'f') return (c - 'a' + 10); + + throw new RuntimeException ("invalid hex char '" + c + "'"); + } + + /** + * Converts a hex String to a byte array. + * + * @param s A string of hexadecimal characters, must be an even number of + * chars long + * + * @return byte array representation + * + * @throws RuntimeException on invalid format + */ + public static byte[] + hexStringToBytes(String s) { + byte[] ret; + + if (s == null) return null; + + int sz = s.length(); + + ret = new byte[sz/2]; + + for (int i=0 ; i <sz ; i+=2) { + ret[i/2] = (byte) ((hexCharToInt(s.charAt(i)) << 4) + | hexCharToInt(s.charAt(i+1))); + } + + return ret; + } + + + /** + * Converts a byte array into a String of hexadecimal characters. + * + * @param bytes an array of bytes + * + * @return hex string representation of bytes array + */ + public static String + bytesToHexString(byte[] bytes) { + if (bytes == null) return null; + + StringBuilder ret = new StringBuilder(2*bytes.length); + + for (int i = 0 ; i < bytes.length ; i++) { + int b; + + b = 0x0f & (bytes[i] >> 4); + + ret.append("0123456789abcdef".charAt(b)); + + b = 0x0f & bytes[i]; + + ret.append("0123456789abcdef".charAt(b)); + } + + return ret.toString(); + } + + + /** + * Convert a TS 24.008 Section 10.5.3.5a Network Name field to a string + * "offset" points to "octet 3", the coding scheme byte + * empty string returned on decode error + */ + public static String + networkNameToString(byte[] data, int offset, int length) { + String ret; + + if ((data[offset] & 0x80) != 0x80 || length < 1) { + return ""; + } + + switch ((data[offset] >>> 4) & 0x7) { + case 0: + // SMS character set + int countSeptets; + int unusedBits = data[offset] & 7; + countSeptets = (((length - 1) * 8) - unusedBits) / 7 ; + ret = GsmAlphabet.gsm7BitPackedToString(data, offset + 1, countSeptets); + break; + case 1: + // UCS2 + try { + ret = new String(data, + offset + 1, length - 1, "utf-16"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Log.e(LOG_TAG,"implausible UnsupportedEncodingException", ex); + } + break; + + // unsupported encoding + default: + ret = ""; + break; + } + + // "Add CI" + // "The MS should add the letters for the Country's Initials and + // a separator (e.g. a space) to the text string" + + if ((data[offset] & 0x40) != 0) { + // FIXME(mkf) add country initials here + + } + + return ret; + } + + /** + * Convert a TS 131.102 image instance of code scheme '11' into Bitmap + * @param data The raw data + * @param length The length of image body + * @return The bitmap + */ + public static Bitmap parseToBnW(byte[] data, int length){ + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int numOfPixels = width*height; + + int[] pixels = new int[numOfPixels]; + + int pixelIndex = 0; + int bitIndex = 7; + byte currentByte = 0x00; + while (pixelIndex < numOfPixels) { + // reassign data and index for every byte (8 bits). + if (pixelIndex % 8 == 0) { + currentByte = data[valueIndex++]; + bitIndex = 7; + } + pixels[pixelIndex++] = bitToRGB((currentByte >> bitIndex-- ) & 0x01); + }; + + if (pixelIndex != numOfPixels) { + Log.e(LOG_TAG, "parse end and size error"); + } + return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); + } + + private static int bitToRGB(int bit){ + if(bit == 1){ + return Color.WHITE; + } else { + return Color.BLACK; + } + } + + /** + * a TS 131.102 image instance of code scheme '11' into color Bitmap + * + * @param data The raw data + * @param length the length of image body + * @param transparency with or without transparency + * @return The color bitmap + */ + public static Bitmap parseToRGB(byte[] data, int length, + boolean transparency) { + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int bits = data[valueIndex++] & 0xFF; + int colorNumber = data[valueIndex++] & 0xFF; + int clutOffset = ((data[valueIndex++] & 0xFF) << 8) + | (data[valueIndex++] & 0xFF); + + int[] colorIndexArray = getCLUT(data, clutOffset, colorNumber); + if (true == transparency) { + colorIndexArray[colorNumber - 1] = Color.TRANSPARENT; + } + + int[] resultArray = null; + if (0 == (8 % bits)) { + resultArray = mapTo2OrderBitColor(data, valueIndex, + (width * height), colorIndexArray, bits); + } else { + resultArray = mapToNon2OrderBitColor(data, valueIndex, + (width * height), colorIndexArray, bits); + } + + return Bitmap.createBitmap(resultArray, width, height, + Bitmap.Config.RGB_565); + } + + private static int[] mapTo2OrderBitColor(byte[] data, int valueIndex, + int length, int[] colorArray, int bits) { + if (0 != (8 % bits)) { + Log.e(LOG_TAG, "not event number of color"); + return mapToNon2OrderBitColor(data, valueIndex, length, colorArray, + bits); + } + + int mask = 0x01; + switch (bits) { + case 1: + mask = 0x01; + break; + case 2: + mask = 0x03; + break; + case 4: + mask = 0x0F; + break; + case 8: + mask = 0xFF; + break; + } + + int[] resultArray = new int[length]; + int resultIndex = 0; + int run = 8 / bits; + while (resultIndex < length) { + byte tempByte = data[valueIndex++]; + for (int runIndex = 0; runIndex < run; ++runIndex) { + int offset = run - runIndex - 1; + resultArray[resultIndex++] = colorArray[(tempByte >> (offset * bits)) + & mask]; + } + } + return resultArray; + } + + private static int[] mapToNon2OrderBitColor(byte[] data, int valueIndex, + int length, int[] colorArray, int bits) { + if (0 == (8 % bits)) { + Log.e(LOG_TAG, "not odd number of color"); + return mapTo2OrderBitColor(data, valueIndex, length, colorArray, + bits); + } + + int[] resultArray = new int[length]; + // TODO fix me: + return resultArray; + } + + private static int[] getCLUT(byte[] rawData, int offset, int number) { + if (null == rawData) { + return null; + } + + int[] result = new int[number]; + int endIndex = offset + (number * 3); // 1 color use 3 bytes + int valueIndex = offset; + int colorIndex = 0; + int alpha = 0xff << 24; + do { + result[colorIndex++] = alpha + | ((rawData[valueIndex++] & 0xFF) << 16) + | ((rawData[valueIndex++] & 0xFF) << 8) + | ((rawData[valueIndex++] & 0xFF)); + } while (valueIndex < endIndex); + return result; + } +} diff --git a/src/java/com/android/internal/telephony/IccVmFixedException.java b/src/java/com/android/internal/telephony/IccVmFixedException.java new file mode 100644 index 0000000..a75496f --- /dev/null +++ b/src/java/com/android/internal/telephony/IccVmFixedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +/** + * {@hide} + */ +public final class IccVmFixedException extends IccException { + IccVmFixedException() + { + + } + + public IccVmFixedException(String s) + { + super(s); + } +}
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/IccVmNotSupportedException.java b/src/java/com/android/internal/telephony/IccVmNotSupportedException.java new file mode 100644 index 0000000..3c9d126 --- /dev/null +++ b/src/java/com/android/internal/telephony/IccVmNotSupportedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +/** + * {@hide} + */ +public final class IccVmNotSupportedException extends IccException { + IccVmNotSupportedException() + { + + } + + public IccVmNotSupportedException(String s) + { + super(s); + } +} diff --git a/src/java/com/android/internal/telephony/IntRangeManager.java b/src/java/com/android/internal/telephony/IntRangeManager.java new file mode 100644 index 0000000..cc7774d --- /dev/null +++ b/src/java/com/android/internal/telephony/IntRangeManager.java @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Clients can enable reception of SMS-CB messages for specific ranges of + * message identifiers (channels). This class keeps track of the currently + * enabled message identifiers and calls abstract methods to update the + * radio when the range of enabled message identifiers changes. + * + * An update is a call to {@link #startUpdate} followed by zero or more + * calls to {@link #addRange} followed by a call to {@link #finishUpdate}. + * Calls to {@link #enableRange} and {@link #disableRange} will perform + * an incremental update operation if the enabled ranges have changed. + * A full update operation (i.e. after a radio reset) can be performed + * by a call to {@link #updateRanges}. + * + * Clients are identified by String (the name associated with the User ID + * of the caller) so that a call to remove a range can be mapped to the + * client that enabled that range (or else rejected). + */ +public abstract class IntRangeManager { + + /** + * Initial capacity for IntRange clients array list. There will be + * few cell broadcast listeners on a typical device, so this can be small. + */ + private static final int INITIAL_CLIENTS_ARRAY_SIZE = 4; + + /** + * One or more clients forming the continuous range [startId, endId]. + * <p>When a client is added, the IntRange may merge with one or more + * adjacent IntRanges to form a single combined IntRange. + * <p>When a client is removed, the IntRange may divide into several + * non-contiguous IntRanges. + */ + private class IntRange { + int startId; + int endId; + // sorted by earliest start id + final ArrayList<ClientRange> clients; + + /** + * Create a new IntRange with a single client. + * @param startId the first id included in the range + * @param endId the last id included in the range + * @param client the client requesting the enabled range + */ + IntRange(int startId, int endId, String client) { + this.startId = startId; + this.endId = endId; + clients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE); + clients.add(new ClientRange(startId, endId, client)); + } + + /** + * Create a new IntRange for an existing ClientRange. + * @param clientRange the initial ClientRange to add + */ + IntRange(ClientRange clientRange) { + startId = clientRange.startId; + endId = clientRange.endId; + clients = new ArrayList<ClientRange>(INITIAL_CLIENTS_ARRAY_SIZE); + clients.add(clientRange); + } + + /** + * Create a new IntRange from an existing IntRange. This is used for + * removing a ClientRange, because new IntRanges may need to be created + * for any gaps that open up after the ClientRange is removed. A copy + * is made of the elements of the original IntRange preceding the element + * that is being removed. The following elements will be added to this + * IntRange or to a new IntRange when a gap is found. + * @param intRange the original IntRange to copy elements from + * @param numElements the number of elements to copy from the original + */ + IntRange(IntRange intRange, int numElements) { + this.startId = intRange.startId; + this.endId = intRange.endId; + this.clients = new ArrayList<ClientRange>(intRange.clients.size()); + for (int i=0; i < numElements; i++) { + this.clients.add(intRange.clients.get(i)); + } + } + + /** + * Insert new ClientRange in order by start id. + * <p>If the new ClientRange is known to be sorted before or after the + * existing ClientRanges, or at a particular index, it can be added + * to the clients array list directly, instead of via this method. + * <p>Note that this can be changed from linear to binary search if the + * number of clients grows large enough that it would make a difference. + * @param range the new ClientRange to insert + */ + void insert(ClientRange range) { + int len = clients.size(); + for (int i=0; i < len; i++) { + ClientRange nextRange = clients.get(i); + if (range.startId <= nextRange.startId) { + // ignore duplicate ranges from the same client + if (!range.equals(nextRange)) { + clients.add(i, range); + } + return; + } + } + clients.add(range); // append to end of list + } + } + + /** + * The message id range for a single client. + */ + private class ClientRange { + final int startId; + final int endId; + final String client; + + ClientRange(int startId, int endId, String client) { + this.startId = startId; + this.endId = endId; + this.client = client; + } + + @Override + public boolean equals(Object o) { + if (o != null && o instanceof ClientRange) { + ClientRange other = (ClientRange) o; + return startId == other.startId && + endId == other.endId && + client.equals(other.client); + } else { + return false; + } + } + + @Override + public int hashCode() { + return (startId * 31 + endId) * 31 + client.hashCode(); + } + } + + /** + * List of integer ranges, one per client, sorted by start id. + */ + private ArrayList<IntRange> mRanges = new ArrayList<IntRange>(); + + protected IntRangeManager() {} + + /** + * Enable a range for the specified client and update ranges + * if necessary. If {@link #finishUpdate} returns failure, + * false is returned and the range is not added. + * + * @param startId the first id included in the range + * @param endId the last id included in the range + * @param client the client requesting the enabled range + * @return true if successful, false otherwise + */ + public synchronized boolean enableRange(int startId, int endId, String client) { + int len = mRanges.size(); + + // empty range list: add the initial IntRange + if (len == 0) { + if (tryAddSingleRange(startId, endId, true)) { + mRanges.add(new IntRange(startId, endId, client)); + return true; + } else { + return false; // failed to update radio + } + } + + for (int startIndex = 0; startIndex < len; startIndex++) { + IntRange range = mRanges.get(startIndex); + if (startId < range.startId) { + // test if new range completely precedes this range + // note that [1, 4] and [5, 6] coalesce to [1, 6] + if ((endId + 1) < range.startId) { + // insert new int range before previous first range + if (tryAddSingleRange(startId, endId, true)) { + mRanges.add(startIndex, new IntRange(startId, endId, client)); + return true; + } else { + return false; // failed to update radio + } + } else if (endId <= range.endId) { + // extend the start of this range + if (tryAddSingleRange(startId, range.startId - 1, true)) { + range.startId = startId; + range.clients.add(0, new ClientRange(startId, endId, client)); + return true; + } else { + return false; // failed to update radio + } + } else { + // find last range that can coalesce into the new combined range + for (int endIndex = startIndex+1; endIndex < len; endIndex++) { + IntRange endRange = mRanges.get(endIndex); + if ((endId + 1) < endRange.startId) { + // try to add entire new range + if (tryAddSingleRange(startId, endId, true)) { + range.startId = startId; + range.endId = endId; + // insert new ClientRange before existing ranges + range.clients.add(0, new ClientRange(startId, endId, client)); + // coalesce range with following ranges up to endIndex-1 + // remove each range after adding its elements, so the index + // of the next range to join is always startIndex+1. + // i is the index if no elements were removed: we only care + // about the number of loop iterations, not the value of i. + int joinIndex = startIndex + 1; + for (int i = joinIndex; i < endIndex; i++) { + IntRange joinRange = mRanges.get(joinIndex); + range.clients.addAll(joinRange.clients); + mRanges.remove(joinRange); + } + return true; + } else { + return false; // failed to update radio + } + } else if (endId <= endRange.endId) { + // add range from start id to start of last overlapping range, + // values from endRange.startId to endId are already enabled + if (tryAddSingleRange(startId, endRange.startId - 1, true)) { + range.startId = startId; + range.endId = endRange.endId; + // insert new ClientRange before existing ranges + range.clients.add(0, new ClientRange(startId, endId, client)); + // coalesce range with following ranges up to endIndex + // remove each range after adding its elements, so the index + // of the next range to join is always startIndex+1. + // i is the index if no elements were removed: we only care + // about the number of loop iterations, not the value of i. + int joinIndex = startIndex + 1; + for (int i = joinIndex; i <= endIndex; i++) { + IntRange joinRange = mRanges.get(joinIndex); + range.clients.addAll(joinRange.clients); + mRanges.remove(joinRange); + } + return true; + } else { + return false; // failed to update radio + } + } + } + + // endId extends past all existing IntRanges: combine them all together + if (tryAddSingleRange(startId, endId, true)) { + range.startId = startId; + range.endId = endId; + // insert new ClientRange before existing ranges + range.clients.add(0, new ClientRange(startId, endId, client)); + // coalesce range with following ranges up to len-1 + // remove each range after adding its elements, so the index + // of the next range to join is always startIndex+1. + // i is the index if no elements were removed: we only care + // about the number of loop iterations, not the value of i. + int joinIndex = startIndex + 1; + for (int i = joinIndex; i < len; i++) { + IntRange joinRange = mRanges.get(joinIndex); + range.clients.addAll(joinRange.clients); + mRanges.remove(joinRange); + } + return true; + } else { + return false; // failed to update radio + } + } + } else if ((startId + 1) <= range.endId) { + if (endId <= range.endId) { + // completely contained in existing range; no radio changes + range.insert(new ClientRange(startId, endId, client)); + return true; + } else { + // find last range that can coalesce into the new combined range + int endIndex = startIndex; + for (int testIndex = startIndex+1; testIndex < len; testIndex++) { + IntRange testRange = mRanges.get(testIndex); + if ((endId + 1) < testRange.startId) { + break; + } else { + endIndex = testIndex; + } + } + // no adjacent IntRanges to combine + if (endIndex == startIndex) { + // add range from range.endId+1 to endId, + // values from startId to range.endId are already enabled + if (tryAddSingleRange(range.endId + 1, endId, true)) { + range.endId = endId; + range.insert(new ClientRange(startId, endId, client)); + return true; + } else { + return false; // failed to update radio + } + } + // get last range to coalesce into start range + IntRange endRange = mRanges.get(endIndex); + // Values from startId to range.endId have already been enabled. + // if endId > endRange.endId, then enable range from range.endId+1 to endId, + // else enable range from range.endId+1 to endRange.startId-1, because + // values from endRange.startId to endId have already been added. + int newRangeEndId = (endId <= endRange.endId) ? endRange.startId - 1 : endId; + if (tryAddSingleRange(range.endId + 1, newRangeEndId, true)) { + range.endId = endId; + // insert new ClientRange in place + range.insert(new ClientRange(startId, endId, client)); + // coalesce range with following ranges up to endIndex-1 + // remove each range after adding its elements, so the index + // of the next range to join is always startIndex+1 (joinIndex). + // i is the index if no elements had been removed: we only care + // about the number of loop iterations, not the value of i. + int joinIndex = startIndex + 1; + for (int i = joinIndex; i < endIndex; i++) { + IntRange joinRange = mRanges.get(joinIndex); + range.clients.addAll(joinRange.clients); + mRanges.remove(joinRange); + } + return true; + } else { + return false; // failed to update radio + } + } + } + } + + // append new range after existing IntRanges + if (tryAddSingleRange(startId, endId, true)) { + mRanges.add(new IntRange(startId, endId, client)); + return true; + } else { + return false; // failed to update radio + } + } + + /** + * Disable a range for the specified client and update ranges + * if necessary. If {@link #finishUpdate} returns failure, + * false is returned and the range is not removed. + * + * @param startId the first id included in the range + * @param endId the last id included in the range + * @param client the client requesting to disable the range + * @return true if successful, false otherwise + */ + public synchronized boolean disableRange(int startId, int endId, String client) { + int len = mRanges.size(); + + for (int i=0; i < len; i++) { + IntRange range = mRanges.get(i); + if (startId < range.startId) { + return false; // not found + } else if (endId <= range.endId) { + // found the IntRange that encloses the client range, if any + // search for it in the clients list + ArrayList<ClientRange> clients = range.clients; + + // handle common case of IntRange containing one ClientRange + int crLength = clients.size(); + if (crLength == 1) { + ClientRange cr = clients.get(0); + if (cr.startId == startId && cr.endId == endId && cr.client.equals(client)) { + // disable range in radio then remove the entire IntRange + if (tryAddSingleRange(startId, endId, false)) { + mRanges.remove(i); + return true; + } else { + return false; // failed to update radio + } + } else { + return false; // not found + } + } + + // several ClientRanges: remove one, potentially splitting into many IntRanges. + // Save the original start and end id for the original IntRange + // in case the radio update fails and we have to revert it. If the + // update succeeds, we remove the client range and insert the new IntRanges. + int largestEndId = Integer.MIN_VALUE; // largest end identifier found + boolean updateStarted = false; + + for (int crIndex=0; crIndex < crLength; crIndex++) { + ClientRange cr = clients.get(crIndex); + if (cr.startId == startId && cr.endId == endId && cr.client.equals(client)) { + // found the ClientRange to remove, check if it's the last in the list + if (crIndex == crLength - 1) { + if (range.endId == largestEndId) { + // no channels to remove from radio; return success + clients.remove(crIndex); + return true; + } else { + // disable the channels at the end and lower the end id + if (tryAddSingleRange(largestEndId + 1, range.endId, false)) { + clients.remove(crIndex); + range.endId = largestEndId; + return true; + } else { + return false; + } + } + } + + // copy the IntRange so that we can remove elements and modify the + // start and end id's in the copy, leaving the original unmodified + // until after the radio update succeeds + IntRange rangeCopy = new IntRange(range, crIndex); + + if (crIndex == 0) { + // removing the first ClientRange, so we may need to increase + // the start id of the IntRange. + // We know there are at least two ClientRanges in the list, + // so clients.get(1) should always succeed. + int nextStartId = clients.get(1).startId; + if (nextStartId != range.startId) { + startUpdate(); + updateStarted = true; + addRange(range.startId, nextStartId - 1, false); + rangeCopy.startId = nextStartId; + } + // init largestEndId + largestEndId = clients.get(1).endId; + } + + // go through remaining ClientRanges, creating new IntRanges when + // there is a gap in the sequence. After radio update succeeds, + // remove the original IntRange and append newRanges to mRanges. + // Otherwise, leave the original IntRange in mRanges and return false. + ArrayList<IntRange> newRanges = new ArrayList<IntRange>(); + + IntRange currentRange = rangeCopy; + for (int nextIndex = crIndex + 1; nextIndex < crLength; nextIndex++) { + ClientRange nextCr = clients.get(nextIndex); + if (nextCr.startId > largestEndId + 1) { + if (!updateStarted) { + startUpdate(); + updateStarted = true; + } + addRange(largestEndId + 1, nextCr.startId - 1, false); + currentRange.endId = largestEndId; + newRanges.add(currentRange); + currentRange = new IntRange(nextCr); + } else { + currentRange.clients.add(nextCr); + } + if (nextCr.endId > largestEndId) { + largestEndId = nextCr.endId; + } + } + + // remove any channels between largestEndId and endId + if (largestEndId < endId) { + if (!updateStarted) { + startUpdate(); + updateStarted = true; + } + addRange(largestEndId + 1, endId, false); + currentRange.endId = largestEndId; + } + newRanges.add(currentRange); + + if (updateStarted && !finishUpdate()) { + return false; // failed to update radio + } + + // replace the original IntRange with newRanges + mRanges.remove(i); + mRanges.addAll(i, newRanges); + return true; + } else { + // not the ClientRange to remove; save highest end ID seen so far + if (cr.endId > largestEndId) { + largestEndId = cr.endId; + } + } + } + } + } + + return false; // not found + } + + /** + * Perform a complete update operation (enable all ranges). Useful + * after a radio reset. Calls {@link #startUpdate}, followed by zero or + * more calls to {@link #addRange}, followed by {@link #finishUpdate}. + * @return true if successful, false otherwise + */ + public boolean updateRanges() { + startUpdate(); + Iterator<IntRange> iterator = mRanges.iterator(); + if (iterator.hasNext()) { + IntRange range = iterator.next(); + int start = range.startId; + int end = range.endId; + // accumulate ranges of [startId, endId] + while (iterator.hasNext()) { + IntRange nextNode = iterator.next(); + // [startIdA, endIdA], [endIdA + 1, endIdB] -> [startIdA, endIdB] + if (nextNode.startId <= (end + 1)) { + if (nextNode.endId > end) { + end = nextNode.endId; + } + } else { + addRange(start, end, true); + start = nextNode.startId; + end = nextNode.endId; + } + } + // add final range + addRange(start, end, true); + } + return finishUpdate(); + } + + /** + * Enable or disable a single range of message identifiers. + * @param startId the first id included in the range + * @param endId the last id included in the range + * @param selected true to enable range, false to disable range + * @return true if successful, false otherwise + */ + private boolean tryAddSingleRange(int startId, int endId, boolean selected) { + startUpdate(); + addRange(startId, endId, selected); + return finishUpdate(); + } + + /** + * Returns whether the list of ranges is completely empty. + * @return true if there are no enabled ranges + */ + public boolean isEmpty() { + return mRanges.isEmpty(); + } + + /** + * Called when the list of enabled ranges has changed. This will be + * followed by zero or more calls to {@link #addRange} followed by + * a call to {@link #finishUpdate}. + */ + protected abstract void startUpdate(); + + /** + * Called after {@link #startUpdate} to indicate a range of enabled + * or disabled values. + * + * @param startId the first id included in the range + * @param endId the last id included in the range + * @param selected true to enable range, false to disable range + */ + protected abstract void addRange(int startId, int endId, boolean selected); + + /** + * Called to indicate the end of a range update started by the + * previous call to {@link #startUpdate}. + * @return true if successful, false otherwise + */ + protected abstract boolean finishUpdate(); +} diff --git a/src/java/com/android/internal/telephony/MccTable.java b/src/java/com/android/internal/telephony/MccTable.java new file mode 100644 index 0000000..cb33521 --- /dev/null +++ b/src/java/com/android/internal/telephony/MccTable.java @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.app.ActivityManagerNative; +import android.app.AlarmManager; +import android.app.IActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.net.wifi.WifiManager; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Locale; +import libcore.icu.TimeZones; + +/** + * Mobile Country Code + * + * {@hide} + */ +public final class MccTable +{ + static final String LOG_TAG = "MccTable"; + + static ArrayList<MccEntry> table; + + static class MccEntry implements Comparable<MccEntry> + { + int mcc; + String iso; + int smallestDigitsMnc; + String language; + + MccEntry(int mnc, String iso, int smallestDigitsMCC) { + this(mnc, iso, smallestDigitsMCC, null); + } + + MccEntry(int mnc, String iso, int smallestDigitsMCC, String language) { + this.mcc = mnc; + this.iso = iso; + this.smallestDigitsMnc = smallestDigitsMCC; + this.language = language; + } + + + public int compareTo(MccEntry o) + { + return mcc - o.mcc; + } + } + + private static MccEntry + entryForMcc(int mcc) + { + int index; + + MccEntry m; + + m = new MccEntry(mcc, null, 0); + + index = Collections.binarySearch(table, m); + + if (index < 0) { + return null; + } else { + return table.get(index); + } + } + + /** + * Returns a default time zone ID for the given MCC. + * @param mcc Mobile Country Code + * @return default TimeZone ID, or null if not specified + */ + public static String defaultTimeZoneForMcc(int mcc) { + MccEntry entry; + + entry = entryForMcc(mcc); + if (entry == null || entry.iso == null) { + return null; + } else { + Locale locale; + if (entry.language == null) { + locale = new Locale(entry.iso); + } else { + locale = new Locale(entry.language, entry.iso); + } + String[] tz = TimeZones.forLocale(locale); + if (tz.length == 0) return null; + return tz[0]; + } + } + + /** + * Given a GSM Mobile Country Code, returns + * an ISO two-character country code if available. + * Returns "" if unavailable. + */ + public static String + countryCodeForMcc(int mcc) + { + MccEntry entry; + + entry = entryForMcc(mcc); + + if (entry == null) { + return ""; + } else { + return entry.iso; + } + } + + /** + * Given a GSM Mobile Country Code, returns + * an ISO 2-3 character language code if available. + * Returns null if unavailable. + */ + public static String defaultLanguageForMcc(int mcc) { + MccEntry entry; + + entry = entryForMcc(mcc); + + if (entry == null) { + return null; + } else { + return entry.language; + } + } + + /** + * Given a GSM Mobile Country Code, returns + * the smallest number of digits that M if available. + * Returns 2 if unavailable. + */ + public static int + smallestDigitsMccForMnc(int mcc) + { + MccEntry entry; + + entry = entryForMcc(mcc); + + if (entry == null) { + return 2; + } else { + return entry.smallestDigitsMnc; + } + } + + /** + * Updates MCC and MNC device configuration information for application retrieving + * correct version of resources. If either MCC or MNC is 0, they will be ignored (not set). + * @param context Context to act on. + * @param mccmnc truncated imsi with just the MCC and MNC - MNC assumed to be from 4th to end + */ + public static void updateMccMncConfiguration(Context context, String mccmnc) { + if (!TextUtils.isEmpty(mccmnc)) { + int mcc, mnc; + + try { + mcc = Integer.parseInt(mccmnc.substring(0,3)); + mnc = Integer.parseInt(mccmnc.substring(3)); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, "Error parsing IMSI"); + return; + } + + Log.d(LOG_TAG, "updateMccMncConfiguration: mcc=" + mcc + ", mnc=" + mnc); + + if (mcc != 0) { + setTimezoneFromMccIfNeeded(context, mcc); + setLocaleFromMccIfNeeded(context, mcc); + setWifiCountryCodeFromMcc(context, mcc); + } + try { + Configuration config = ActivityManagerNative.getDefault().getConfiguration(); + if (mcc != 0) { + config.mcc = mcc; + } + if (mnc != 0) { + config.mnc = mnc; + } + ActivityManagerNative.getDefault().updateConfiguration(config); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Can't update configuration", e); + } + } + } + + /** + * Utility code to set the system locale if it's not set already + * @param context Context to act on. + * @param language Two character language code desired + * @param country Two character country code desired + * + * {@hide} + */ + public static void setSystemLocale(Context context, String language, String country) { + String l = SystemProperties.get("persist.sys.language"); + String c = SystemProperties.get("persist.sys.country"); + + if (null == language) { + return; // no match possible + } + language = language.toLowerCase(); + if (null == country) { + country = ""; + } + country = country.toUpperCase(); + + if((null == l || 0 == l.length()) && (null == c || 0 == c.length())) { + try { + // try to find a good match + String[] locales = context.getAssets().getLocales(); + final int N = locales.length; + String bestMatch = null; + for(int i = 0; i < N; i++) { + // only match full (lang + country) locales + if (locales[i]!=null && locales[i].length() >= 5 && + locales[i].substring(0,2).equals(language)) { + if (locales[i].substring(3,5).equals(country)) { + bestMatch = locales[i]; + break; + } else if (null == bestMatch) { + bestMatch = locales[i]; + } + } + } + if (null != bestMatch) { + IActivityManager am = ActivityManagerNative.getDefault(); + Configuration config = am.getConfiguration(); + config.locale = new Locale(bestMatch.substring(0,2), + bestMatch.substring(3,5)); + config.userSetLocale = true; + am.updateConfiguration(config); + } + } catch (Exception e) { + // Intentionally left blank + } + } + } + + /** + * If the timezone is not already set, set it based on the MCC of the SIM. + * @param context Context to act on. + * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) + */ + private static void setTimezoneFromMccIfNeeded(Context context, int mcc) { + String timezone = SystemProperties.get(ServiceStateTracker.TIMEZONE_PROPERTY); + if (timezone == null || timezone.length() == 0) { + String zoneId = defaultTimeZoneForMcc(mcc); + if (zoneId != null && zoneId.length() > 0) { + // Set time zone based on MCC + AlarmManager alarm = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone(zoneId); + Log.d(LOG_TAG, "timezone set to "+zoneId); + } + } + } + + /** + * If the locale is not already set, set it based on the MCC of the SIM. + * @param context Context to act on. + * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) + */ + private static void setLocaleFromMccIfNeeded(Context context, int mcc) { + if (TelephonyManager.getLteOnCdmaModeStatic() == PhoneConstants.LTE_ON_CDMA_TRUE) { + // Avoid system locale is set from MCC table if CDMALTEPhone is used. + // The locale will be picked up based on EFpl/EFli once CSIM records are loaded. + return; + } + String language = MccTable.defaultLanguageForMcc(mcc); + String country = MccTable.countryCodeForMcc(mcc); + + Log.d(LOG_TAG, "locale set to "+language+"_"+country); + setSystemLocale(context, language, country); + } + + /** + * If the number of allowed wifi channels has not been set, set it based on + * the MCC of the SIM. + * @param context Context to act on. + * @param mcc Mobile Country Code of the SIM or SIM-like entity (build prop on CDMA) + */ + private static void setWifiCountryCodeFromMcc(Context context, int mcc) { + String country = MccTable.countryCodeForMcc(mcc); + if (!country.isEmpty()) { + Log.d(LOG_TAG, "WIFI_COUNTRY_CODE set to " + country); + WifiManager wM = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); + //persist + wM.setCountryCode(country, true); + } + } + + static { + table = new ArrayList<MccEntry>(240); + + + /* + * The table below is built from two resources: + * + * 1) ITU "Mobile Network Code (MNC) for the international + * identification plan for mobile terminals and mobile users" + * which is available as an annex to the ITU operational bulletin + * available here: http://www.itu.int/itu-t/bulletin/annex.html + * + * 2) The ISO 3166 country codes list, available here: + * http://www.iso.org/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/index.html + * + * This table has not been verified. + * + */ + + table.add(new MccEntry(202,"gr",2)); //Greece + table.add(new MccEntry(204,"nl",2,"nl")); //Netherlands (Kingdom of the) + table.add(new MccEntry(206,"be",2)); //Belgium + table.add(new MccEntry(208,"fr",2,"fr")); //France + table.add(new MccEntry(212,"mc",2)); //Monaco (Principality of) + table.add(new MccEntry(213,"ad",2)); //Andorra (Principality of) + table.add(new MccEntry(214,"es",2,"es")); //Spain + table.add(new MccEntry(216,"hu",2)); //Hungary (Republic of) + table.add(new MccEntry(218,"ba",2)); //Bosnia and Herzegovina + table.add(new MccEntry(219,"hr",2)); //Croatia (Republic of) + table.add(new MccEntry(220,"rs",2)); //Serbia and Montenegro + table.add(new MccEntry(222,"it",2,"it")); //Italy + table.add(new MccEntry(225,"va",2,"it")); //Vatican City State + table.add(new MccEntry(226,"ro",2)); //Romania + table.add(new MccEntry(228,"ch",2,"de")); //Switzerland (Confederation of) + table.add(new MccEntry(230,"cz",2,"cs")); //Czech Republic + table.add(new MccEntry(231,"sk",2)); //Slovak Republic + table.add(new MccEntry(232,"at",2,"de")); //Austria + table.add(new MccEntry(234,"gb",2,"en")); //United Kingdom of Great Britain and Northern Ireland + table.add(new MccEntry(235,"gb",2,"en")); //United Kingdom of Great Britain and Northern Ireland + table.add(new MccEntry(238,"dk",2)); //Denmark + table.add(new MccEntry(240,"se",2)); //Sweden + table.add(new MccEntry(242,"no",2)); //Norway + table.add(new MccEntry(244,"fi",2)); //Finland + table.add(new MccEntry(246,"lt",2)); //Lithuania (Republic of) + table.add(new MccEntry(247,"lv",2)); //Latvia (Republic of) + table.add(new MccEntry(248,"ee",2)); //Estonia (Republic of) + table.add(new MccEntry(250,"ru",2)); //Russian Federation + table.add(new MccEntry(255,"ua",2)); //Ukraine + table.add(new MccEntry(257,"by",2)); //Belarus (Republic of) + table.add(new MccEntry(259,"md",2)); //Moldova (Republic of) + table.add(new MccEntry(260,"pl",2)); //Poland (Republic of) + table.add(new MccEntry(262,"de",2,"de")); //Germany (Federal Republic of) + table.add(new MccEntry(266,"gi",2)); //Gibraltar + table.add(new MccEntry(268,"pt",2)); //Portugal + table.add(new MccEntry(270,"lu",2)); //Luxembourg + table.add(new MccEntry(272,"ie",2,"en")); //Ireland + table.add(new MccEntry(274,"is",2)); //Iceland + table.add(new MccEntry(276,"al",2)); //Albania (Republic of) + table.add(new MccEntry(278,"mt",2)); //Malta + table.add(new MccEntry(280,"cy",2)); //Cyprus (Republic of) + table.add(new MccEntry(282,"ge",2)); //Georgia + table.add(new MccEntry(283,"am",2)); //Armenia (Republic of) + table.add(new MccEntry(284,"bg",2)); //Bulgaria (Republic of) + table.add(new MccEntry(286,"tr",2)); //Turkey + table.add(new MccEntry(288,"fo",2)); //Faroe Islands + table.add(new MccEntry(289,"ge",2)); //Abkhazia (Georgia) + table.add(new MccEntry(290,"gl",2)); //Greenland (Denmark) + table.add(new MccEntry(292,"sm",2)); //San Marino (Republic of) + table.add(new MccEntry(293,"si",2)); //Slovenia (Republic of) + table.add(new MccEntry(294,"mk",2)); //The Former Yugoslav Republic of Macedonia + table.add(new MccEntry(295,"li",2)); //Liechtenstein (Principality of) + table.add(new MccEntry(297,"me",2)); //Montenegro (Republic of) + table.add(new MccEntry(302,"ca",3,"")); //Canada + table.add(new MccEntry(308,"pm",2)); //Saint Pierre and Miquelon (Collectivit territoriale de la Rpublique franaise) + table.add(new MccEntry(310,"us",3,"en")); //United States of America + table.add(new MccEntry(311,"us",3,"en")); //United States of America + table.add(new MccEntry(312,"us",3,"en")); //United States of America + table.add(new MccEntry(313,"us",3,"en")); //United States of America + table.add(new MccEntry(314,"us",3,"en")); //United States of America + table.add(new MccEntry(315,"us",3,"en")); //United States of America + table.add(new MccEntry(316,"us",3,"en")); //United States of America + table.add(new MccEntry(330,"pr",2)); //Puerto Rico + table.add(new MccEntry(332,"vi",2)); //United States Virgin Islands + table.add(new MccEntry(334,"mx",3)); //Mexico + table.add(new MccEntry(338,"jm",3)); //Jamaica + table.add(new MccEntry(340,"gp",2)); //Guadeloupe (French Department of) + table.add(new MccEntry(342,"bb",3)); //Barbados + table.add(new MccEntry(344,"ag",3)); //Antigua and Barbuda + table.add(new MccEntry(346,"ky",3)); //Cayman Islands + table.add(new MccEntry(348,"vg",3)); //British Virgin Islands + table.add(new MccEntry(350,"bm",2)); //Bermuda + table.add(new MccEntry(352,"gd",2)); //Grenada + table.add(new MccEntry(354,"ms",2)); //Montserrat + table.add(new MccEntry(356,"kn",2)); //Saint Kitts and Nevis + table.add(new MccEntry(358,"lc",2)); //Saint Lucia + table.add(new MccEntry(360,"vc",2)); //Saint Vincent and the Grenadines + table.add(new MccEntry(362,"ai",2)); //Netherlands Antilles + table.add(new MccEntry(363,"aw",2)); //Aruba + table.add(new MccEntry(364,"bs",2)); //Bahamas (Commonwealth of the) + table.add(new MccEntry(365,"ai",3)); //Anguilla + table.add(new MccEntry(366,"dm",2)); //Dominica (Commonwealth of) + table.add(new MccEntry(368,"cu",2)); //Cuba + table.add(new MccEntry(370,"do",2)); //Dominican Republic + table.add(new MccEntry(372,"ht",2)); //Haiti (Republic of) + table.add(new MccEntry(374,"tt",2)); //Trinidad and Tobago + table.add(new MccEntry(376,"tc",2)); //Turks and Caicos Islands + table.add(new MccEntry(400,"az",2)); //Azerbaijani Republic + table.add(new MccEntry(401,"kz",2)); //Kazakhstan (Republic of) + table.add(new MccEntry(402,"bt",2)); //Bhutan (Kingdom of) + table.add(new MccEntry(404,"in",2)); //India (Republic of) + table.add(new MccEntry(405,"in",2)); //India (Republic of) + table.add(new MccEntry(410,"pk",2)); //Pakistan (Islamic Republic of) + table.add(new MccEntry(412,"af",2)); //Afghanistan + table.add(new MccEntry(413,"lk",2)); //Sri Lanka (Democratic Socialist Republic of) + table.add(new MccEntry(414,"mm",2)); //Myanmar (Union of) + table.add(new MccEntry(415,"lb",2)); //Lebanon + table.add(new MccEntry(416,"jo",2)); //Jordan (Hashemite Kingdom of) + table.add(new MccEntry(417,"sy",2)); //Syrian Arab Republic + table.add(new MccEntry(418,"iq",2)); //Iraq (Republic of) + table.add(new MccEntry(419,"kw",2)); //Kuwait (State of) + table.add(new MccEntry(420,"sa",2)); //Saudi Arabia (Kingdom of) + table.add(new MccEntry(421,"ye",2)); //Yemen (Republic of) + table.add(new MccEntry(422,"om",2)); //Oman (Sultanate of) + table.add(new MccEntry(423,"ps",2)); //Palestine + table.add(new MccEntry(424,"ae",2)); //United Arab Emirates + table.add(new MccEntry(425,"il",2)); //Israel (State of) + table.add(new MccEntry(426,"bh",2)); //Bahrain (Kingdom of) + table.add(new MccEntry(427,"qa",2)); //Qatar (State of) + table.add(new MccEntry(428,"mn",2)); //Mongolia + table.add(new MccEntry(429,"np",2)); //Nepal + table.add(new MccEntry(430,"ae",2)); //United Arab Emirates + table.add(new MccEntry(431,"ae",2)); //United Arab Emirates + table.add(new MccEntry(432,"ir",2)); //Iran (Islamic Republic of) + table.add(new MccEntry(434,"uz",2)); //Uzbekistan (Republic of) + table.add(new MccEntry(436,"tj",2)); //Tajikistan (Republic of) + table.add(new MccEntry(437,"kg",2)); //Kyrgyz Republic + table.add(new MccEntry(438,"tm",2)); //Turkmenistan + table.add(new MccEntry(440,"jp",2,"ja")); //Japan + table.add(new MccEntry(441,"jp",2,"ja")); //Japan + table.add(new MccEntry(450,"kr",2,"ko")); //Korea (Republic of) + table.add(new MccEntry(452,"vn",2)); //Viet Nam (Socialist Republic of) + table.add(new MccEntry(454,"hk",2)); //"Hong Kong, China" + table.add(new MccEntry(455,"mo",2)); //"Macao, China" + table.add(new MccEntry(456,"kh",2)); //Cambodia (Kingdom of) + table.add(new MccEntry(457,"la",2)); //Lao People's Democratic Republic + table.add(new MccEntry(460,"cn",2,"zh")); //China (People's Republic of) + table.add(new MccEntry(461,"cn",2,"zh")); //China (People's Republic of) + table.add(new MccEntry(466,"tw",2)); //"Taiwan, China" + table.add(new MccEntry(467,"kp",2)); //Democratic People's Republic of Korea + table.add(new MccEntry(470,"bd",2)); //Bangladesh (People's Republic of) + table.add(new MccEntry(472,"mv",2)); //Maldives (Republic of) + table.add(new MccEntry(502,"my",2)); //Malaysia + table.add(new MccEntry(505,"au",2,"en")); //Australia + table.add(new MccEntry(510,"id",2)); //Indonesia (Republic of) + table.add(new MccEntry(514,"tl",2)); //Democratic Republic of Timor-Leste + table.add(new MccEntry(515,"ph",2)); //Philippines (Republic of the) + table.add(new MccEntry(520,"th",2)); //Thailand + table.add(new MccEntry(525,"sg",2,"en")); //Singapore (Republic of) + table.add(new MccEntry(528,"bn",2)); //Brunei Darussalam + table.add(new MccEntry(530,"nz",2, "en")); //New Zealand + table.add(new MccEntry(534,"mp",2)); //Northern Mariana Islands (Commonwealth of the) + table.add(new MccEntry(535,"gu",2)); //Guam + table.add(new MccEntry(536,"nr",2)); //Nauru (Republic of) + table.add(new MccEntry(537,"pg",2)); //Papua New Guinea + table.add(new MccEntry(539,"to",2)); //Tonga (Kingdom of) + table.add(new MccEntry(540,"sb",2)); //Solomon Islands + table.add(new MccEntry(541,"vu",2)); //Vanuatu (Republic of) + table.add(new MccEntry(542,"fj",2)); //Fiji (Republic of) + table.add(new MccEntry(543,"wf",2)); //Wallis and Futuna (Territoire franais d'outre-mer) + table.add(new MccEntry(544,"as",2)); //American Samoa + table.add(new MccEntry(545,"ki",2)); //Kiribati (Republic of) + table.add(new MccEntry(546,"nc",2)); //New Caledonia (Territoire franais d'outre-mer) + table.add(new MccEntry(547,"pf",2)); //French Polynesia (Territoire franais d'outre-mer) + table.add(new MccEntry(548,"ck",2)); //Cook Islands + table.add(new MccEntry(549,"ws",2)); //Samoa (Independent State of) + table.add(new MccEntry(550,"fm",2)); //Micronesia (Federated States of) + table.add(new MccEntry(551,"mh",2)); //Marshall Islands (Republic of the) + table.add(new MccEntry(552,"pw",2)); //Palau (Republic of) + table.add(new MccEntry(602,"eg",2)); //Egypt (Arab Republic of) + table.add(new MccEntry(603,"dz",2)); //Algeria (People's Democratic Republic of) + table.add(new MccEntry(604,"ma",2)); //Morocco (Kingdom of) + table.add(new MccEntry(605,"tn",2)); //Tunisia + table.add(new MccEntry(606,"ly",2)); //Libya (Socialist People's Libyan Arab Jamahiriya) + table.add(new MccEntry(607,"gm",2)); //Gambia (Republic of the) + table.add(new MccEntry(608,"sn",2)); //Senegal (Republic of) + table.add(new MccEntry(609,"mr",2)); //Mauritania (Islamic Republic of) + table.add(new MccEntry(610,"ml",2)); //Mali (Republic of) + table.add(new MccEntry(611,"gn",2)); //Guinea (Republic of) + table.add(new MccEntry(612,"ci",2)); //Cte d'Ivoire (Republic of) + table.add(new MccEntry(613,"bf",2)); //Burkina Faso + table.add(new MccEntry(614,"ne",2)); //Niger (Republic of the) + table.add(new MccEntry(615,"tg",2)); //Togolese Republic + table.add(new MccEntry(616,"bj",2)); //Benin (Republic of) + table.add(new MccEntry(617,"mu",2)); //Mauritius (Republic of) + table.add(new MccEntry(618,"lr",2)); //Liberia (Republic of) + table.add(new MccEntry(619,"sl",2)); //Sierra Leone + table.add(new MccEntry(620,"gh",2)); //Ghana + table.add(new MccEntry(621,"ng",2)); //Nigeria (Federal Republic of) + table.add(new MccEntry(622,"td",2)); //Chad (Republic of) + table.add(new MccEntry(623,"cf",2)); //Central African Republic + table.add(new MccEntry(624,"cm",2)); //Cameroon (Republic of) + table.add(new MccEntry(625,"cv",2)); //Cape Verde (Republic of) + table.add(new MccEntry(626,"st",2)); //Sao Tome and Principe (Democratic Republic of) + table.add(new MccEntry(627,"gq",2)); //Equatorial Guinea (Republic of) + table.add(new MccEntry(628,"ga",2)); //Gabonese Republic + table.add(new MccEntry(629,"cg",2)); //Congo (Republic of the) + table.add(new MccEntry(630,"cg",2)); //Democratic Republic of the Congo + table.add(new MccEntry(631,"ao",2)); //Angola (Republic of) + table.add(new MccEntry(632,"gw",2)); //Guinea-Bissau (Republic of) + table.add(new MccEntry(633,"sc",2)); //Seychelles (Republic of) + table.add(new MccEntry(634,"sd",2)); //Sudan (Republic of the) + table.add(new MccEntry(635,"rw",2)); //Rwanda (Republic of) + table.add(new MccEntry(636,"et",2)); //Ethiopia (Federal Democratic Republic of) + table.add(new MccEntry(637,"so",2)); //Somali Democratic Republic + table.add(new MccEntry(638,"dj",2)); //Djibouti (Republic of) + table.add(new MccEntry(639,"ke",2)); //Kenya (Republic of) + table.add(new MccEntry(640,"tz",2)); //Tanzania (United Republic of) + table.add(new MccEntry(641,"ug",2)); //Uganda (Republic of) + table.add(new MccEntry(642,"bi",2)); //Burundi (Republic of) + table.add(new MccEntry(643,"mz",2)); //Mozambique (Republic of) + table.add(new MccEntry(645,"zm",2)); //Zambia (Republic of) + table.add(new MccEntry(646,"mg",2)); //Madagascar (Republic of) + table.add(new MccEntry(647,"re",2)); //Reunion (French Department of) + table.add(new MccEntry(648,"zw",2)); //Zimbabwe (Republic of) + table.add(new MccEntry(649,"na",2)); //Namibia (Republic of) + table.add(new MccEntry(650,"mw",2)); //Malawi + table.add(new MccEntry(651,"ls",2)); //Lesotho (Kingdom of) + table.add(new MccEntry(652,"bw",2)); //Botswana (Republic of) + table.add(new MccEntry(653,"sz",2)); //Swaziland (Kingdom of) + table.add(new MccEntry(654,"km",2)); //Comoros (Union of the) + table.add(new MccEntry(655,"za",2,"en")); //South Africa (Republic of) + table.add(new MccEntry(657,"er",2)); //Eritrea + table.add(new MccEntry(702,"bz",2)); //Belize + table.add(new MccEntry(704,"gt",2)); //Guatemala (Republic of) + table.add(new MccEntry(706,"sv",2)); //El Salvador (Republic of) + table.add(new MccEntry(708,"hn",3)); //Honduras (Republic of) + table.add(new MccEntry(710,"ni",2)); //Nicaragua + table.add(new MccEntry(712,"cr",2)); //Costa Rica + table.add(new MccEntry(714,"pa",2)); //Panama (Republic of) + table.add(new MccEntry(716,"pe",2)); //Peru + table.add(new MccEntry(722,"ar",3)); //Argentine Republic + table.add(new MccEntry(724,"br",2)); //Brazil (Federative Republic of) + table.add(new MccEntry(730,"cl",2)); //Chile + table.add(new MccEntry(732,"co",3)); //Colombia (Republic of) + table.add(new MccEntry(734,"ve",2)); //Venezuela (Bolivarian Republic of) + table.add(new MccEntry(736,"bo",2)); //Bolivia (Republic of) + table.add(new MccEntry(738,"gy",2)); //Guyana + table.add(new MccEntry(740,"ec",2)); //Ecuador + table.add(new MccEntry(742,"gf",2)); //French Guiana (French Department of) + table.add(new MccEntry(744,"py",2)); //Paraguay (Republic of) + table.add(new MccEntry(746,"sr",2)); //Suriname (Republic of) + table.add(new MccEntry(748,"uy",2)); //Uruguay (Eastern Republic of) + table.add(new MccEntry(750,"fk",2)); //Falkland Islands (Malvinas) + //table.add(new MccEntry(901,"",2)); //"International Mobile, shared code" + + Collections.sort(table); + } +} diff --git a/src/java/com/android/internal/telephony/MmiCode.java b/src/java/com/android/internal/telephony/MmiCode.java new file mode 100644 index 0000000..c71ff77 --- /dev/null +++ b/src/java/com/android/internal/telephony/MmiCode.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +/** + * {@hide} + */ +public interface MmiCode +{ + /** + * {@hide} + */ + public enum State { + PENDING, + CANCELLED, + COMPLETE, + FAILED + } + + + /** + * @return Current state of MmiCode request + */ + public State getState(); + + /** + * @return Localized message for UI display, valid only in COMPLETE + * or FAILED states. null otherwise + */ + + public CharSequence getMessage(); + + /** + * Cancels pending MMI request. + * State becomes CANCELLED unless already COMPLETE or FAILED + */ + public void cancel(); + + /** + * @return true if the network response is a REQUEST for more user input. + */ + public boolean isUssdRequest(); + + /** + * @return true if an outstanding request can be canceled. + */ + public boolean isCancelable(); +} diff --git a/src/java/com/android/internal/telephony/OperatorInfo.java b/src/java/com/android/internal/telephony/OperatorInfo.java new file mode 100644 index 0000000..1999cb3 --- /dev/null +++ b/src/java/com/android/internal/telephony/OperatorInfo.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * {@hide} + */ +public class OperatorInfo implements Parcelable { + public enum State { + UNKNOWN, + AVAILABLE, + CURRENT, + FORBIDDEN; + } + + private String operatorAlphaLong; + private String operatorAlphaShort; + private String operatorNumeric; + + private State state = State.UNKNOWN; + + + public String + getOperatorAlphaLong() { + return operatorAlphaLong; + } + + public String + getOperatorAlphaShort() { + return operatorAlphaShort; + } + + public String + getOperatorNumeric() { + return operatorNumeric; + } + + public State + getState() { + return state; + } + + OperatorInfo(String operatorAlphaLong, + String operatorAlphaShort, + String operatorNumeric, + State state) { + + this.operatorAlphaLong = operatorAlphaLong; + this.operatorAlphaShort = operatorAlphaShort; + this.operatorNumeric = operatorNumeric; + + this.state = state; + } + + + public OperatorInfo(String operatorAlphaLong, + String operatorAlphaShort, + String operatorNumeric, + String stateString) { + this (operatorAlphaLong, operatorAlphaShort, + operatorNumeric, rilStateToState(stateString)); + } + + /** + * See state strings defined in ril.h RIL_REQUEST_QUERY_AVAILABLE_NETWORKS + */ + private static State rilStateToState(String s) { + if (s.equals("unknown")) { + return State.UNKNOWN; + } else if (s.equals("available")) { + return State.AVAILABLE; + } else if (s.equals("current")) { + return State.CURRENT; + } else if (s.equals("forbidden")) { + return State.FORBIDDEN; + } else { + throw new RuntimeException( + "RIL impl error: Invalid network state '" + s + "'"); + } + } + + + public String toString() { + return "OperatorInfo " + operatorAlphaLong + + "/" + operatorAlphaShort + + "/" + operatorNumeric + + "/" + state; + } + + /** + * Parcelable interface implemented below. + * This is a simple effort to make OperatorInfo parcelable rather than + * trying to make the conventional containing object (AsyncResult), + * implement parcelable. This functionality is needed for the + * NetworkQueryService to fix 1128695. + */ + + public int describeContents() { + return 0; + } + + /** + * Implement the Parcelable interface. + * Method to serialize a OperatorInfo object. + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(operatorAlphaLong); + dest.writeString(operatorAlphaShort); + dest.writeString(operatorNumeric); + dest.writeSerializable(state); + } + + /** + * Implement the Parcelable interface + * Method to deserialize a OperatorInfo object, or an array thereof. + */ + public static final Creator<OperatorInfo> CREATOR = + new Creator<OperatorInfo>() { + public OperatorInfo createFromParcel(Parcel in) { + OperatorInfo opInfo = new OperatorInfo( + in.readString(), /*operatorAlphaLong*/ + in.readString(), /*operatorAlphaShort*/ + in.readString(), /*operatorNumeric*/ + (State) in.readSerializable()); /*state*/ + return opInfo; + } + + public OperatorInfo[] newArray(int size) { + return new OperatorInfo[size]; + } + }; +} diff --git a/src/java/com/android/internal/telephony/Phone.java b/src/java/com/android/internal/telephony/Phone.java new file mode 100644 index 0000000..34aa96c --- /dev/null +++ b/src/java/com/android/internal/telephony/Phone.java @@ -0,0 +1,1695 @@ +/* + * 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 com.android.internal.telephony; + +import android.content.Context; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.telephony.CellLocation; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; + +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.gsm.UsimServiceTable; +import com.android.internal.telephony.ims.IsimRecords; +import com.android.internal.telephony.test.SimulatedRadioControl; + +import com.android.internal.telephony.PhoneConstants.*; // ???? + +import java.util.List; + +/** + * Internal interface used to control the phone; SDK developers cannot + * obtain this interface. + * + * {@hide} + * + */ +public interface Phone { + + /** used to enable additional debug messages */ + static final boolean DEBUG_PHONE = true; + + public enum DataActivityState { + /** + * The state of a data activity. + * <ul> + * <li>NONE = No traffic</li> + * <li>DATAIN = Receiving IP ppp traffic</li> + * <li>DATAOUT = Sending IP ppp traffic</li> + * <li>DATAINANDOUT = Both receiving and sending IP ppp traffic</li> + * <li>DORMANT = The data connection is still active, + but physical link is down</li> + * </ul> + */ + NONE, DATAIN, DATAOUT, DATAINANDOUT, DORMANT; + }; + + enum SuppService { + UNKNOWN, SWITCH, SEPARATE, TRANSFER, CONFERENCE, REJECT, HANGUP; + }; + + // "Features" accessible through the connectivity manager + static final String FEATURE_ENABLE_MMS = "enableMMS"; + static final String FEATURE_ENABLE_SUPL = "enableSUPL"; + static final String FEATURE_ENABLE_DUN = "enableDUN"; + static final String FEATURE_ENABLE_HIPRI = "enableHIPRI"; + static final String FEATURE_ENABLE_DUN_ALWAYS = "enableDUNAlways"; + static final String FEATURE_ENABLE_FOTA = "enableFOTA"; + static final String FEATURE_ENABLE_IMS = "enableIMS"; + static final String FEATURE_ENABLE_CBS = "enableCBS"; + + /** + * Optional reasons for disconnect and connect + */ + static final String REASON_ROAMING_ON = "roamingOn"; + static final String REASON_ROAMING_OFF = "roamingOff"; + static final String REASON_DATA_DISABLED = "dataDisabled"; + static final String REASON_DATA_ENABLED = "dataEnabled"; + static final String REASON_DATA_ATTACHED = "dataAttached"; + static final String REASON_DATA_DETACHED = "dataDetached"; + static final String REASON_CDMA_DATA_ATTACHED = "cdmaDataAttached"; + static final String REASON_CDMA_DATA_DETACHED = "cdmaDataDetached"; + static final String REASON_APN_CHANGED = "apnChanged"; + static final String REASON_APN_SWITCHED = "apnSwitched"; + static final String REASON_APN_FAILED = "apnFailed"; + static final String REASON_RESTORE_DEFAULT_APN = "restoreDefaultApn"; + static final String REASON_RADIO_TURNED_OFF = "radioTurnedOff"; + static final String REASON_PDP_RESET = "pdpReset"; + static final String REASON_VOICE_CALL_ENDED = "2GVoiceCallEnded"; + static final String REASON_VOICE_CALL_STARTED = "2GVoiceCallStarted"; + static final String REASON_PS_RESTRICT_ENABLED = "psRestrictEnabled"; + static final String REASON_PS_RESTRICT_DISABLED = "psRestrictDisabled"; + static final String REASON_SIM_LOADED = "simLoaded"; + static final String REASON_NW_TYPE_CHANGED = "nwTypeChanged"; + static final String REASON_DATA_DEPENDENCY_MET = "dependencyMet"; + static final String REASON_DATA_DEPENDENCY_UNMET = "dependencyUnmet"; + + // Used for band mode selection methods + static final int BM_UNSPECIFIED = 0; // selected by baseband automatically + static final int BM_EURO_BAND = 1; // GSM-900 / DCS-1800 / WCDMA-IMT-2000 + static final int BM_US_BAND = 2; // GSM-850 / PCS-1900 / WCDMA-850 / WCDMA-PCS-1900 + static final int BM_JPN_BAND = 3; // WCDMA-800 / WCDMA-IMT-2000 + static final int BM_AUS_BAND = 4; // GSM-900 / DCS-1800 / WCDMA-850 / WCDMA-IMT-2000 + static final int BM_AUS2_BAND = 5; // GSM-900 / DCS-1800 / WCDMA-850 + static final int BM_BOUNDARY = 6; // upper band boundary + + // Used for preferred network type + // Note NT_* substitute RILConstants.NETWORK_MODE_* above the Phone + int NT_MODE_WCDMA_PREF = RILConstants.NETWORK_MODE_WCDMA_PREF; + int NT_MODE_GSM_ONLY = RILConstants.NETWORK_MODE_GSM_ONLY; + int NT_MODE_WCDMA_ONLY = RILConstants.NETWORK_MODE_WCDMA_ONLY; + int NT_MODE_GSM_UMTS = RILConstants.NETWORK_MODE_GSM_UMTS; + + int NT_MODE_CDMA = RILConstants.NETWORK_MODE_CDMA; + + int NT_MODE_CDMA_NO_EVDO = RILConstants.NETWORK_MODE_CDMA_NO_EVDO; + int NT_MODE_EVDO_NO_CDMA = RILConstants.NETWORK_MODE_EVDO_NO_CDMA; + int NT_MODE_GLOBAL = RILConstants.NETWORK_MODE_GLOBAL; + + int NT_MODE_LTE_ONLY = RILConstants.NETWORK_MODE_LTE_ONLY; + int PREFERRED_NT_MODE = RILConstants.PREFERRED_NETWORK_MODE; + + + // Used for CDMA roaming mode + static final int CDMA_RM_HOME = 0; // Home Networks only, as defined in PRL + static final int CDMA_RM_AFFILIATED = 1; // Roaming an Affiliated networks, as defined in PRL + static final int CDMA_RM_ANY = 2; // Roaming on Any Network, as defined in PRL + + // Used for CDMA subscription mode + static final int CDMA_SUBSCRIPTION_RUIM_SIM = 0; // RUIM/SIM (default) + static final int CDMA_SUBSCRIPTION_NV = 1; // NV -> non-volatile memory + + static final int PREFERRED_CDMA_SUBSCRIPTION = CDMA_SUBSCRIPTION_NV; + + static final int TTY_MODE_OFF = 0; + static final int TTY_MODE_FULL = 1; + static final int TTY_MODE_HCO = 2; + static final int TTY_MODE_VCO = 3; + + /** + * CDMA OTA PROVISION STATUS, the same as RIL_CDMA_OTA_Status in ril.h + */ + + public static final int CDMA_OTA_PROVISION_STATUS_SPL_UNLOCKED = 0; + public static final int CDMA_OTA_PROVISION_STATUS_SPC_RETRIES_EXCEEDED = 1; + public static final int CDMA_OTA_PROVISION_STATUS_A_KEY_EXCHANGED = 2; + public static final int CDMA_OTA_PROVISION_STATUS_SSD_UPDATED = 3; + public static final int CDMA_OTA_PROVISION_STATUS_NAM_DOWNLOADED = 4; + public static final int CDMA_OTA_PROVISION_STATUS_MDN_DOWNLOADED = 5; + public static final int CDMA_OTA_PROVISION_STATUS_IMSI_DOWNLOADED = 6; + public static final int CDMA_OTA_PROVISION_STATUS_PRL_DOWNLOADED = 7; + public static final int CDMA_OTA_PROVISION_STATUS_COMMITTED = 8; + public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_STARTED = 9; + public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED = 10; + public static final int CDMA_OTA_PROVISION_STATUS_OTAPA_ABORTED = 11; + + + /** + * Get the current ServiceState. Use + * <code>registerForServiceStateChanged</code> to be informed of + * updates. + */ + ServiceState getServiceState(); + + /** + * Get the current CellLocation. + */ + CellLocation getCellLocation(); + + /** + * Get the current for the default apn DataState. No change notification + * exists at this interface -- use + * {@link android.telephony.PhoneStateListener} instead. + */ + DataState getDataConnectionState(); + + /** + * Get the current DataState. No change notification exists at this + * interface -- use + * {@link android.telephony.PhoneStateListener} instead. + * @param apnType specify for which apn to get connection state info. + */ + DataState getDataConnectionState(String apnType); + + /** + * Get the current DataActivityState. No change notification exists at this + * interface -- use + * {@link android.telephony.TelephonyManager} instead. + */ + DataActivityState getDataActivityState(); + + /** + * Gets the context for the phone, as set at initialization time. + */ + Context getContext(); + + /** + * Disables the DNS check (i.e., allows "0.0.0.0"). + * Useful for lab testing environment. + * @param b true disables the check, false enables. + */ + void disableDnsCheck(boolean b); + + /** + * Returns true if the DNS check is currently disabled. + */ + boolean isDnsCheckDisabled(); + + /** + * Get current coarse-grained voice call state. + * Use {@link #registerForPreciseCallStateChanged(Handler, int, Object) + * registerForPreciseCallStateChanged()} for change notification. <p> + * If the phone has an active call and call waiting occurs, + * then the phone state is RINGING not OFFHOOK + * <strong>Note:</strong> + * This registration point provides notification of finer-grained + * changes.<p> + * + */ + State getState(); + + /** + * Returns a string identifier for this phone interface for parties + * outside the phone app process. + * @return The string name. + */ + String getPhoneName(); + + /** + * Return a numerical identifier for the phone radio interface. + * @return PHONE_TYPE_XXX as defined above. + */ + int getPhoneType(); + + /** + * Returns an array of string identifiers for the APN types serviced by the + * currently active. + * @return The string array will always return at least one entry, Phone.APN_TYPE_DEFAULT. + * TODO: Revisit if we always should return at least one entry. + */ + String[] getActiveApnTypes(); + + /** + * Returns string for the active APN host. + * @return type as a string or null if none. + */ + String getActiveApnHost(String apnType); + + /** + * Return the LinkProperties for the named apn or null if not available + */ + LinkProperties getLinkProperties(String apnType); + + /** + * Return the LinkCapabilities + */ + LinkCapabilities getLinkCapabilities(String apnType); + + /** + * Get current signal strength. No change notification available on this + * interface. Use <code>PhoneStateNotifier</code> or an equivalent. + * An ASU is 0-31 or -1 if unknown (for GSM, dBm = -113 - 2 * asu). + * The following special values are defined:</p> + * <ul><li>0 means "-113 dBm or less".</li> + * <li>31 means "-51 dBm or greater".</li></ul> + * + * @return Current signal strength as SignalStrength + */ + SignalStrength getSignalStrength(); + + /** + * Notifies when a previously untracked non-ringing/waiting connection has appeared. + * This is likely due to some other entity (eg, SIM card application) initiating a call. + */ + void registerForUnknownConnection(Handler h, int what, Object obj); + + /** + * Unregisters for unknown connection notifications. + */ + void unregisterForUnknownConnection(Handler h); + + /** + * Register for getting notifications for change in the Call State {@link Call.State} + * This is called PreciseCallState because the call state is more precise than the + * {@link Phone.State} which can be obtained using the {@link PhoneStateListener} + * + * Resulting events will have an AsyncResult in <code>Message.obj</code>. + * AsyncResult.userData will be set to the obj argument here. + * The <em>h</em> parameter is held only by a weak reference. + */ + void registerForPreciseCallStateChanged(Handler h, int what, Object obj); + + /** + * Unregisters for voice call state change notifications. + * Extraneous calls are tolerated silently. + */ + void unregisterForPreciseCallStateChanged(Handler h); + + + /** + * Notifies when a new ringing or waiting connection has appeared.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = a Connection. <p> + * Please check Connection.isRinging() to make sure the Connection + * has not dropped since this message was posted. + * If Connection.isRinging() is true, then + * Connection.getCall() == Phone.getRingingCall() + */ + void registerForNewRingingConnection(Handler h, int what, Object obj); + + /** + * Unregisters for new ringing connection notification. + * Extraneous calls are tolerated silently + */ + + void unregisterForNewRingingConnection(Handler h); + + /** + * Notifies when an incoming call rings.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = a Connection. <p> + */ + void registerForIncomingRing(Handler h, int what, Object obj); + + /** + * Unregisters for ring notification. + * Extraneous calls are tolerated silently + */ + + void unregisterForIncomingRing(Handler h); + + /** + * Notifies when out-band ringback tone is needed.<p> + * + * Messages received from this: + * Message.obj will be an AsyncResult + * AsyncResult.userObj = obj + * AsyncResult.result = boolean, true to start play ringback tone + * and false to stop. <p> + */ + void registerForRingbackTone(Handler h, int what, Object obj); + + /** + * Unregisters for ringback tone notification. + */ + + void unregisterForRingbackTone(Handler h); + + /** + * Registers the handler to reset the uplink mute state to get + * uplink audio. + */ + void registerForResendIncallMute(Handler h, int what, Object obj); + + /** + * Unregisters for resend incall mute notifications. + */ + void unregisterForResendIncallMute(Handler h); + + /** + * Notifies when a voice connection has disconnected, either due to local + * or remote hangup or error. + * + * Messages received from this will have the following members:<p> + * <ul><li>Message.obj will be an AsyncResult</li> + * <li>AsyncResult.userObj = obj</li> + * <li>AsyncResult.result = a Connection object that is + * no longer connected.</li></ul> + */ + void registerForDisconnect(Handler h, int what, Object obj); + + /** + * Unregisters for voice disconnection notification. + * Extraneous calls are tolerated silently + */ + void unregisterForDisconnect(Handler h); + + + /** + * Register for notifications of initiation of a new MMI code request. + * MMI codes for GSM are discussed in 3GPP TS 22.030.<p> + * + * Example: If Phone.dial is called with "*#31#", then the app will + * be notified here.<p> + * + * The returned <code>Message.obj</code> will contain an AsyncResult. + * + * <code>obj.result</code> will be an "MmiCode" object. + */ + void registerForMmiInitiate(Handler h, int what, Object obj); + + /** + * Unregisters for new MMI initiate notification. + * Extraneous calls are tolerated silently + */ + void unregisterForMmiInitiate(Handler h); + + /** + * Register for notifications that an MMI request has completed + * its network activity and is in its final state. This may mean a state + * of COMPLETE, FAILED, or CANCELLED. + * + * <code>Message.obj</code> will contain an AsyncResult. + * <code>obj.result</code> will be an "MmiCode" object + */ + void registerForMmiComplete(Handler h, int what, Object obj); + + /** + * Unregisters for MMI complete notification. + * Extraneous calls are tolerated silently + */ + void unregisterForMmiComplete(Handler h); + + /** + * Registration point for Ecm timer reset + * @param h handler to notify + * @param what user-defined message code + * @param obj placed in Message.obj + */ + public void registerForEcmTimerReset(Handler h, int what, Object obj); + + /** + * Unregister for notification for Ecm timer reset + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForEcmTimerReset(Handler h); + + /** + * Returns a list of MMI codes that are pending. (They have initiated + * but have not yet completed). + * Presently there is only ever one. + * Use <code>registerForMmiInitiate</code> + * and <code>registerForMmiComplete</code> for change notification. + */ + public List<? extends MmiCode> getPendingMmiCodes(); + + /** + * Sends user response to a USSD REQUEST message. An MmiCode instance + * representing this response is sent to handlers registered with + * registerForMmiInitiate. + * + * @param ussdMessge Message to send in the response. + */ + public void sendUssdResponse(String ussdMessge); + + /** + * Register for ServiceState changed. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a ServiceState instance + */ + void registerForServiceStateChanged(Handler h, int what, Object obj); + + /** + * Unregisters for ServiceStateChange notification. + * Extraneous calls are tolerated silently + */ + void unregisterForServiceStateChanged(Handler h); + + /** + * Register for Supplementary Service notifications from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a SuppServiceNotification instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForSuppServiceNotification(Handler h, int what, Object obj); + + /** + * Unregisters for Supplementary Service notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForSuppServiceNotification(Handler h); + + /** + * Register for notifications when a supplementary service attempt fails. + * Message.obj will contain an AsyncResult. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForSuppServiceFailed(Handler h, int what, Object obj); + + /** + * Unregister for notifications when a supplementary service attempt fails. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForSuppServiceFailed(Handler h); + + /** + * Register for notifications when a sInCall VoicePrivacy is enabled + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj); + + /** + * Unegister for notifications when a sInCall VoicePrivacy is enabled + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForInCallVoicePrivacyOn(Handler h); + + /** + * Register for notifications when a sInCall VoicePrivacy is disabled + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj); + + /** + * Unegister for notifications when a sInCall VoicePrivacy is disabled + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForInCallVoicePrivacyOff(Handler h); + + /** + * Register for notifications when CDMA OTA Provision status change + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForCdmaOtaStatusChange(Handler h, int what, Object obj); + + /** + * Unegister for notifications when CDMA OTA Provision status change + * @param h Handler to be removed from the registrant list. + */ + void unregisterForCdmaOtaStatusChange(Handler h); + + /** + * Registration point for subscription info ready + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj); + + /** + * Unregister for notifications for subscription info + * @param h Handler to be removed from the registrant list. + */ + public void unregisterForSubscriptionInfoReady(Handler h); + + /** + * Returns SIM record load state. Use + * <code>getSimCard().registerForReady()</code> for change notification. + * + * @return true if records from the SIM have been loaded and are + * available (if applicable). If not applicable to the underlying + * technology, returns true as well. + */ + boolean getIccRecordsLoaded(); + + /** + * Returns the ICC card interface for this phone, or null + * if not applicable to underlying technology. + */ + IccCard getIccCard(); + + /** + * Answers a ringing or waiting call. Active calls, if any, go on hold. + * Answering occurs asynchronously, and final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException when no call is ringing or waiting + */ + void acceptCall() throws CallStateException; + + /** + * Reject (ignore) a ringing call. In GSM, this means UDUB + * (User Determined User Busy). Reject occurs asynchronously, + * and final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException when no call is ringing or waiting + */ + void rejectCall() throws CallStateException; + + /** + * Places any active calls on hold, and makes any held calls + * active. Switch occurs asynchronously and may fail. + * Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if a call is ringing, waiting, or + * dialing/alerting. In these cases, this operation may not be performed. + */ + void switchHoldingAndActive() throws CallStateException; + + /** + * Whether or not the phone can conference in the current phone + * state--that is, one call holding and one call active. + * @return true if the phone can conference; false otherwise. + */ + boolean canConference(); + + /** + * Conferences holding and active. Conference occurs asynchronously + * and may fail. Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if canConference() would return false. + * In these cases, this operation may not be performed. + */ + void conference() throws CallStateException; + + /** + * Enable or disable enhanced Voice Privacy (VP). If enhanced VP is + * disabled, normal VP is enabled. + * + * @param enable whether true or false to enable or disable. + * @param onComplete a callback message when the action is completed. + */ + void enableEnhancedVoicePrivacy(boolean enable, Message onComplete); + + /** + * Get the currently set Voice Privacy (VP) mode. + * + * @param onComplete a callback message when the action is completed. + */ + void getEnhancedVoicePrivacy(Message onComplete); + + /** + * Whether or not the phone can do explicit call transfer in the current + * phone state--that is, one call holding and one call active. + * @return true if the phone can do explicit call transfer; false otherwise. + */ + boolean canTransfer(); + + /** + * Connects the two calls and disconnects the subscriber from both calls + * Explicit Call Transfer occurs asynchronously + * and may fail. Final notification occurs via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + * + * @exception CallStateException if canTransfer() would return false. + * In these cases, this operation may not be performed. + */ + void explicitCallTransfer() throws CallStateException; + + /** + * Clears all DISCONNECTED connections from Call connection lists. + * Calls that were in the DISCONNECTED state become idle. This occurs + * synchronously. + */ + void clearDisconnected(); + + + /** + * Gets the foreground call object, which represents all connections that + * are dialing or active (all connections + * that have their audio path connected).<p> + * + * The foreground call is a singleton object. It is constant for the life + * of this phone. It is never null.<p> + * + * The foreground call will only ever be in one of these states: + * IDLE, ACTIVE, DIALING, ALERTING, or DISCONNECTED. + * + * State change notification is available via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + */ + Call getForegroundCall(); + + /** + * Gets the background call object, which represents all connections that + * are holding (all connections that have been accepted or connected, but + * do not have their audio path connected). <p> + * + * The background call is a singleton object. It is constant for the life + * of this phone object . It is never null.<p> + * + * The background call will only ever be in one of these states: + * IDLE, HOLDING or DISCONNECTED. + * + * State change notification is available via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + */ + Call getBackgroundCall(); + + /** + * Gets the ringing call object, which represents an incoming + * connection (if present) that is pending answer/accept. (This connection + * may be RINGING or WAITING, and there may be only one.)<p> + + * The ringing call is a singleton object. It is constant for the life + * of this phone. It is never null.<p> + * + * The ringing call will only ever be in one of these states: + * IDLE, INCOMING, WAITING or DISCONNECTED. + * + * State change notification is available via + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()}. + */ + Call getRingingCall(); + + /** + * Initiate a new voice connection. This happens asynchronously, so you + * cannot assume the audio path is connected (or a call index has been + * assigned) until PhoneStateChanged notification has occurred. + * + * @exception CallStateException if a new outgoing call is not currently + * possible because no more call slots exist or a call exists that is + * dialing, alerting, ringing, or waiting. Other errors are + * handled asynchronously. + */ + Connection dial(String dialString) throws CallStateException; + + /** + * Initiate a new voice connection with supplementary User to User + * Information. This happens asynchronously, so you cannot assume the audio + * path is connected (or a call index has been assigned) until + * PhoneStateChanged notification has occurred. + * + * @exception CallStateException if a new outgoing call is not currently + * possible because no more call slots exist or a call exists + * that is dialing, alerting, ringing, or waiting. Other + * errors are handled asynchronously. + */ + Connection dial(String dialString, UUSInfo uusInfo) throws CallStateException; + + /** + * Handles PIN MMI commands (PIN/PIN2/PUK/PUK2), which are initiated + * without SEND (so <code>dial</code> is not appropriate). + * + * @param dialString the MMI command to be executed. + * @return true if MMI command is executed. + */ + boolean handlePinMmi(String dialString); + + /** + * Handles in-call MMI commands. While in a call, or while receiving a + * call, use this to execute MMI commands. + * see 3GPP 20.030, section 6.5.5.1 for specs on the allowed MMI commands. + * + * @param command the MMI command to be executed. + * @return true if the MMI command is executed. + * @throws CallStateException + */ + boolean handleInCallMmiCommands(String command) throws CallStateException; + + /** + * Play a DTMF tone on the active call. Ignored if there is no active call. + * @param c should be one of 0-9, '*' or '#'. Other values will be + * silently ignored. + */ + void sendDtmf(char c); + + /** + * Start to paly a DTMF tone on the active call. Ignored if there is no active call + * or there is a playing DTMF tone. + * @param c should be one of 0-9, '*' or '#'. Other values will be + * silently ignored. + */ + void startDtmf(char c); + + /** + * Stop the playing DTMF tone. Ignored if there is no playing DTMF + * tone or no active call. + */ + void stopDtmf(); + + /** + * send burst DTMF tone, it can send the string as single character or multiple character + * ignore if there is no active call or not valid digits string. + * Valid digit means only includes characters ISO-LATIN characters 0-9, *, # + * The difference between sendDtmf and sendBurstDtmf is sendDtmf only sends one character, + * this api can send single character and multiple character, also, this api has response + * back to caller. + * + * @param dtmfString is string representing the dialing digit(s) in the active call + * @param on the DTMF ON length in milliseconds, or 0 for default + * @param off the DTMF OFF length in milliseconds, or 0 for default + * @param onComplete is the callback message when the action is processed by BP + * + */ + void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete); + + /** + * Sets the radio power on/off state (off is sometimes + * called "airplane mode"). Current state can be gotten via + * {@link #getServiceState()}.{@link + * android.telephony.ServiceState#getState() getState()}. + * <strong>Note: </strong>This request is asynchronous. + * getServiceState().getState() will not change immediately after this call. + * registerForServiceStateChanged() to find out when the + * request is complete. + * + * @param power true means "on", false means "off". + */ + void setRadioPower(boolean power); + + /** + * Get voice message waiting indicator status. No change notification + * available on this interface. Use PhoneStateNotifier or similar instead. + * + * @return true if there is a voice message waiting + */ + boolean getMessageWaitingIndicator(); + + /** + * Get voice call forwarding indicator status. No change notification + * available on this interface. Use PhoneStateNotifier or similar instead. + * + * @return true if there is a voice call forwarding + */ + boolean getCallForwardingIndicator(); + + /** + * Get the line 1 phone number (MSISDN). For CDMA phones, the MDN is returned + * and {@link #getMsisdn()} will return the MSISDN on CDMA/LTE phones.<p> + * + * @return phone number. May return null if not + * available or the SIM is not ready + */ + String getLine1Number(); + + /** + * Returns the alpha tag associated with the msisdn number. + * If there is no alpha tag associated or the record is not yet available, + * returns a default localized string. <p> + */ + String getLine1AlphaTag(); + + /** + * Sets the MSISDN phone number in the SIM card. + * + * @param alphaTag the alpha tag associated with the MSISDN phone number + * (see getMsisdnAlphaTag) + * @param number the new MSISDN phone number to be set on the SIM. + * @param onComplete a callback message when the action is completed. + */ + void setLine1Number(String alphaTag, String number, Message onComplete); + + /** + * Get the voice mail access phone number. Typically dialed when the + * user holds the "1" key in the phone app. May return null if not + * available or the SIM is not ready.<p> + */ + String getVoiceMailNumber(); + + /** + * Returns unread voicemail count. This count is shown when the voicemail + * notification is expanded.<p> + */ + int getVoiceMessageCount(); + + /** + * Returns the alpha tag associated with the voice mail number. + * If there is no alpha tag associated or the record is not yet available, + * returns a default localized string. <p> + * + * Please use this value instead of some other localized string when + * showing a name for this number in the UI. For example, call log + * entries should show this alpha tag. <p> + * + * Usage of this alpha tag in the UI is a common carrier requirement. + */ + String getVoiceMailAlphaTag(); + + /** + * setVoiceMailNumber + * sets the voicemail number in the SIM card. + * + * @param alphaTag the alpha tag associated with the voice mail number + * (see getVoiceMailAlphaTag) + * @param voiceMailNumber the new voicemail number to be set on the SIM. + * @param onComplete a callback message when the action is completed. + */ + void setVoiceMailNumber(String alphaTag, + String voiceMailNumber, + Message onComplete); + + /** + * getCallForwardingOptions + * gets a call forwarding option. The return value of + * ((AsyncResult)onComplete.obj) is an array of CallForwardInfo. + * + * @param commandInterfaceCFReason is one of the valid call forwarding + * CF_REASONS, as defined in + * <code>com.android.internal.telephony.CommandsInterface.</code> + * @param onComplete a callback message when the action is completed. + * @see com.android.internal.telephony.CallForwardInfo for details. + */ + void getCallForwardingOption(int commandInterfaceCFReason, + Message onComplete); + + /** + * setCallForwardingOptions + * sets a call forwarding option. + * + * @param commandInterfaceCFReason is one of the valid call forwarding + * CF_REASONS, as defined in + * <code>com.android.internal.telephony.CommandsInterface.</code> + * @param commandInterfaceCFAction is one of the valid call forwarding + * CF_ACTIONS, as defined in + * <code>com.android.internal.telephony.CommandsInterface.</code> + * @param dialingNumber is the target phone number to forward calls to + * @param timerSeconds is used by CFNRy to indicate the timeout before + * forwarding is attempted. + * @param onComplete a callback message when the action is completed. + */ + void setCallForwardingOption(int commandInterfaceCFReason, + int commandInterfaceCFAction, + String dialingNumber, + int timerSeconds, + Message onComplete); + + /** + * getOutgoingCallerIdDisplay + * gets outgoing caller id display. The return value of + * ((AsyncResult)onComplete.obj) is an array of int, with a length of 2. + * + * @param onComplete a callback message when the action is completed. + * @see com.android.internal.telephony.CommandsInterface#getCLIR for details. + */ + void getOutgoingCallerIdDisplay(Message onComplete); + + /** + * setOutgoingCallerIdDisplay + * sets a call forwarding option. + * + * @param commandInterfaceCLIRMode is one of the valid call CLIR + * modes, as defined in + * <code>com.android.internal.telephony.CommandsInterface./code> + * @param onComplete a callback message when the action is completed. + */ + void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, + Message onComplete); + + /** + * getCallWaiting + * gets call waiting activation state. The return value of + * ((AsyncResult)onComplete.obj) is an array of int, with a length of 1. + * + * @param onComplete a callback message when the action is completed. + * @see com.android.internal.telephony.CommandsInterface#queryCallWaiting for details. + */ + void getCallWaiting(Message onComplete); + + /** + * setCallWaiting + * sets a call forwarding option. + * + * @param enable is a boolean representing the state that you are + * requesting, true for enabled, false for disabled. + * @param onComplete a callback message when the action is completed. + */ + void setCallWaiting(boolean enable, Message onComplete); + + /** + * Scan available networks. This method is asynchronous; . + * On completion, <code>response.obj</code> is set to an AsyncResult with + * one of the following members:.<p> + *<ul> + * <li><code>response.obj.result</code> will be a <code>List</code> of + * <code>OperatorInfo</code> objects, or</li> + * <li><code>response.obj.exception</code> will be set with an exception + * on failure.</li> + * </ul> + */ + void getAvailableNetworks(Message response); + + /** + * Switches network selection mode to "automatic", re-scanning and + * re-selecting a network if appropriate. + * + * @param response The message to dispatch when the network selection + * is complete. + * + * @see #selectNetworkManually(OperatorInfo, android.os.Message ) + */ + void setNetworkSelectionModeAutomatic(Message response); + + /** + * Manually selects a network. <code>response</code> is + * dispatched when this is complete. <code>response.obj</code> will be + * an AsyncResult, and <code>response.obj.exception</code> will be non-null + * on failure. + * + * @see #setNetworkSelectionModeAutomatic(Message) + */ + void selectNetworkManually(OperatorInfo network, + Message response); + + /** + * Requests to set the preferred network type for searching and registering + * (CS/PS domain, RAT, and operation mode) + * @param networkType one of NT_*_TYPE + * @param response is callback message + */ + void setPreferredNetworkType(int networkType, Message response); + + /** + * Query the preferred network type setting + * + * @param response is callback message to report one of NT_*_TYPE + */ + void getPreferredNetworkType(Message response); + + /** + * Gets the default SMSC address. + * + * @param result Callback message contains the SMSC address. + */ + void getSmscAddress(Message result); + + /** + * Sets the default SMSC address. + * + * @param address new SMSC address + * @param result Callback message is empty on completion + */ + void setSmscAddress(String address, Message result); + + /** + * Query neighboring cell IDs. <code>response</code> is dispatched when + * this is complete. <code>response.obj</code> will be an AsyncResult, + * and <code>response.obj.exception</code> will be non-null on failure. + * On success, <code>AsyncResult.result</code> will be a <code>String[]</code> + * containing the neighboring cell IDs. Index 0 will contain the count + * of available cell IDs. Cell IDs are in hexadecimal format. + * + * @param response callback message that is dispatched when the query + * completes. + */ + void getNeighboringCids(Message response); + + /** + * Sets an event to be fired when the telephony system processes + * a post-dial character on an outgoing call.<p> + * + * Messages of type <code>what</code> will be sent to <code>h</code>. + * The <code>obj</code> field of these Message's will be instances of + * <code>AsyncResult</code>. <code>Message.obj.result</code> will be + * a Connection object.<p> + * + * Message.arg1 will be the post dial character being processed, + * or 0 ('\0') if end of string.<p> + * + * If Connection.getPostDialState() == WAIT, + * the application must call + * {@link com.android.internal.telephony.Connection#proceedAfterWaitChar() + * Connection.proceedAfterWaitChar()} or + * {@link com.android.internal.telephony.Connection#cancelPostDial() + * Connection.cancelPostDial()} + * for the telephony system to continue playing the post-dial + * DTMF sequence.<p> + * + * If Connection.getPostDialState() == WILD, + * the application must call + * {@link com.android.internal.telephony.Connection#proceedAfterWildChar + * Connection.proceedAfterWildChar()} + * or + * {@link com.android.internal.telephony.Connection#cancelPostDial() + * Connection.cancelPostDial()} + * for the telephony system to continue playing the + * post-dial DTMF sequence.<p> + * + * Only one post dial character handler may be set. <p> + * Calling this method with "h" equal to null unsets this handler.<p> + */ + void setOnPostDialCharacter(Handler h, int what, Object obj); + + + /** + * Mutes or unmutes the microphone for the active call. The microphone + * is automatically unmuted if a call is answered, dialed, or resumed + * from a holding state. + * + * @param muted true to mute the microphone, + * false to activate the microphone. + */ + + void setMute(boolean muted); + + /** + * Gets current mute status. Use + * {@link #registerForPreciseCallStateChanged(android.os.Handler, int, + * java.lang.Object) registerForPreciseCallStateChanged()} + * as a change notifcation, although presently phone state changed is not + * fired when setMute() is called. + * + * @return true is muting, false is unmuting + */ + boolean getMute(); + + /** + * Enables or disables echo suppression. + */ + void setEchoSuppressionEnabled(boolean enabled); + + /** + * Invokes RIL_REQUEST_OEM_HOOK_RAW on RIL implementation. + * + * @param data The data for the request. + * @param response <strong>On success</strong>, + * (byte[])(((AsyncResult)response.obj).result) + * <strong>On failure</strong>, + * (((AsyncResult)response.obj).result) == null and + * (((AsyncResult)response.obj).exception) being an instance of + * com.android.internal.telephony.gsm.CommandException + * + * @see #invokeOemRilRequestRaw(byte[], android.os.Message) + */ + void invokeOemRilRequestRaw(byte[] data, Message response); + + /** + * Invokes RIL_REQUEST_OEM_HOOK_Strings on RIL implementation. + * + * @param strings The strings to make available as the request data. + * @param response <strong>On success</strong>, "response" bytes is + * made available as: + * (String[])(((AsyncResult)response.obj).result). + * <strong>On failure</strong>, + * (((AsyncResult)response.obj).result) == null and + * (((AsyncResult)response.obj).exception) being an instance of + * com.android.internal.telephony.gsm.CommandException + * + * @see #invokeOemRilRequestStrings(java.lang.String[], android.os.Message) + */ + void invokeOemRilRequestStrings(String[] strings, Message response); + + /** + * Get the current active Data Call list + * + * @param response <strong>On success</strong>, "response" bytes is + * made available as: + * (String[])(((AsyncResult)response.obj).result). + * <strong>On failure</strong>, + * (((AsyncResult)response.obj).result) == null and + * (((AsyncResult)response.obj).exception) being an instance of + * com.android.internal.telephony.gsm.CommandException + */ + void getDataCallList(Message response); + + /** + * Update the ServiceState CellLocation for current network registration. + */ + void updateServiceLocation(); + + /** + * Enable location update notifications. + */ + void enableLocationUpdates(); + + /** + * Disable location update notifications. + */ + void disableLocationUpdates(); + + /** + * For unit tests; don't send notifications to "Phone" + * mailbox registrants if true. + */ + void setUnitTestMode(boolean f); + + /** + * @return true If unit test mode is enabled + */ + boolean getUnitTestMode(); + + /** + * Assign a specified band for RF configuration. + * + * @param bandMode one of BM_*_BAND + * @param response is callback message + */ + void setBandMode(int bandMode, Message response); + + /** + * Query the list of band mode supported by RF. + * + * @param response is callback message + * ((AsyncResult)response.obj).result is an int[] with every + * element representing one avialable BM_*_BAND + */ + void queryAvailableBandMode(Message response); + + /** + * @return true if enable data connection on roaming + */ + boolean getDataRoamingEnabled(); + + /** + * @param enable set true if enable data connection on roaming + */ + void setDataRoamingEnabled(boolean enable); + + /** + * Query the CDMA roaming preference setting + * + * @param response is callback message to report one of CDMA_RM_* + */ + void queryCdmaRoamingPreference(Message response); + + /** + * Requests to set the CDMA roaming preference + * @param cdmaRoamingType one of CDMA_RM_* + * @param response is callback message + */ + void setCdmaRoamingPreference(int cdmaRoamingType, Message response); + + /** + * Requests to set the CDMA subscription mode + * @param cdmaSubscriptionType one of CDMA_SUBSCRIPTION_* + * @param response is callback message + */ + void setCdmaSubscription(int cdmaSubscriptionType, Message response); + + /** + * If this is a simulated phone interface, returns a SimulatedRadioControl. + * @ return A SimulatedRadioControl if this is a simulated interface; + * otherwise, null. + */ + SimulatedRadioControl getSimulatedRadioControl(); + + /** + * Enables the specified APN type. Only works for "special" APN types, + * i.e., not the default APN. + * @param type The desired APN type. Cannot be {@link #APN_TYPE_DEFAULT}. + * @return <code>APN_ALREADY_ACTIVE</code> if the current APN + * services the requested type.<br/> + * <code>APN_TYPE_NOT_AVAILABLE</code> if the carrier does not + * support the requested APN.<br/> + * <code>APN_REQUEST_STARTED</code> if the request has been initiated.<br/> + * <code>APN_REQUEST_FAILED</code> if the request was invalid.<br/> + * A <code>ACTION_ANY_DATA_CONNECTION_STATE_CHANGED</code> broadcast will + * indicate connection state progress. + */ + int enableApnType(String type); + + /** + * Disables the specified APN type, and switches back to the default APN, + * if necessary. Switching to the default APN will not happen if default + * data traffic has been explicitly disabled via a call to {@link #disableDataConnectivity}. + * <p/>Only works for "special" APN types, + * i.e., not the default APN. + * @param type The desired APN type. Cannot be {@link #APN_TYPE_DEFAULT}. + * @return <code>APN_ALREADY_ACTIVE</code> if the default APN + * is already active.<br/> + * <code>APN_REQUEST_STARTED</code> if the request to switch to the default + * APN has been initiated.<br/> + * <code>APN_REQUEST_FAILED</code> if the request was invalid.<br/> + * A <code>ACTION_ANY_DATA_CONNECTION_STATE_CHANGED</code> broadcast will + * indicate connection state progress. + */ + int disableApnType(String type); + + /** + * Report on whether data connectivity is allowed. + */ + boolean isDataConnectivityPossible(); + + /** + * Report on whether data connectivity is allowed for an APN. + */ + boolean isDataConnectivityPossible(String apnType); + + /** + * Retrieves the unique device ID, e.g., IMEI for GSM phones and MEID for CDMA phones. + */ + String getDeviceId(); + + /** + * Retrieves the software version number for the device, e.g., IMEI/SV + * for GSM phones. + */ + String getDeviceSvn(); + + /** + * Retrieves the unique subscriber ID, e.g., IMSI for GSM phones. + */ + String getSubscriberId(); + + /** + * Retrieves the serial number of the ICC, if applicable. + */ + String getIccSerialNumber(); + + /* CDMA support methods */ + + /** + * Retrieves the MIN for CDMA phones. + */ + String getCdmaMin(); + + /** + * Check if subscription data has been assigned to mMin + * + * return true if MIN info is ready; false otherwise. + */ + boolean isMinInfoReady(); + + /** + * Retrieves PRL Version for CDMA phones + */ + String getCdmaPrlVersion(); + + /** + * Retrieves the ESN for CDMA phones. + */ + String getEsn(); + + /** + * Retrieves MEID for CDMA phones. + */ + String getMeid(); + + /** + * Retrieves the MSISDN from the UICC. For GSM/UMTS phones, this is equivalent to + * {@link #getLine1Number()}. For CDMA phones, {@link #getLine1Number()} returns + * the MDN, so this method is provided to return the MSISDN on CDMA/LTE phones. + */ + String getMsisdn(); + + /** + * Retrieves IMEI for phones. Returns null if IMEI is not set. + */ + String getImei(); + + /** + * Retrieves the PhoneSubInfo of the Phone + */ + public PhoneSubInfo getPhoneSubInfo(); + + /** + * Retrieves the IccSmsInterfaceManager of the Phone + */ + public IccSmsInterfaceManager getIccSmsInterfaceManager(); + + /** + * Retrieves the IccPhoneBookInterfaceManager of the Phone + */ + public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(); + + /** + * setTTYMode + * sets a TTY mode option. + * @param ttyMode is a one of the following: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * @param onComplete a callback message when the action is completed + */ + void setTTYMode(int ttyMode, Message onComplete); + + /** + * queryTTYMode + * query the status of the TTY mode + * + * @param onComplete a callback message when the action is completed. + */ + void queryTTYMode(Message onComplete); + + /** + * Activate or deactivate cell broadcast SMS. + * + * @param activate + * 0 = activate, 1 = deactivate + * @param response + * Callback message is empty on completion + */ + void activateCellBroadcastSms(int activate, Message response); + + /** + * Query the current configuration of cdma cell broadcast SMS. + * + * @param response + * Callback message is empty on completion + */ + void getCellBroadcastSmsConfig(Message response); + + /** + * Configure cell broadcast SMS. + * + * TODO: Change the configValuesArray to a RIL_BroadcastSMSConfig + * + * @param response + * Callback message is empty on completion + */ + public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response); + + public void notifyDataActivity(); + + /** + * Returns the CDMA ERI icon index to display + */ + public int getCdmaEriIconIndex(); + + /** + * Returns the CDMA ERI icon mode, + * 0 - ON + * 1 - FLASHING + */ + public int getCdmaEriIconMode(); + + /** + * Returns the CDMA ERI text, + */ + public String getCdmaEriText(); + + /** + * request to exit emergency call back mode + * the caller should use setOnECMModeExitResponse + * to receive the emergency callback mode exit response + */ + void exitEmergencyCallbackMode(); + + /** + * this decides if the dial number is OTA(Over the air provision) number or not + * @param dialStr is string representing the dialing digit(s) + * @return true means the dialStr is OTA number, and false means the dialStr is not OTA number + */ + boolean isOtaSpNumber(String dialStr); + + /** + * Returns true if OTA Service Provisioning needs to be performed. + */ + boolean needsOtaServiceProvisioning(); + + /** + * Register for notifications when CDMA call waiting comes + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForCallWaiting(Handler h, int what, Object obj); + + /** + * Unegister for notifications when CDMA Call waiting comes + * @param h Handler to be removed from the registrant list. + */ + void unregisterForCallWaiting(Handler h); + + + /** + * Register for signal information notifications from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a SuppServiceNotification instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + + void registerForSignalInfo(Handler h, int what, Object obj) ; + /** + * Unregisters for signal information notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForSignalInfo(Handler h); + + /** + * Register for display information notifications from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a SuppServiceNotification instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForDisplayInfo(Handler h, int what, Object obj); + + /** + * Unregisters for display information notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForDisplayInfo(Handler h) ; + + /** + * Register for CDMA number information record notification from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a CdmaInformationRecords.CdmaNumberInfoRec + * instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForNumberInfo(Handler h, int what, Object obj); + + /** + * Unregisters for number information record notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForNumberInfo(Handler h); + + /** + * Register for CDMA redirected number information record notification + * from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a CdmaInformationRecords.CdmaRedirectingNumberInfoRec + * instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForRedirectedNumberInfo(Handler h, int what, Object obj); + + /** + * Unregisters for redirected number information record notification. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForRedirectedNumberInfo(Handler h); + + /** + * Register for CDMA line control information record notification + * from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a CdmaInformationRecords.CdmaLineControlInfoRec + * instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForLineControlInfo(Handler h, int what, Object obj); + + /** + * Unregisters for line control information notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForLineControlInfo(Handler h); + + /** + * Register for CDMA T53 CLIR information record notifications + * from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a CdmaInformationRecords.CdmaT53ClirInfoRec + * instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerFoT53ClirlInfo(Handler h, int what, Object obj); + + /** + * Unregisters for T53 CLIR information record notification + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForT53ClirInfo(Handler h); + + /** + * Register for CDMA T53 audio control information record notifications + * from the network. + * Message.obj will contain an AsyncResult. + * AsyncResult.result will be a CdmaInformationRecords.CdmaT53AudioControlInfoRec + * instance. + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + void registerForT53AudioControlInfo(Handler h, int what, Object obj); + + /** + * Unregisters for T53 audio control information record notifications. + * Extraneous calls are tolerated silently + * + * @param h Handler to be removed from the registrant list. + */ + void unregisterForT53AudioControlInfo(Handler h); + + /** + * registers for exit emergency call back mode request response + * + * @param h Handler that receives the notification message. + * @param what User-defined message code. + * @param obj User object. + */ + + void setOnEcbModeExitResponse(Handler h, int what, Object obj); + + /** + * Unregisters for exit emergency call back mode request response + * + * @param h Handler to be removed from the registrant list. + */ + void unsetOnEcbModeExitResponse(Handler h); + + /** + * Return if the current radio is LTE on CDMA. This + * is a tri-state return value as for a period of time + * the mode may be unknown. + * + * @return {@link #LTE_ON_CDMA_UNKNOWN}, {@link #LTE_ON_CDMA_FALSE} or {@link #LTE_ON_CDMA_TRUE} + */ + public int getLteOnCdmaMode(); + + /** + * TODO: Adding a function for each property is not good. + * A fucntion of type getPhoneProp(propType) where propType is an + * enum of GSM+CDMA+LTE props would be a better approach. + * + * Get "Restriction of menu options for manual PLMN selection" bit + * status from EF_CSP data, this belongs to "Value Added Services Group". + * @return true if this bit is set or EF_CSP data is unavailable, + * false otherwise + */ + boolean isCspPlmnEnabled(); + + /** + * Return an interface to retrieve the ISIM records for IMS, if available. + * @return the interface to retrieve the ISIM records, or null if not supported + */ + IsimRecords getIsimRecords(); + + /** + * Request the ISIM application on the UICC to perform the AKA + * challenge/response algorithm for IMS authentication. The nonce string + * and challenge response are Base64 encoded Strings. + * + * @param nonce the nonce string to pass with the ISIM authentication request + * @param response a callback message with the String response in the obj field + */ + void requestIsimAuthentication(String nonce, Message response); + + /** + * Sets the SIM voice message waiting indicator records. + * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported + * @param countWaiting The number of messages waiting, if known. Use + * -1 to indicate that an unknown number of + * messages are waiting + */ + void setVoiceMessageWaiting(int line, int countWaiting); + + /** + * Gets the USIM service table from the UICC, if present and available. + * @return an interface to the UsimServiceTable record, or null if not available + */ + UsimServiceTable getUsimServiceTable(); + + /** + * Unregister from all events it registered for and dispose objects + * created by this object. + */ + void dispose(); + + /** + * Remove references to external object stored in this object. + */ + void removeReferences(); +} diff --git a/src/java/com/android/internal/telephony/PhoneBase.java b/src/java/com/android/internal/telephony/PhoneBase.java new file mode 100644 index 0000000..b55240a --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneBase.java @@ -0,0 +1,1189 @@ +/* + * 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 com.android.internal.telephony; + +import android.app.ActivityManagerNative; +import android.app.IActivityManager; +import android.content.Context; +import android.content.res.Configuration; +import android.content.SharedPreferences; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.net.wifi.WifiManager; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RegistrantList; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.provider.Settings; +import android.telephony.ServiceState; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.R; +import com.android.internal.telephony.gsm.UsimServiceTable; +import com.android.internal.telephony.ims.IsimRecords; +import com.android.internal.telephony.test.SimulatedRadioControl; +import com.android.internal.telephony.gsm.SIMRecords; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * (<em>Not for SDK use</em>) + * A base implementation for the com.android.internal.telephony.Phone interface. + * + * Note that implementations of Phone.java are expected to be used + * from a single application thread. This should be the same thread that + * originally called PhoneFactory to obtain the interface. + * + * {@hide} + * + */ + +public abstract class PhoneBase extends Handler implements Phone { + private static final String LOG_TAG = "PHONE"; + private static final boolean LOCAL_DEBUG = true; + + // Key used to read and write the saved network selection numeric value + public static final String NETWORK_SELECTION_KEY = "network_selection_key"; + // Key used to read and write the saved network selection operator name + public static final String NETWORK_SELECTION_NAME_KEY = "network_selection_name_key"; + + + // Key used to read/write "disable data connection on boot" pref (used for testing) + public static final String DATA_DISABLED_ON_BOOT_KEY = "disabled_on_boot_key"; + + /* Event Constants */ + protected static final int EVENT_RADIO_AVAILABLE = 1; + /** Supplementary Service Notification received. */ + protected static final int EVENT_SSN = 2; + protected static final int EVENT_SIM_RECORDS_LOADED = 3; + protected static final int EVENT_MMI_DONE = 4; + protected static final int EVENT_RADIO_ON = 5; + protected static final int EVENT_GET_BASEBAND_VERSION_DONE = 6; + protected static final int EVENT_USSD = 7; + protected static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = 8; + protected static final int EVENT_GET_IMEI_DONE = 9; + protected static final int EVENT_GET_IMEISV_DONE = 10; + protected static final int EVENT_GET_SIM_STATUS_DONE = 11; + protected static final int EVENT_SET_CALL_FORWARD_DONE = 12; + protected static final int EVENT_GET_CALL_FORWARD_DONE = 13; + protected static final int EVENT_CALL_RING = 14; + protected static final int EVENT_CALL_RING_CONTINUE = 15; + + // Used to intercept the carrier selection calls so that + // we can save the values. + protected static final int EVENT_SET_NETWORK_MANUAL_COMPLETE = 16; + protected static final int EVENT_SET_NETWORK_AUTOMATIC_COMPLETE = 17; + protected static final int EVENT_SET_CLIR_COMPLETE = 18; + protected static final int EVENT_REGISTERED_TO_NETWORK = 19; + protected static final int EVENT_SET_VM_NUMBER_DONE = 20; + // Events for CDMA support + protected static final int EVENT_GET_DEVICE_IDENTITY_DONE = 21; + protected static final int EVENT_RUIM_RECORDS_LOADED = 22; + protected static final int EVENT_NV_READY = 23; + protected static final int EVENT_SET_ENHANCED_VP = 24; + protected static final int EVENT_EMERGENCY_CALLBACK_MODE_ENTER = 25; + protected static final int EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE = 26; + protected static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 27; + // other + protected static final int EVENT_SET_NETWORK_AUTOMATIC = 28; + protected static final int EVENT_NEW_ICC_SMS = 29; + protected static final int EVENT_ICC_RECORD_EVENTS = 30; + + // Key used to read/write current CLIR setting + public static final String CLIR_KEY = "clir_key"; + + // Key used to read/write "disable DNS server check" pref (used for testing) + public static final String DNS_SERVER_CHECK_DISABLED_KEY = "dns_server_check_disabled_key"; + + /* Instance Variables */ + public CommandsInterface mCM; + boolean mDnsCheckDisabled; + public DataConnectionTracker mDataConnectionTracker; + boolean mDoesRilSendMultipleCallRing; + int mCallRingContinueToken; + int mCallRingDelay; + public boolean mIsTheCurrentActivePhone = true; + boolean mIsVoiceCapable = true; + public IccRecords mIccRecords; + protected AtomicReference<IccCard> mIccCard = new AtomicReference<IccCard>(); + public SmsStorageMonitor mSmsStorageMonitor; + public SmsUsageMonitor mSmsUsageMonitor; + public SMSDispatcher mSMS; + + /** + * Set a system property, unless we're in unit test mode + */ + public void + setSystemProperty(String property, String value) { + if(getUnitTestMode()) { + return; + } + SystemProperties.set(property, value); + } + + + protected final RegistrantList mPreciseCallStateRegistrants + = new RegistrantList(); + + protected final RegistrantList mNewRingingConnectionRegistrants + = new RegistrantList(); + + protected final RegistrantList mIncomingRingRegistrants + = new RegistrantList(); + + protected final RegistrantList mDisconnectRegistrants + = new RegistrantList(); + + protected final RegistrantList mServiceStateRegistrants + = new RegistrantList(); + + protected final RegistrantList mMmiCompleteRegistrants + = new RegistrantList(); + + protected final RegistrantList mMmiRegistrants + = new RegistrantList(); + + protected final RegistrantList mUnknownConnectionRegistrants + = new RegistrantList(); + + protected final RegistrantList mSuppServiceFailedRegistrants + = new RegistrantList(); + + protected Looper mLooper; /* to insure registrants are in correct thread*/ + + protected final Context mContext; + + /** + * PhoneNotifier is an abstraction for all system-wide + * state change notification. DefaultPhoneNotifier is + * used here unless running we're inside a unit test. + */ + protected PhoneNotifier mNotifier; + + protected SimulatedRadioControl mSimulatedRadioControl; + + boolean mUnitTestMode; + + /** + * Constructs a PhoneBase in normal (non-unit test) mode. + * + * @param context Context object from hosting application + * @param notifier An instance of DefaultPhoneNotifier, + * unless unit testing. + */ + protected PhoneBase(PhoneNotifier notifier, Context context, CommandsInterface ci) { + this(notifier, context, ci, false); + } + + /** + * Constructs a PhoneBase in normal (non-unit test) mode. + * + * @param context Context object from hosting application + * @param notifier An instance of DefaultPhoneNotifier, + * unless unit testing. + * @param unitTestMode when true, prevents notifications + * of state change events + */ + protected PhoneBase(PhoneNotifier notifier, Context context, CommandsInterface ci, + boolean unitTestMode) { + this.mNotifier = notifier; + this.mContext = context; + mLooper = Looper.myLooper(); + mCM = ci; + + setPropertiesByCarrier(); + + setUnitTestMode(unitTestMode); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); + mDnsCheckDisabled = sp.getBoolean(DNS_SERVER_CHECK_DISABLED_KEY, false); + mCM.setOnCallRing(this, EVENT_CALL_RING, null); + + /* "Voice capable" means that this device supports circuit-switched + * (i.e. voice) phone calls over the telephony network, and is allowed + * to display the in-call UI while a cellular voice call is active. + * This will be false on "data only" devices which can't make voice + * calls and don't support any in-call UI. + */ + mIsVoiceCapable = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_voice_capable); + + /** + * Some RIL's don't always send RIL_UNSOL_CALL_RING so it needs + * to be generated locally. Ideally all ring tones should be loops + * and this wouldn't be necessary. But to minimize changes to upper + * layers it is requested that it be generated by lower layers. + * + * By default old phones won't have the property set but do generate + * the RIL_UNSOL_CALL_RING so the default if there is no property is + * true. + */ + mDoesRilSendMultipleCallRing = SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_RIL_SENDS_MULTIPLE_CALL_RING, true); + Log.d(LOG_TAG, "mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing); + + mCallRingDelay = SystemProperties.getInt( + TelephonyProperties.PROPERTY_CALL_RING_DELAY, 3000); + Log.d(LOG_TAG, "mCallRingDelay=" + mCallRingDelay); + + // Initialize device storage and outgoing SMS usage monitors for SMSDispatchers. + mSmsStorageMonitor = new SmsStorageMonitor(this); + mSmsUsageMonitor = new SmsUsageMonitor(context); + } + + public void dispose() { + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + mCM.unSetOnCallRing(this); + // Must cleanup all connectionS and needs to use sendMessage! + mDataConnectionTracker.cleanUpAllConnections(null); + mIsTheCurrentActivePhone = false; + // Dispose the SMS usage and storage monitors + mSmsStorageMonitor.dispose(); + mSmsUsageMonitor.dispose(); + } + } + + public void removeReferences() { + mSmsStorageMonitor = null; + mSmsUsageMonitor = null; + mSMS = null; + mIccRecords = null; + mIccCard.set(null); + mDataConnectionTracker = null; + } + + /** + * When overridden the derived class needs to call + * super.handleMessage(msg) so this method has a + * a chance to process the message. + * + * @param msg + */ + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch(msg.what) { + case EVENT_CALL_RING: + Log.d(LOG_TAG, "Event EVENT_CALL_RING Received state=" + getState()); + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + PhoneConstants.State state = getState(); + if ((!mDoesRilSendMultipleCallRing) + && ((state == PhoneConstants.State.RINGING) || + (state == PhoneConstants.State.IDLE))) { + mCallRingContinueToken += 1; + sendIncomingCallRingNotification(mCallRingContinueToken); + } else { + notifyIncomingRing(); + } + } + break; + + case EVENT_CALL_RING_CONTINUE: + Log.d(LOG_TAG, "Event EVENT_CALL_RING_CONTINUE Received stat=" + getState()); + if (getState() == PhoneConstants.State.RINGING) { + sendIncomingCallRingNotification(msg.arg1); + } + break; + + default: + throw new RuntimeException("unexpected event not handled"); + } + } + + // Inherited documentation suffices. + public Context getContext() { + return mContext; + } + + /** + * Disables the DNS check (i.e., allows "0.0.0.0"). + * Useful for lab testing environment. + * @param b true disables the check, false enables. + */ + public void disableDnsCheck(boolean b) { + mDnsCheckDisabled = b; + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean(DNS_SERVER_CHECK_DISABLED_KEY, b); + editor.apply(); + } + + /** + * Returns true if the DNS check is currently disabled. + */ + public boolean isDnsCheckDisabled() { + return mDnsCheckDisabled; + } + + // Inherited documentation suffices. + public void registerForPreciseCallStateChanged(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mPreciseCallStateRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForPreciseCallStateChanged(Handler h) { + mPreciseCallStateRegistrants.remove(h); + } + + /** + * Subclasses of Phone probably want to replace this with a + * version scoped to their packages + */ + protected void notifyPreciseCallStateChangedP() { + AsyncResult ar = new AsyncResult(null, this, null); + mPreciseCallStateRegistrants.notifyRegistrants(ar); + } + + // Inherited documentation suffices. + public void registerForUnknownConnection(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mUnknownConnectionRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForUnknownConnection(Handler h) { + mUnknownConnectionRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForNewRingingConnection( + Handler h, int what, Object obj) { + checkCorrectThread(h); + + mNewRingingConnectionRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForNewRingingConnection(Handler h) { + mNewRingingConnectionRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj){ + mCM.registerForInCallVoicePrivacyOn(h,what,obj); + } + + // Inherited documentation suffices. + public void unregisterForInCallVoicePrivacyOn(Handler h){ + mCM.unregisterForInCallVoicePrivacyOn(h); + } + + // Inherited documentation suffices. + public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj){ + mCM.registerForInCallVoicePrivacyOff(h,what,obj); + } + + // Inherited documentation suffices. + public void unregisterForInCallVoicePrivacyOff(Handler h){ + mCM.unregisterForInCallVoicePrivacyOff(h); + } + + // Inherited documentation suffices. + public void registerForIncomingRing( + Handler h, int what, Object obj) { + checkCorrectThread(h); + + mIncomingRingRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForIncomingRing(Handler h) { + mIncomingRingRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForDisconnect(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mDisconnectRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForDisconnect(Handler h) { + mDisconnectRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForSuppServiceFailed(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mSuppServiceFailedRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForSuppServiceFailed(Handler h) { + mSuppServiceFailedRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForMmiInitiate(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mMmiRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForMmiInitiate(Handler h) { + mMmiRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForMmiComplete(Handler h, int what, Object obj) { + checkCorrectThread(h); + + mMmiCompleteRegistrants.addUnique(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForMmiComplete(Handler h) { + checkCorrectThread(h); + + mMmiCompleteRegistrants.remove(h); + } + + /** + * Method to retrieve the saved operator id from the Shared Preferences + */ + private String getSavedNetworkSelection() { + // open the shared preferences and search with our key. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sp.getString(NETWORK_SELECTION_KEY, ""); + } + + /** + * Method to restore the previously saved operator id, or reset to + * automatic selection, all depending upon the value in the shared + * preferences. + */ + public void restoreSavedNetworkSelection(Message response) { + // retrieve the operator id + String networkSelection = getSavedNetworkSelection(); + + // set to auto if the id is empty, otherwise select the network. + if (TextUtils.isEmpty(networkSelection)) { + mCM.setNetworkSelectionModeAutomatic(response); + } else { + mCM.setNetworkSelectionModeManual(networkSelection, response); + } + } + + // Inherited documentation suffices. + public void setUnitTestMode(boolean f) { + mUnitTestMode = f; + } + + // Inherited documentation suffices. + public boolean getUnitTestMode() { + return mUnitTestMode; + } + + /** + * To be invoked when a voice call Connection disconnects. + * + * Subclasses of Phone probably want to replace this with a + * version scoped to their packages + */ + protected void notifyDisconnectP(Connection cn) { + AsyncResult ar = new AsyncResult(null, cn, null); + mDisconnectRegistrants.notifyRegistrants(ar); + } + + // Inherited documentation suffices. + public void registerForServiceStateChanged( + Handler h, int what, Object obj) { + checkCorrectThread(h); + + mServiceStateRegistrants.add(h, what, obj); + } + + // Inherited documentation suffices. + public void unregisterForServiceStateChanged(Handler h) { + mServiceStateRegistrants.remove(h); + } + + // Inherited documentation suffices. + public void registerForRingbackTone(Handler h, int what, Object obj) { + mCM.registerForRingbackTone(h,what,obj); + } + + // Inherited documentation suffices. + public void unregisterForRingbackTone(Handler h) { + mCM.unregisterForRingbackTone(h); + } + + // Inherited documentation suffices. + public void registerForResendIncallMute(Handler h, int what, Object obj) { + mCM.registerForResendIncallMute(h,what,obj); + } + + // Inherited documentation suffices. + public void unregisterForResendIncallMute(Handler h) { + mCM.unregisterForResendIncallMute(h); + } + + public void setEchoSuppressionEnabled(boolean enabled) { + // no need for regular phone + } + + /** + * Subclasses of Phone probably want to replace this with a + * version scoped to their packages + */ + protected void notifyServiceStateChangedP(ServiceState ss) { + AsyncResult ar = new AsyncResult(null, ss, null); + mServiceStateRegistrants.notifyRegistrants(ar); + + mNotifier.notifyServiceState(this); + } + + // Inherited documentation suffices. + public SimulatedRadioControl getSimulatedRadioControl() { + return mSimulatedRadioControl; + } + + /** + * Verifies the current thread is the same as the thread originally + * used in the initialization of this instance. Throws RuntimeException + * if not. + * + * @exception RuntimeException if the current thread is not + * the thread that originally obtained this PhoneBase instance. + */ + private void checkCorrectThread(Handler h) { + if (h.getLooper() != mLooper) { + throw new RuntimeException( + "com.android.internal.telephony.Phone must be used from within one thread"); + } + } + + /** + * Set the properties by matching the carrier string in + * a string-array resource + */ + private void setPropertiesByCarrier() { + String carrier = SystemProperties.get("ro.carrier"); + + if (null == carrier || 0 == carrier.length() || "unknown".equals(carrier)) { + return; + } + + CharSequence[] carrierLocales = mContext. + getResources().getTextArray(R.array.carrier_properties); + + for (int i = 0; i < carrierLocales.length; i+=3) { + String c = carrierLocales[i].toString(); + if (carrier.equals(c)) { + String l = carrierLocales[i+1].toString(); + + String language = l.substring(0, 2); + String country = ""; + if (l.length() >=5) { + country = l.substring(3, 5); + } + MccTable.setSystemLocale(mContext, language, country); + + if (!country.isEmpty()) { + try { + Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.WIFI_COUNTRY_CODE); + } catch (Settings.SettingNotFoundException e) { + // note this is not persisting + WifiManager wM = (WifiManager) + mContext.getSystemService(Context.WIFI_SERVICE); + wM.setCountryCode(country, false); + } + } + return; + } + } + } + + /** + * Get state + */ + public abstract PhoneConstants.State getState(); + + /** + * Retrieves the IccFileHandler of the Phone instance + */ + public IccFileHandler getIccFileHandler(){ + IccCard iccCard = mIccCard.get(); + if (iccCard == null) return null; + return iccCard.getIccFileHandler(); + } + + /* + * Retrieves the Handler of the Phone instance + */ + public Handler getHandler() { + return this; + } + + /** + * Retrieves the ServiceStateTracker of the phone instance. + */ + public ServiceStateTracker getServiceStateTracker() { + return null; + } + + /** + * Get call tracker + */ + public CallTracker getCallTracker() { + return null; + } + + @Override + public IccCard getIccCard() { + return mIccCard.get(); + } + + @Override + public String getIccSerialNumber() { + return mIccRecords.iccid; + } + + @Override + public boolean getIccRecordsLoaded() { + return mIccRecords.getRecordsLoaded(); + } + + @Override + public boolean getMessageWaitingIndicator() { + return mIccRecords.getVoiceMessageWaiting(); + } + + @Override + public boolean getCallForwardingIndicator() { + return mIccRecords.getVoiceCallForwardingFlag(); + } + + /** + * Query the status of the CDMA roaming preference + */ + public void queryCdmaRoamingPreference(Message response) { + mCM.queryCdmaRoamingPreference(response); + } + + /** + * Set the status of the CDMA roaming preference + */ + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + mCM.setCdmaRoamingPreference(cdmaRoamingType, response); + } + + /** + * Set the status of the CDMA subscription mode + */ + public void setCdmaSubscription(int cdmaSubscriptionType, Message response) { + mCM.setCdmaSubscriptionSource(cdmaSubscriptionType, response); + } + + /** + * Set the preferred Network Type: Global, CDMA only or GSM/UMTS only + */ + public void setPreferredNetworkType(int networkType, Message response) { + mCM.setPreferredNetworkType(networkType, response); + } + + public void getPreferredNetworkType(Message response) { + mCM.getPreferredNetworkType(response); + } + + public void getSmscAddress(Message result) { + mCM.getSmscAddress(result); + } + + public void setSmscAddress(String address, Message result) { + mCM.setSmscAddress(address, result); + } + + public void setTTYMode(int ttyMode, Message onComplete) { + mCM.setTTYMode(ttyMode, onComplete); + } + + public void queryTTYMode(Message onComplete) { + mCM.queryTTYMode(onComplete); + } + + public void enableEnhancedVoicePrivacy(boolean enable, Message onComplete) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("enableEnhancedVoicePrivacy"); + } + + public void getEnhancedVoicePrivacy(Message onComplete) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("getEnhancedVoicePrivacy"); + } + + public void setBandMode(int bandMode, Message response) { + mCM.setBandMode(bandMode, response); + } + + public void queryAvailableBandMode(Message response) { + mCM.queryAvailableBandMode(response); + } + + public void invokeOemRilRequestRaw(byte[] data, Message response) { + mCM.invokeOemRilRequestRaw(data, response); + } + + public void invokeOemRilRequestStrings(String[] strings, Message response) { + mCM.invokeOemRilRequestStrings(strings, response); + } + + public void notifyDataActivity() { + mNotifier.notifyDataActivity(this); + } + + public void notifyMessageWaitingIndicator() { + // Do not notify voice mail waiting if device doesn't support voice + if (!mIsVoiceCapable) + return; + + // This function is added to send the notification to DefaultPhoneNotifier. + mNotifier.notifyMessageWaitingChanged(this); + } + + public void notifyDataConnection(String reason, String apnType, + PhoneConstants.DataState state) { + mNotifier.notifyDataConnection(this, reason, apnType, state); + } + + public void notifyDataConnection(String reason, String apnType) { + mNotifier.notifyDataConnection(this, reason, apnType, getDataConnectionState(apnType)); + } + + public void notifyDataConnection(String reason) { + String types[] = getActiveApnTypes(); + for (String apnType : types) { + mNotifier.notifyDataConnection(this, reason, apnType, getDataConnectionState(apnType)); + } + } + + public void notifyOtaspChanged(int otaspMode) { + mNotifier.notifyOtaspChanged(this, otaspMode); + } + + /** + * @return true if a mobile originating emergency call is active + */ + public boolean isInEmergencyCall() { + return false; + } + + /** + * @return true if we are in the emergency call back mode. This is a period where + * the phone should be using as little power as possible and be ready to receive an + * incoming call from the emergency operator. + */ + public boolean isInEcm() { + return false; + } + + public abstract String getPhoneName(); + + public abstract int getPhoneType(); + + /** @hide */ + public int getVoiceMessageCount(){ + return 0; + } + + /** + * Returns the CDMA ERI icon index to display + */ + public int getCdmaEriIconIndex() { + logUnexpectedCdmaMethodCall("getCdmaEriIconIndex"); + return -1; + } + + /** + * Returns the CDMA ERI icon mode, + * 0 - ON + * 1 - FLASHING + */ + public int getCdmaEriIconMode() { + logUnexpectedCdmaMethodCall("getCdmaEriIconMode"); + return -1; + } + + /** + * Returns the CDMA ERI text, + */ + public String getCdmaEriText() { + logUnexpectedCdmaMethodCall("getCdmaEriText"); + return "GSM nw, no ERI"; + } + + public String getCdmaMin() { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("getCdmaMin"); + return null; + } + + public boolean isMinInfoReady() { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("isMinInfoReady"); + return false; + } + + public String getCdmaPrlVersion(){ + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("getCdmaPrlVersion"); + return null; + } + + public void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("sendBurstDtmf"); + } + + public void exitEmergencyCallbackMode() { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("exitEmergencyCallbackMode"); + } + + public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("registerForCdmaOtaStatusChange"); + } + + public void unregisterForCdmaOtaStatusChange(Handler h) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("unregisterForCdmaOtaStatusChange"); + } + + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("registerForSubscriptionInfoReady"); + } + + public void unregisterForSubscriptionInfoReady(Handler h) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("unregisterForSubscriptionInfoReady"); + } + + /** + * Returns true if OTA Service Provisioning needs to be performed. + * If not overridden return false. + */ + public boolean needsOtaServiceProvisioning() { + return false; + } + + /** + * Return true if number is an OTASP number. + * If not overridden return false. + */ + public boolean isOtaSpNumber(String dialStr) { + return false; + } + + public void registerForCallWaiting(Handler h, int what, Object obj){ + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("registerForCallWaiting"); + } + + public void unregisterForCallWaiting(Handler h){ + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("unregisterForCallWaiting"); + } + + public void registerForEcmTimerReset(Handler h, int what, Object obj) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("registerForEcmTimerReset"); + } + + public void unregisterForEcmTimerReset(Handler h) { + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("unregisterForEcmTimerReset"); + } + + public void registerForSignalInfo(Handler h, int what, Object obj) { + mCM.registerForSignalInfo(h, what, obj); + } + + public void unregisterForSignalInfo(Handler h) { + mCM.unregisterForSignalInfo(h); + } + + public void registerForDisplayInfo(Handler h, int what, Object obj) { + mCM.registerForDisplayInfo(h, what, obj); + } + + public void unregisterForDisplayInfo(Handler h) { + mCM.unregisterForDisplayInfo(h); + } + + public void registerForNumberInfo(Handler h, int what, Object obj) { + mCM.registerForNumberInfo(h, what, obj); + } + + public void unregisterForNumberInfo(Handler h) { + mCM.unregisterForNumberInfo(h); + } + + public void registerForRedirectedNumberInfo(Handler h, int what, Object obj) { + mCM.registerForRedirectedNumberInfo(h, what, obj); + } + + public void unregisterForRedirectedNumberInfo(Handler h) { + mCM.unregisterForRedirectedNumberInfo(h); + } + + public void registerForLineControlInfo(Handler h, int what, Object obj) { + mCM.registerForLineControlInfo( h, what, obj); + } + + public void unregisterForLineControlInfo(Handler h) { + mCM.unregisterForLineControlInfo(h); + } + + public void registerFoT53ClirlInfo(Handler h, int what, Object obj) { + mCM.registerFoT53ClirlInfo(h, what, obj); + } + + public void unregisterForT53ClirInfo(Handler h) { + mCM.unregisterForT53ClirInfo(h); + } + + public void registerForT53AudioControlInfo(Handler h, int what, Object obj) { + mCM.registerForT53AudioControlInfo( h, what, obj); + } + + public void unregisterForT53AudioControlInfo(Handler h) { + mCM.unregisterForT53AudioControlInfo(h); + } + + public void setOnEcbModeExitResponse(Handler h, int what, Object obj){ + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("setOnEcbModeExitResponse"); + } + + public void unsetOnEcbModeExitResponse(Handler h){ + // This function should be overridden by the class CDMAPhone. Not implemented in GSMPhone. + logUnexpectedCdmaMethodCall("unsetOnEcbModeExitResponse"); + } + + public String[] getActiveApnTypes() { + return mDataConnectionTracker.getActiveApnTypes(); + } + + public String getActiveApnHost(String apnType) { + return mDataConnectionTracker.getActiveApnString(apnType); + } + + public LinkProperties getLinkProperties(String apnType) { + return mDataConnectionTracker.getLinkProperties(apnType); + } + + public LinkCapabilities getLinkCapabilities(String apnType) { + return mDataConnectionTracker.getLinkCapabilities(apnType); + } + + public int enableApnType(String type) { + return mDataConnectionTracker.enableApnType(type); + } + + public int disableApnType(String type) { + return mDataConnectionTracker.disableApnType(type); + } + + public boolean isDataConnectivityPossible() { + return isDataConnectivityPossible(PhoneConstants.APN_TYPE_DEFAULT); + } + + public boolean isDataConnectivityPossible(String apnType) { + return ((mDataConnectionTracker != null) && + (mDataConnectionTracker.isDataPossible(apnType))); + } + + /** + * Notify registrants of a new ringing Connection. + * Subclasses of Phone probably want to replace this with a + * version scoped to their packages + */ + protected void notifyNewRingingConnectionP(Connection cn) { + if (!mIsVoiceCapable) + return; + AsyncResult ar = new AsyncResult(null, cn, null); + mNewRingingConnectionRegistrants.notifyRegistrants(ar); + } + + /** + * Notify registrants of a RING event. + */ + private void notifyIncomingRing() { + if (!mIsVoiceCapable) + return; + AsyncResult ar = new AsyncResult(null, this, null); + mIncomingRingRegistrants.notifyRegistrants(ar); + } + + /** + * Send the incoming call Ring notification if conditions are right. + */ + private void sendIncomingCallRingNotification(int token) { + if (mIsVoiceCapable && !mDoesRilSendMultipleCallRing && + (token == mCallRingContinueToken)) { + Log.d(LOG_TAG, "Sending notifyIncomingRing"); + notifyIncomingRing(); + sendMessageDelayed( + obtainMessage(EVENT_CALL_RING_CONTINUE, token, 0), mCallRingDelay); + } else { + Log.d(LOG_TAG, "Ignoring ring notification request," + + " mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing + + " token=" + token + + " mCallRingContinueToken=" + mCallRingContinueToken + + " mIsVoiceCapable=" + mIsVoiceCapable); + } + } + + public boolean isCspPlmnEnabled() { + // This function should be overridden by the class GSMPhone. + // Not implemented in CDMAPhone. + logUnexpectedGsmMethodCall("isCspPlmnEnabled"); + return false; + } + + public IsimRecords getIsimRecords() { + Log.e(LOG_TAG, "getIsimRecords() is only supported on LTE devices"); + return null; + } + + public void requestIsimAuthentication(String nonce, Message result) { + Log.e(LOG_TAG, "requestIsimAuthentication() is only supported on LTE devices"); + } + + public String getMsisdn() { + logUnexpectedGsmMethodCall("getMsisdn"); + return null; + } + + /** + * Common error logger method for unexpected calls to CDMA-only methods. + */ + private static void logUnexpectedCdmaMethodCall(String name) + { + Log.e(LOG_TAG, "Error! " + name + "() in PhoneBase should not be " + + "called, CDMAPhone inactive."); + } + + public PhoneConstants.DataState getDataConnectionState() { + return getDataConnectionState(PhoneConstants.APN_TYPE_DEFAULT); + } + + /** + * Common error logger method for unexpected calls to GSM/WCDMA-only methods. + */ + private static void logUnexpectedGsmMethodCall(String name) { + Log.e(LOG_TAG, "Error! " + name + "() in PhoneBase should not be " + + "called, GSMPhone inactive."); + } + + // Called by SimRecords which is constructed with a PhoneBase instead of a GSMPhone. + public void notifyCallForwardingIndicator() { + // This function should be overridden by the class GSMPhone. Not implemented in CDMAPhone. + Log.e(LOG_TAG, "Error! This function should never be executed, inactive CDMAPhone."); + } + + public void notifyDataConnectionFailed(String reason, String apnType) { + mNotifier.notifyDataConnectionFailed(this, reason, apnType); + } + + /** + * {@inheritDoc} + */ + @Override + public int getLteOnCdmaMode() { + return mCM.getLteOnCdmaMode(); + } + + /** + * Sets the SIM voice message waiting indicator records. + * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported + * @param countWaiting The number of messages waiting, if known. Use + * -1 to indicate that an unknown number of + * messages are waiting + */ + @Override + public void setVoiceMessageWaiting(int line, int countWaiting) { + mIccRecords.setVoiceMessageWaiting(line, countWaiting); + } + + /** + * Gets the USIM service table from the UICC, if present and available. + * @return an interface to the UsimServiceTable record, or null if not available + */ + @Override + public UsimServiceTable getUsimServiceTable() { + return mIccRecords.getUsimServiceTable(); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("PhoneBase:"); + pw.println(" mCM=" + mCM); + pw.println(" mDnsCheckDisabled=" + mDnsCheckDisabled); + pw.println(" mDataConnectionTracker=" + mDataConnectionTracker); + pw.println(" mDoesRilSendMultipleCallRing=" + mDoesRilSendMultipleCallRing); + pw.println(" mCallRingContinueToken=" + mCallRingContinueToken); + pw.println(" mCallRingDelay=" + mCallRingDelay); + pw.println(" mIsTheCurrentActivePhone=" + mIsTheCurrentActivePhone); + pw.println(" mIsVoiceCapable=" + mIsVoiceCapable); + pw.println(" mIccRecords=" + mIccRecords); + pw.println(" mIccCard=" + mIccCard.get()); + pw.println(" mSmsStorageMonitor=" + mSmsStorageMonitor); + pw.println(" mSmsUsageMonitor=" + mSmsUsageMonitor); + pw.println(" mSMS=" + mSMS); + pw.flush(); + pw.println(" mLooper=" + mLooper); + pw.println(" mContext=" + mContext); + pw.println(" mNotifier=" + mNotifier); + pw.println(" mSimulatedRadioControl=" + mSimulatedRadioControl); + pw.println(" mUnitTestMode=" + mUnitTestMode); + pw.println(" isDnsCheckDisabled()=" + isDnsCheckDisabled()); + pw.println(" getUnitTestMode()=" + getUnitTestMode()); + pw.println(" getState()=" + getState()); + pw.println(" getIccSerialNumber()=" + getIccSerialNumber()); + pw.println(" getIccRecordsLoaded()=" + getIccRecordsLoaded()); + pw.println(" getMessageWaitingIndicator()=" + getMessageWaitingIndicator()); + pw.println(" getCallForwardingIndicator()=" + getCallForwardingIndicator()); + pw.println(" isInEmergencyCall()=" + isInEmergencyCall()); + pw.flush(); + pw.println(" isInEcm()=" + isInEcm()); + pw.println(" getPhoneName()=" + getPhoneName()); + pw.println(" getPhoneType()=" + getPhoneType()); + pw.println(" getVoiceMessageCount()=" + getVoiceMessageCount()); + pw.println(" getActiveApnTypes()=" + getActiveApnTypes()); + pw.println(" isDataConnectivityPossible()=" + isDataConnectivityPossible()); + pw.println(" needsOtaServiceProvisioning=" + needsOtaServiceProvisioning()); + } +} diff --git a/src/java/com/android/internal/telephony/PhoneFactory.java b/src/java/com/android/internal/telephony/PhoneFactory.java new file mode 100644 index 0000000..2c85dc6 --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneFactory.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.Context; +import android.net.LocalServerSocket; +import android.os.Looper; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.os.SystemProperties; + +import com.android.internal.telephony.cdma.CDMAPhone; +import com.android.internal.telephony.cdma.CDMALTEPhone; +import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager; +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.sip.SipPhone; +import com.android.internal.telephony.sip.SipPhoneFactory; + +/** + * {@hide} + */ +public class PhoneFactory { + static final String LOG_TAG = "PHONE"; + static final int SOCKET_OPEN_RETRY_MILLIS = 2 * 1000; + static final int SOCKET_OPEN_MAX_RETRY = 3; + + //***** Class Variables + + static private Phone sProxyPhone = null; + static private CommandsInterface sCommandsInterface = null; + + static private boolean sMadeDefaults = false; + static private PhoneNotifier sPhoneNotifier; + static private Looper sLooper; + static private Context sContext; + + static final int preferredCdmaSubscription = + CdmaSubscriptionSourceManager.PREFERRED_CDMA_SUBSCRIPTION; + + //***** Class Methods + + public static void makeDefaultPhones(Context context) { + makeDefaultPhone(context); + } + + /** + * FIXME replace this with some other way of making these + * instances + */ + public static void makeDefaultPhone(Context context) { + synchronized(Phone.class) { + if (!sMadeDefaults) { + sLooper = Looper.myLooper(); + sContext = context; + + if (sLooper == null) { + throw new RuntimeException( + "PhoneFactory.makeDefaultPhone must be called from Looper thread"); + } + + int retryCount = 0; + for(;;) { + boolean hasException = false; + retryCount ++; + + try { + // use UNIX domain socket to + // prevent subsequent initialization + new LocalServerSocket("com.android.internal.telephony"); + } catch (java.io.IOException ex) { + hasException = true; + } + + if ( !hasException ) { + break; + } else if (retryCount > SOCKET_OPEN_MAX_RETRY) { + throw new RuntimeException("PhoneFactory probably already running"); + } else { + try { + Thread.sleep(SOCKET_OPEN_RETRY_MILLIS); + } catch (InterruptedException er) { + } + } + } + + sPhoneNotifier = new DefaultPhoneNotifier(); + + // Get preferred network mode + int preferredNetworkMode = RILConstants.PREFERRED_NETWORK_MODE; + if (TelephonyManager.getLteOnCdmaModeStatic() == PhoneConstants.LTE_ON_CDMA_TRUE) { + preferredNetworkMode = Phone.NT_MODE_GLOBAL; + } + int networkMode = Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.PREFERRED_NETWORK_MODE, preferredNetworkMode); + Log.i(LOG_TAG, "Network Mode set to " + Integer.toString(networkMode)); + + // Get cdmaSubscription + // TODO: Change when the ril will provides a way to know at runtime + // the configuration, bug 4202572. And the ril issues the + // RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED, bug 4295439. + int cdmaSubscription; + int lteOnCdma = TelephonyManager.getLteOnCdmaModeStatic(); + switch (lteOnCdma) { + case PhoneConstants.LTE_ON_CDMA_FALSE: + cdmaSubscription = CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_NV; + Log.i(LOG_TAG, "lteOnCdma is 0 use SUBSCRIPTION_FROM_NV"); + break; + case PhoneConstants.LTE_ON_CDMA_TRUE: + cdmaSubscription = CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM; + Log.i(LOG_TAG, "lteOnCdma is 1 use SUBSCRIPTION_FROM_RUIM"); + break; + case PhoneConstants.LTE_ON_CDMA_UNKNOWN: + default: + //Get cdmaSubscription mode from Settings.System + cdmaSubscription = Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.PREFERRED_CDMA_SUBSCRIPTION, + preferredCdmaSubscription); + Log.i(LOG_TAG, "lteOnCdma not set, using PREFERRED_CDMA_SUBSCRIPTION"); + break; + } + Log.i(LOG_TAG, "Cdma Subscription set to " + cdmaSubscription); + + //reads the system properties and makes commandsinterface + sCommandsInterface = new RIL(context, networkMode, cdmaSubscription); + + int phoneType = TelephonyManager.getPhoneType(networkMode); + if (phoneType == PhoneConstants.PHONE_TYPE_GSM) { + Log.i(LOG_TAG, "Creating GSMPhone"); + sProxyPhone = new PhoneProxy(new GSMPhone(context, + sCommandsInterface, sPhoneNotifier)); + } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { + switch (TelephonyManager.getLteOnCdmaModeStatic()) { + case PhoneConstants.LTE_ON_CDMA_TRUE: + Log.i(LOG_TAG, "Creating CDMALTEPhone"); + sProxyPhone = new PhoneProxy(new CDMALTEPhone(context, + sCommandsInterface, sPhoneNotifier)); + break; + case PhoneConstants.LTE_ON_CDMA_FALSE: + default: + Log.i(LOG_TAG, "Creating CDMAPhone"); + sProxyPhone = new PhoneProxy(new CDMAPhone(context, + sCommandsInterface, sPhoneNotifier)); + break; + } + } + + sMadeDefaults = true; + } + } + } + + public static Phone getDefaultPhone() { + if (sLooper != Looper.myLooper()) { + throw new RuntimeException( + "PhoneFactory.getDefaultPhone must be called from Looper thread"); + } + + if (!sMadeDefaults) { + throw new IllegalStateException("Default phones haven't been made yet!"); + } + return sProxyPhone; + } + + public static Phone getCdmaPhone() { + Phone phone; + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + switch (TelephonyManager.getLteOnCdmaModeStatic()) { + case PhoneConstants.LTE_ON_CDMA_TRUE: { + phone = new CDMALTEPhone(sContext, sCommandsInterface, sPhoneNotifier); + break; + } + case PhoneConstants.LTE_ON_CDMA_FALSE: + case PhoneConstants.LTE_ON_CDMA_UNKNOWN: + default: { + phone = new CDMAPhone(sContext, sCommandsInterface, sPhoneNotifier); + break; + } + } + } + return phone; + } + + public static Phone getGsmPhone() { + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + Phone phone = new GSMPhone(sContext, sCommandsInterface, sPhoneNotifier); + return phone; + } + } + + /** + * Makes a {@link SipPhone} object. + * @param sipUri the local SIP URI the phone runs on + * @return the {@code SipPhone} object or null if the SIP URI is not valid + */ + public static SipPhone makeSipPhone(String sipUri) { + return SipPhoneFactory.makePhone(sipUri, sContext, sPhoneNotifier); + } +} diff --git a/src/java/com/android/internal/telephony/PhoneNotifier.java b/src/java/com/android/internal/telephony/PhoneNotifier.java new file mode 100644 index 0000000..efc7a13 --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneNotifier.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.telephony.CellInfo; + +/** + * {@hide} + */ +public interface PhoneNotifier { + + public void notifyPhoneState(Phone sender); + + public void notifyServiceState(Phone sender); + + public void notifyCellLocation(Phone sender); + + public void notifySignalStrength(Phone sender); + + public void notifyMessageWaitingChanged(Phone sender); + + public void notifyCallForwardingChanged(Phone sender); + + /** TODO - reason should never be null */ + public void notifyDataConnection(Phone sender, String reason, String apnType, + PhoneConstants.DataState state); + + public void notifyDataConnectionFailed(Phone sender, String reason, String apnType); + + public void notifyDataActivity(Phone sender); + + public void notifyOtaspChanged(Phone sender, int otaspMode); + + // TODO - trigger notifyCellInfo from ServiceStateTracker + public void notifyCellInfo(Phone sender, CellInfo cellInfo); +} diff --git a/src/java/com/android/internal/telephony/PhoneProxy.java b/src/java/com/android/internal/telephony/PhoneProxy.java new file mode 100644 index 0000000..77135d4 --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneProxy.java @@ -0,0 +1,972 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + + +import android.app.ActivityManagerNative; +import android.content.Context; +import android.content.Intent; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.telephony.CellLocation; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.util.Log; + +import com.android.internal.telephony.cdma.CDMAPhone; +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.gsm.UsimServiceTable; +import com.android.internal.telephony.ims.IsimRecords; +import com.android.internal.telephony.test.SimulatedRadioControl; +import com.android.internal.telephony.CallManager; + +import java.util.List; + +public class PhoneProxy extends Handler implements Phone { + public final static Object lockForRadioTechnologyChange = new Object(); + + private Phone mActivePhone; + private CommandsInterface mCommandsInterface; + private IccSmsInterfaceManagerProxy mIccSmsInterfaceManagerProxy; + private IccPhoneBookInterfaceManagerProxy mIccPhoneBookInterfaceManagerProxy; + private PhoneSubInfoProxy mPhoneSubInfoProxy; + + private boolean mResetModemOnRadioTechnologyChange = false; + + private int mRilVersion; + + private static final int EVENT_VOICE_RADIO_TECH_CHANGED = 1; + private static final int EVENT_RADIO_ON = 2; + private static final int EVENT_REQUEST_VOICE_RADIO_TECH_DONE = 3; + private static final int EVENT_RIL_CONNECTED = 4; + + private static final String LOG_TAG = "PHONE"; + + //***** Class Methods + public PhoneProxy(Phone phone) { + mActivePhone = phone; + mResetModemOnRadioTechnologyChange = SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_RESET_ON_RADIO_TECH_CHANGE, false); + mIccSmsInterfaceManagerProxy = new IccSmsInterfaceManagerProxy( + phone.getIccSmsInterfaceManager()); + mIccPhoneBookInterfaceManagerProxy = new IccPhoneBookInterfaceManagerProxy( + phone.getIccPhoneBookInterfaceManager()); + mPhoneSubInfoProxy = new PhoneSubInfoProxy(phone.getPhoneSubInfo()); + mCommandsInterface = ((PhoneBase)mActivePhone).mCM; + + mCommandsInterface.registerForRilConnected(this, EVENT_RIL_CONNECTED, null); + mCommandsInterface.registerForOn(this, EVENT_RADIO_ON, null); + mCommandsInterface.registerForVoiceRadioTechChanged( + this, EVENT_VOICE_RADIO_TECH_CHANGED, null); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar = (AsyncResult) msg.obj; + switch(msg.what) { + case EVENT_RADIO_ON: + /* Proactively query voice radio technologies */ + mCommandsInterface.getVoiceRadioTechnology( + this.obtainMessage(EVENT_REQUEST_VOICE_RADIO_TECH_DONE)); + break; + + case EVENT_RIL_CONNECTED: + if (ar.exception == null && ar.result != null) { + mRilVersion = (Integer) ar.result; + } else { + logd("Unexpected exception on EVENT_RIL_CONNECTED"); + mRilVersion = -1; + } + break; + + case EVENT_VOICE_RADIO_TECH_CHANGED: + case EVENT_REQUEST_VOICE_RADIO_TECH_DONE: + + if (ar.exception == null) { + if ((ar.result != null) && (((int[]) ar.result).length != 0)) { + int newVoiceTech = ((int[]) ar.result)[0]; + updatePhoneObject(newVoiceTech); + } else { + loge("Voice Radio Technology event " + msg.what + " has no tech!"); + } + } else { + loge("Voice Radio Technology event " + msg.what + " exception!" + ar.exception); + } + break; + + default: + loge("Error! This handler was not registered for this message type. Message: " + + msg.what); + break; + } + super.handleMessage(msg); + } + + private static void logd(String msg) { + Log.d(LOG_TAG, "[PhoneProxy] " + msg); + } + + private void logw(String msg) { + Log.w(LOG_TAG, "[PhoneProxy] " + msg); + } + + private void loge(String msg) { + Log.e(LOG_TAG, "[PhoneProxy] " + msg); + } + + private void updatePhoneObject(int newVoiceRadioTech) { + + if (mActivePhone != null) { + if(mRilVersion == 6 && getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE) { + /* + * On v6 RIL, when LTE_ON_CDMA is TRUE, always create CDMALTEPhone + * irrespective of the voice radio tech reported. + */ + if (mActivePhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + logd("LTE ON CDMA property is set. Use CDMA Phone" + + " newVoiceRadioTech = " + newVoiceRadioTech + + " Active Phone = " + mActivePhone.getPhoneName()); + return; + } else { + logd("LTE ON CDMA property is set. Switch to CDMALTEPhone" + + " newVoiceRadioTech = " + newVoiceRadioTech + + " Active Phone = " + mActivePhone.getPhoneName()); + newVoiceRadioTech = ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT; + } + } else { + if ((ServiceState.isCdma(newVoiceRadioTech) && + mActivePhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) || + (ServiceState.isGsm(newVoiceRadioTech) && + mActivePhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)) { + // Nothing changed. Keep phone as it is. + logd("Ignoring voice radio technology changed message." + + " newVoiceRadioTech = " + newVoiceRadioTech + + " Active Phone = " + mActivePhone.getPhoneName()); + return; + } + } + } + + if (newVoiceRadioTech == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) { + // We need some voice phone object to be active always, so never + // delete the phone without anything to replace it with! + logd("Ignoring voice radio technology changed message. newVoiceRadioTech = Unknown." + + " Active Phone = " + mActivePhone.getPhoneName()); + return; + } + + boolean oldPowerState = false; // old power state to off + if (mResetModemOnRadioTechnologyChange) { + if (mCommandsInterface.getRadioState().isOn()) { + oldPowerState = true; + logd("Setting Radio Power to Off"); + mCommandsInterface.setRadioPower(false, null); + } + } + + deleteAndCreatePhone(newVoiceRadioTech); + + if (mResetModemOnRadioTechnologyChange && oldPowerState) { // restore power state + logd("Resetting Radio"); + mCommandsInterface.setRadioPower(oldPowerState, null); + } + + // Set the new interfaces in the proxy's + mIccSmsInterfaceManagerProxy.setmIccSmsInterfaceManager( + mActivePhone.getIccSmsInterfaceManager()); + mIccPhoneBookInterfaceManagerProxy.setmIccPhoneBookInterfaceManager(mActivePhone + .getIccPhoneBookInterfaceManager()); + mPhoneSubInfoProxy.setmPhoneSubInfo(this.mActivePhone.getPhoneSubInfo()); + + mCommandsInterface = ((PhoneBase)mActivePhone).mCM; + + // Send an Intent to the PhoneApp that we had a radio technology change + Intent intent = new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra(PhoneConstants.PHONE_NAME_KEY, mActivePhone.getPhoneName()); + ActivityManagerNative.broadcastStickyIntent(intent, null); + + } + + private void deleteAndCreatePhone(int newVoiceRadioTech) { + + String outgoingPhoneName = "Unknown"; + Phone oldPhone = mActivePhone; + + if (oldPhone != null) { + outgoingPhoneName = ((PhoneBase) oldPhone).getPhoneName(); + } + + logd("Switching Voice Phone : " + outgoingPhoneName + " >>> " + + (ServiceState.isGsm(newVoiceRadioTech) ? "GSM" : "CDMA")); + + if (oldPhone != null) { + CallManager.getInstance().unregisterPhone(oldPhone); + logd("Disposing old phone.."); + oldPhone.dispose(); + } + + // Give the garbage collector a hint to start the garbage collection + // asap NOTE this has been disabled since radio technology change could + // happen during e.g. a multimedia playing and could slow the system. + // Tests needs to be done to see the effects of the GC call here when + // system is busy. + // System.gc(); + + if (ServiceState.isCdma(newVoiceRadioTech)) { + mActivePhone = PhoneFactory.getCdmaPhone(); + } else if (ServiceState.isGsm(newVoiceRadioTech)) { + mActivePhone = PhoneFactory.getGsmPhone(); + } + + if (oldPhone != null) { + oldPhone.removeReferences(); + } + + if(mActivePhone != null) { + CallManager.getInstance().registerPhone(mActivePhone); + } + + oldPhone = null; + } + + public ServiceState getServiceState() { + return mActivePhone.getServiceState(); + } + + public CellLocation getCellLocation() { + return mActivePhone.getCellLocation(); + } + + public PhoneConstants.DataState getDataConnectionState() { + return mActivePhone.getDataConnectionState(PhoneConstants.APN_TYPE_DEFAULT); + } + + public PhoneConstants.DataState getDataConnectionState(String apnType) { + return mActivePhone.getDataConnectionState(apnType); + } + + public DataActivityState getDataActivityState() { + return mActivePhone.getDataActivityState(); + } + + public Context getContext() { + return mActivePhone.getContext(); + } + + public void disableDnsCheck(boolean b) { + mActivePhone.disableDnsCheck(b); + } + + public boolean isDnsCheckDisabled() { + return mActivePhone.isDnsCheckDisabled(); + } + + public PhoneConstants.State getState() { + return mActivePhone.getState(); + } + + public String getPhoneName() { + return mActivePhone.getPhoneName(); + } + + public int getPhoneType() { + return mActivePhone.getPhoneType(); + } + + public String[] getActiveApnTypes() { + return mActivePhone.getActiveApnTypes(); + } + + public String getActiveApnHost(String apnType) { + return mActivePhone.getActiveApnHost(apnType); + } + + public LinkProperties getLinkProperties(String apnType) { + return mActivePhone.getLinkProperties(apnType); + } + + public LinkCapabilities getLinkCapabilities(String apnType) { + return mActivePhone.getLinkCapabilities(apnType); + } + + public SignalStrength getSignalStrength() { + return mActivePhone.getSignalStrength(); + } + + public void registerForUnknownConnection(Handler h, int what, Object obj) { + mActivePhone.registerForUnknownConnection(h, what, obj); + } + + public void unregisterForUnknownConnection(Handler h) { + mActivePhone.unregisterForUnknownConnection(h); + } + + public void registerForPreciseCallStateChanged(Handler h, int what, Object obj) { + mActivePhone.registerForPreciseCallStateChanged(h, what, obj); + } + + public void unregisterForPreciseCallStateChanged(Handler h) { + mActivePhone.unregisterForPreciseCallStateChanged(h); + } + + public void registerForNewRingingConnection(Handler h, int what, Object obj) { + mActivePhone.registerForNewRingingConnection(h, what, obj); + } + + public void unregisterForNewRingingConnection(Handler h) { + mActivePhone.unregisterForNewRingingConnection(h); + } + + public void registerForIncomingRing(Handler h, int what, Object obj) { + mActivePhone.registerForIncomingRing(h, what, obj); + } + + public void unregisterForIncomingRing(Handler h) { + mActivePhone.unregisterForIncomingRing(h); + } + + public void registerForDisconnect(Handler h, int what, Object obj) { + mActivePhone.registerForDisconnect(h, what, obj); + } + + public void unregisterForDisconnect(Handler h) { + mActivePhone.unregisterForDisconnect(h); + } + + public void registerForMmiInitiate(Handler h, int what, Object obj) { + mActivePhone.registerForMmiInitiate(h, what, obj); + } + + public void unregisterForMmiInitiate(Handler h) { + mActivePhone.unregisterForMmiInitiate(h); + } + + public void registerForMmiComplete(Handler h, int what, Object obj) { + mActivePhone.registerForMmiComplete(h, what, obj); + } + + public void unregisterForMmiComplete(Handler h) { + mActivePhone.unregisterForMmiComplete(h); + } + + public List<? extends MmiCode> getPendingMmiCodes() { + return mActivePhone.getPendingMmiCodes(); + } + + public void sendUssdResponse(String ussdMessge) { + mActivePhone.sendUssdResponse(ussdMessge); + } + + public void registerForServiceStateChanged(Handler h, int what, Object obj) { + mActivePhone.registerForServiceStateChanged(h, what, obj); + } + + public void unregisterForServiceStateChanged(Handler h) { + mActivePhone.unregisterForServiceStateChanged(h); + } + + public void registerForSuppServiceNotification(Handler h, int what, Object obj) { + mActivePhone.registerForSuppServiceNotification(h, what, obj); + } + + public void unregisterForSuppServiceNotification(Handler h) { + mActivePhone.unregisterForSuppServiceNotification(h); + } + + public void registerForSuppServiceFailed(Handler h, int what, Object obj) { + mActivePhone.registerForSuppServiceFailed(h, what, obj); + } + + public void unregisterForSuppServiceFailed(Handler h) { + mActivePhone.unregisterForSuppServiceFailed(h); + } + + public void registerForInCallVoicePrivacyOn(Handler h, int what, Object obj){ + mActivePhone.registerForInCallVoicePrivacyOn(h,what,obj); + } + + public void unregisterForInCallVoicePrivacyOn(Handler h){ + mActivePhone.unregisterForInCallVoicePrivacyOn(h); + } + + public void registerForInCallVoicePrivacyOff(Handler h, int what, Object obj){ + mActivePhone.registerForInCallVoicePrivacyOff(h,what,obj); + } + + public void unregisterForInCallVoicePrivacyOff(Handler h){ + mActivePhone.unregisterForInCallVoicePrivacyOff(h); + } + + public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj) { + mActivePhone.registerForCdmaOtaStatusChange(h,what,obj); + } + + public void unregisterForCdmaOtaStatusChange(Handler h) { + mActivePhone.unregisterForCdmaOtaStatusChange(h); + } + + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) { + mActivePhone.registerForSubscriptionInfoReady(h, what, obj); + } + + public void unregisterForSubscriptionInfoReady(Handler h) { + mActivePhone.unregisterForSubscriptionInfoReady(h); + } + + public void registerForEcmTimerReset(Handler h, int what, Object obj) { + mActivePhone.registerForEcmTimerReset(h,what,obj); + } + + public void unregisterForEcmTimerReset(Handler h) { + mActivePhone.unregisterForEcmTimerReset(h); + } + + public void registerForRingbackTone(Handler h, int what, Object obj) { + mActivePhone.registerForRingbackTone(h,what,obj); + } + + public void unregisterForRingbackTone(Handler h) { + mActivePhone.unregisterForRingbackTone(h); + } + + public void registerForResendIncallMute(Handler h, int what, Object obj) { + mActivePhone.registerForResendIncallMute(h,what,obj); + } + + public void unregisterForResendIncallMute(Handler h) { + mActivePhone.unregisterForResendIncallMute(h); + } + + public boolean getIccRecordsLoaded() { + return mActivePhone.getIccRecordsLoaded(); + } + + public IccCard getIccCard() { + return mActivePhone.getIccCard(); + } + + public void acceptCall() throws CallStateException { + mActivePhone.acceptCall(); + } + + public void rejectCall() throws CallStateException { + mActivePhone.rejectCall(); + } + + public void switchHoldingAndActive() throws CallStateException { + mActivePhone.switchHoldingAndActive(); + } + + public boolean canConference() { + return mActivePhone.canConference(); + } + + public void conference() throws CallStateException { + mActivePhone.conference(); + } + + public void enableEnhancedVoicePrivacy(boolean enable, Message onComplete) { + mActivePhone.enableEnhancedVoicePrivacy(enable, onComplete); + } + + public void getEnhancedVoicePrivacy(Message onComplete) { + mActivePhone.getEnhancedVoicePrivacy(onComplete); + } + + public boolean canTransfer() { + return mActivePhone.canTransfer(); + } + + public void explicitCallTransfer() throws CallStateException { + mActivePhone.explicitCallTransfer(); + } + + public void clearDisconnected() { + mActivePhone.clearDisconnected(); + } + + public Call getForegroundCall() { + return mActivePhone.getForegroundCall(); + } + + public Call getBackgroundCall() { + return mActivePhone.getBackgroundCall(); + } + + public Call getRingingCall() { + return mActivePhone.getRingingCall(); + } + + public Connection dial(String dialString) throws CallStateException { + return mActivePhone.dial(dialString); + } + + public Connection dial(String dialString, UUSInfo uusInfo) throws CallStateException { + return mActivePhone.dial(dialString, uusInfo); + } + + public boolean handlePinMmi(String dialString) { + return mActivePhone.handlePinMmi(dialString); + } + + public boolean handleInCallMmiCommands(String command) throws CallStateException { + return mActivePhone.handleInCallMmiCommands(command); + } + + public void sendDtmf(char c) { + mActivePhone.sendDtmf(c); + } + + public void startDtmf(char c) { + mActivePhone.startDtmf(c); + } + + public void stopDtmf() { + mActivePhone.stopDtmf(); + } + + public void setRadioPower(boolean power) { + mActivePhone.setRadioPower(power); + } + + public boolean getMessageWaitingIndicator() { + return mActivePhone.getMessageWaitingIndicator(); + } + + public boolean getCallForwardingIndicator() { + return mActivePhone.getCallForwardingIndicator(); + } + + public String getLine1Number() { + return mActivePhone.getLine1Number(); + } + + public String getCdmaMin() { + return mActivePhone.getCdmaMin(); + } + + public boolean isMinInfoReady() { + return mActivePhone.isMinInfoReady(); + } + + public String getCdmaPrlVersion() { + return mActivePhone.getCdmaPrlVersion(); + } + + public String getLine1AlphaTag() { + return mActivePhone.getLine1AlphaTag(); + } + + public void setLine1Number(String alphaTag, String number, Message onComplete) { + mActivePhone.setLine1Number(alphaTag, number, onComplete); + } + + public String getVoiceMailNumber() { + return mActivePhone.getVoiceMailNumber(); + } + + /** @hide */ + public int getVoiceMessageCount(){ + return mActivePhone.getVoiceMessageCount(); + } + + public String getVoiceMailAlphaTag() { + return mActivePhone.getVoiceMailAlphaTag(); + } + + public void setVoiceMailNumber(String alphaTag,String voiceMailNumber, + Message onComplete) { + mActivePhone.setVoiceMailNumber(alphaTag, voiceMailNumber, onComplete); + } + + public void getCallForwardingOption(int commandInterfaceCFReason, + Message onComplete) { + mActivePhone.getCallForwardingOption(commandInterfaceCFReason, + onComplete); + } + + public void setCallForwardingOption(int commandInterfaceCFReason, + int commandInterfaceCFAction, String dialingNumber, + int timerSeconds, Message onComplete) { + mActivePhone.setCallForwardingOption(commandInterfaceCFReason, + commandInterfaceCFAction, dialingNumber, timerSeconds, onComplete); + } + + public void getOutgoingCallerIdDisplay(Message onComplete) { + mActivePhone.getOutgoingCallerIdDisplay(onComplete); + } + + public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, + Message onComplete) { + mActivePhone.setOutgoingCallerIdDisplay(commandInterfaceCLIRMode, + onComplete); + } + + public void getCallWaiting(Message onComplete) { + mActivePhone.getCallWaiting(onComplete); + } + + public void setCallWaiting(boolean enable, Message onComplete) { + mActivePhone.setCallWaiting(enable, onComplete); + } + + public void getAvailableNetworks(Message response) { + mActivePhone.getAvailableNetworks(response); + } + + public void setNetworkSelectionModeAutomatic(Message response) { + mActivePhone.setNetworkSelectionModeAutomatic(response); + } + + public void selectNetworkManually(OperatorInfo network, Message response) { + mActivePhone.selectNetworkManually(network, response); + } + + public void setPreferredNetworkType(int networkType, Message response) { + mActivePhone.setPreferredNetworkType(networkType, response); + } + + public void getPreferredNetworkType(Message response) { + mActivePhone.getPreferredNetworkType(response); + } + + public void getNeighboringCids(Message response) { + mActivePhone.getNeighboringCids(response); + } + + public void setOnPostDialCharacter(Handler h, int what, Object obj) { + mActivePhone.setOnPostDialCharacter(h, what, obj); + } + + public void setMute(boolean muted) { + mActivePhone.setMute(muted); + } + + public boolean getMute() { + return mActivePhone.getMute(); + } + + public void setEchoSuppressionEnabled(boolean enabled) { + mActivePhone.setEchoSuppressionEnabled(enabled); + } + + public void invokeOemRilRequestRaw(byte[] data, Message response) { + mActivePhone.invokeOemRilRequestRaw(data, response); + } + + public void invokeOemRilRequestStrings(String[] strings, Message response) { + mActivePhone.invokeOemRilRequestStrings(strings, response); + } + + public void getDataCallList(Message response) { + mActivePhone.getDataCallList(response); + } + + public void updateServiceLocation() { + mActivePhone.updateServiceLocation(); + } + + public void enableLocationUpdates() { + mActivePhone.enableLocationUpdates(); + } + + public void disableLocationUpdates() { + mActivePhone.disableLocationUpdates(); + } + + public void setUnitTestMode(boolean f) { + mActivePhone.setUnitTestMode(f); + } + + public boolean getUnitTestMode() { + return mActivePhone.getUnitTestMode(); + } + + public void setBandMode(int bandMode, Message response) { + mActivePhone.setBandMode(bandMode, response); + } + + public void queryAvailableBandMode(Message response) { + mActivePhone.queryAvailableBandMode(response); + } + + public boolean getDataRoamingEnabled() { + return mActivePhone.getDataRoamingEnabled(); + } + + public void setDataRoamingEnabled(boolean enable) { + mActivePhone.setDataRoamingEnabled(enable); + } + + public void queryCdmaRoamingPreference(Message response) { + mActivePhone.queryCdmaRoamingPreference(response); + } + + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + mActivePhone.setCdmaRoamingPreference(cdmaRoamingType, response); + } + + public void setCdmaSubscription(int cdmaSubscriptionType, Message response) { + mActivePhone.setCdmaSubscription(cdmaSubscriptionType, response); + } + + public SimulatedRadioControl getSimulatedRadioControl() { + return mActivePhone.getSimulatedRadioControl(); + } + + public int enableApnType(String type) { + return mActivePhone.enableApnType(type); + } + + public int disableApnType(String type) { + return mActivePhone.disableApnType(type); + } + + public boolean isDataConnectivityPossible() { + return mActivePhone.isDataConnectivityPossible(PhoneConstants.APN_TYPE_DEFAULT); + } + + public boolean isDataConnectivityPossible(String apnType) { + return mActivePhone.isDataConnectivityPossible(apnType); + } + + public String getDeviceId() { + return mActivePhone.getDeviceId(); + } + + public String getDeviceSvn() { + return mActivePhone.getDeviceSvn(); + } + + public String getSubscriberId() { + return mActivePhone.getSubscriberId(); + } + + public String getIccSerialNumber() { + return mActivePhone.getIccSerialNumber(); + } + + public String getEsn() { + return mActivePhone.getEsn(); + } + + public String getMeid() { + return mActivePhone.getMeid(); + } + + public String getMsisdn() { + return mActivePhone.getMsisdn(); + } + + public String getImei() { + return mActivePhone.getImei(); + } + + public PhoneSubInfo getPhoneSubInfo(){ + return mActivePhone.getPhoneSubInfo(); + } + + public IccSmsInterfaceManager getIccSmsInterfaceManager(){ + return mActivePhone.getIccSmsInterfaceManager(); + } + + public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){ + return mActivePhone.getIccPhoneBookInterfaceManager(); + } + + public void setTTYMode(int ttyMode, Message onComplete) { + mActivePhone.setTTYMode(ttyMode, onComplete); + } + + public void queryTTYMode(Message onComplete) { + mActivePhone.queryTTYMode(onComplete); + } + + public void activateCellBroadcastSms(int activate, Message response) { + mActivePhone.activateCellBroadcastSms(activate, response); + } + + public void getCellBroadcastSmsConfig(Message response) { + mActivePhone.getCellBroadcastSmsConfig(response); + } + + public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response) { + mActivePhone.setCellBroadcastSmsConfig(configValuesArray, response); + } + + public void notifyDataActivity() { + mActivePhone.notifyDataActivity(); + } + + public void getSmscAddress(Message result) { + mActivePhone.getSmscAddress(result); + } + + public void setSmscAddress(String address, Message result) { + mActivePhone.setSmscAddress(address, result); + } + + public int getCdmaEriIconIndex() { + return mActivePhone.getCdmaEriIconIndex(); + } + + public String getCdmaEriText() { + return mActivePhone.getCdmaEriText(); + } + + public int getCdmaEriIconMode() { + return mActivePhone.getCdmaEriIconMode(); + } + + public Phone getActivePhone() { + return mActivePhone; + } + + public void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete){ + mActivePhone.sendBurstDtmf(dtmfString, on, off, onComplete); + } + + public void exitEmergencyCallbackMode(){ + mActivePhone.exitEmergencyCallbackMode(); + } + + public boolean needsOtaServiceProvisioning(){ + return mActivePhone.needsOtaServiceProvisioning(); + } + + public boolean isOtaSpNumber(String dialStr){ + return mActivePhone.isOtaSpNumber(dialStr); + } + + public void registerForCallWaiting(Handler h, int what, Object obj){ + mActivePhone.registerForCallWaiting(h,what,obj); + } + + public void unregisterForCallWaiting(Handler h){ + mActivePhone.unregisterForCallWaiting(h); + } + + public void registerForSignalInfo(Handler h, int what, Object obj) { + mActivePhone.registerForSignalInfo(h,what,obj); + } + + public void unregisterForSignalInfo(Handler h) { + mActivePhone.unregisterForSignalInfo(h); + } + + public void registerForDisplayInfo(Handler h, int what, Object obj) { + mActivePhone.registerForDisplayInfo(h,what,obj); + } + + public void unregisterForDisplayInfo(Handler h) { + mActivePhone.unregisterForDisplayInfo(h); + } + + public void registerForNumberInfo(Handler h, int what, Object obj) { + mActivePhone.registerForNumberInfo(h, what, obj); + } + + public void unregisterForNumberInfo(Handler h) { + mActivePhone.unregisterForNumberInfo(h); + } + + public void registerForRedirectedNumberInfo(Handler h, int what, Object obj) { + mActivePhone.registerForRedirectedNumberInfo(h, what, obj); + } + + public void unregisterForRedirectedNumberInfo(Handler h) { + mActivePhone.unregisterForRedirectedNumberInfo(h); + } + + public void registerForLineControlInfo(Handler h, int what, Object obj) { + mActivePhone.registerForLineControlInfo( h, what, obj); + } + + public void unregisterForLineControlInfo(Handler h) { + mActivePhone.unregisterForLineControlInfo(h); + } + + public void registerFoT53ClirlInfo(Handler h, int what, Object obj) { + mActivePhone.registerFoT53ClirlInfo(h, what, obj); + } + + public void unregisterForT53ClirInfo(Handler h) { + mActivePhone.unregisterForT53ClirInfo(h); + } + + public void registerForT53AudioControlInfo(Handler h, int what, Object obj) { + mActivePhone.registerForT53AudioControlInfo( h, what, obj); + } + + public void unregisterForT53AudioControlInfo(Handler h) { + mActivePhone.unregisterForT53AudioControlInfo(h); + } + + public void setOnEcbModeExitResponse(Handler h, int what, Object obj){ + mActivePhone.setOnEcbModeExitResponse(h,what,obj); + } + + public void unsetOnEcbModeExitResponse(Handler h){ + mActivePhone.unsetOnEcbModeExitResponse(h); + } + + public boolean isCspPlmnEnabled() { + return mActivePhone.isCspPlmnEnabled(); + } + + public IsimRecords getIsimRecords() { + return mActivePhone.getIsimRecords(); + } + + public void requestIsimAuthentication(String nonce, Message response) { + mActivePhone.requestIsimAuthentication(nonce, response); + } + + /** + * {@inheritDoc} + */ + @Override + public int getLteOnCdmaMode() { + return mActivePhone.getLteOnCdmaMode(); + } + + @Override + public void setVoiceMessageWaiting(int line, int countWaiting) { + mActivePhone.setVoiceMessageWaiting(line, countWaiting); + } + + @Override + public UsimServiceTable getUsimServiceTable() { + return mActivePhone.getUsimServiceTable(); + } + + public void dispose() { + mCommandsInterface.unregisterForOn(this); + mCommandsInterface.unregisterForVoiceRadioTechChanged(this); + mCommandsInterface.unregisterForRilConnected(this); + } + + public void removeReferences() { + mActivePhone = null; + mCommandsInterface = null; + } +} diff --git a/src/java/com/android/internal/telephony/PhoneStateIntentReceiver.java b/src/java/com/android/internal/telephony/PhoneStateIntentReceiver.java new file mode 100644 index 0000000..89084ac --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneStateIntentReceiver.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Message; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.TelephonyManager; +import android.util.Log; + +/** + * + * DO NOT USE THIS CLASS: + * + * Use android.telephony.TelephonyManager and PhoneStateListener instead. + * + * + */ +@Deprecated +public final class PhoneStateIntentReceiver extends BroadcastReceiver { + private static final String LOG_TAG = "PHONE"; + private static final boolean DBG = false; + + private static final int NOTIF_PHONE = 1 << 0; + private static final int NOTIF_SERVICE = 1 << 1; + private static final int NOTIF_SIGNAL = 1 << 2; + + private static final int NOTIF_MAX = 1 << 5; + + PhoneConstants.State mPhoneState = PhoneConstants.State.IDLE; + ServiceState mServiceState = new ServiceState(); + SignalStrength mSignalStrength = new SignalStrength(); + + private Context mContext; + private Handler mTarget; + private IntentFilter mFilter; + private int mWants; + private int mPhoneStateEventWhat; + private int mServiceStateEventWhat; + private int mLocationEventWhat; + private int mAsuEventWhat; + + public PhoneStateIntentReceiver() { + super(); + mFilter = new IntentFilter(); + } + + public PhoneStateIntentReceiver(Context context, Handler target) { + this(); + setContext(context); + setTarget(target); + } + + public void setContext(Context c) { + mContext = c; + } + + public void setTarget(Handler h) { + mTarget = h; + } + + public PhoneConstants.State getPhoneState() { + if ((mWants & NOTIF_PHONE) == 0) { + throw new RuntimeException + ("client must call notifyPhoneCallState(int)"); + } + return mPhoneState; + } + + public ServiceState getServiceState() { + if ((mWants & NOTIF_SERVICE) == 0) { + throw new RuntimeException + ("client must call notifyServiceState(int)"); + } + return mServiceState; + } + + /** + * Returns current signal strength in as an asu 0..31 + * + * Throws RuntimeException if client has not called notifySignalStrength() + */ + public int getSignalStrengthLevelAsu() { + // TODO: use new SignalStrength instead of asu + if ((mWants & NOTIF_SIGNAL) == 0) { + throw new RuntimeException + ("client must call notifySignalStrength(int)"); + } + return mSignalStrength.getAsuLevel(); + } + + /** + * Return current signal strength in "dBm", ranging from -113 - -51dBm + * or -1 if unknown + * + * @return signal strength in dBm, -1 if not yet updated + * Throws RuntimeException if client has not called notifySignalStrength() + */ + public int getSignalStrengthDbm() { + if ((mWants & NOTIF_SIGNAL) == 0) { + throw new RuntimeException + ("client must call notifySignalStrength(int)"); + } + return mSignalStrength.getDbm(); + } + + public void notifyPhoneCallState(int eventWhat) { + mWants |= NOTIF_PHONE; + mPhoneStateEventWhat = eventWhat; + mFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + } + + public boolean getNotifyPhoneCallState() { + return ((mWants & NOTIF_PHONE) != 0); + } + + public void notifyServiceState(int eventWhat) { + mWants |= NOTIF_SERVICE; + mServiceStateEventWhat = eventWhat; + mFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED); + } + + public boolean getNotifyServiceState() { + return ((mWants & NOTIF_SERVICE) != 0); + } + + public void notifySignalStrength (int eventWhat) { + mWants |= NOTIF_SIGNAL; + mAsuEventWhat = eventWhat; + mFilter.addAction(TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED); + } + + public boolean getNotifySignalStrength() { + return ((mWants & NOTIF_SIGNAL) != 0); + } + + public void registerIntent() { + mContext.registerReceiver(this, mFilter); + } + + public void unregisterIntent() { + mContext.unregisterReceiver(this); + } + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + + try { + if (TelephonyIntents.ACTION_SIGNAL_STRENGTH_CHANGED.equals(action)) { + mSignalStrength = SignalStrength.newFromBundle(intent.getExtras()); + + if (mTarget != null && getNotifySignalStrength()) { + Message message = Message.obtain(mTarget, mAsuEventWhat); + mTarget.sendMessage(message); + } + } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) { + if (DBG) Log.d(LOG_TAG, "onReceiveIntent: ACTION_PHONE_STATE_CHANGED, state=" + + intent.getStringExtra(PhoneConstants.STATE_KEY)); + String phoneState = intent.getStringExtra(PhoneConstants.STATE_KEY); + mPhoneState = (PhoneConstants.State) Enum.valueOf( + PhoneConstants.State.class, phoneState); + + if (mTarget != null && getNotifyPhoneCallState()) { + Message message = Message.obtain(mTarget, + mPhoneStateEventWhat); + mTarget.sendMessage(message); + } + } else if (TelephonyIntents.ACTION_SERVICE_STATE_CHANGED.equals(action)) { + mServiceState = ServiceState.newFromBundle(intent.getExtras()); + + if (mTarget != null && getNotifyServiceState()) { + Message message = Message.obtain(mTarget, + mServiceStateEventWhat); + mTarget.sendMessage(message); + } + } + } catch (Exception ex) { + Log.e(LOG_TAG, "[PhoneStateIntentRecv] caught " + ex); + ex.printStackTrace(); + } + } + +} diff --git a/src/java/com/android/internal/telephony/PhoneSubInfo.java b/src/java/com/android/internal/telephony/PhoneSubInfo.java new file mode 100755 index 0000000..e8449ce --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneSubInfo.java @@ -0,0 +1,206 @@ +/* + * 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 com.android.internal.telephony; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.Binder; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.android.internal.telephony.ims.IsimRecords; + +public class PhoneSubInfo extends IPhoneSubInfo.Stub { + static final String LOG_TAG = "PHONE"; + private Phone mPhone; + private Context mContext; + private static final String READ_PHONE_STATE = + android.Manifest.permission.READ_PHONE_STATE; + // TODO: change getCompleteVoiceMailNumber() to require READ_PRIVILEGED_PHONE_STATE + private static final String CALL_PRIVILEGED = + android.Manifest.permission.CALL_PRIVILEGED; + private static final String READ_PRIVILEGED_PHONE_STATE = + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE; + + public PhoneSubInfo(Phone phone) { + mPhone = phone; + mContext = phone.getContext(); + } + + public void dispose() { + } + + protected void finalize() { + try { + super.finalize(); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Error while finalizing:", throwable); + } + Log.d(LOG_TAG, "PhoneSubInfo finalized"); + } + + /** + * Retrieves the unique device ID, e.g., IMEI for GSM phones and MEID for CDMA phones. + */ + public String getDeviceId() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getDeviceId(); + } + + /** + * Retrieves the software version number for the device, e.g., IMEI/SV + * for GSM phones. + */ + public String getDeviceSvn() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getDeviceSvn(); + } + + /** + * Retrieves the unique subscriber ID, e.g., IMSI for GSM phones. + */ + public String getSubscriberId() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getSubscriberId(); + } + + /** + * Retrieves the serial number of the ICC, if applicable. + */ + public String getIccSerialNumber() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getIccSerialNumber(); + } + + /** + * Retrieves the phone number string for line 1. + */ + public String getLine1Number() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getLine1Number(); + } + + /** + * Retrieves the alpha identifier for line 1. + */ + public String getLine1AlphaTag() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return (String) mPhone.getLine1AlphaTag(); + } + + /** + * Retrieves the MSISDN string. + */ + public String getMsisdn() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return mPhone.getMsisdn(); + } + + /** + * Retrieves the voice mail number. + */ + public String getVoiceMailNumber() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + String number = PhoneNumberUtils.extractNetworkPortion(mPhone.getVoiceMailNumber()); + Log.d(LOG_TAG, "VM: PhoneSubInfo.getVoiceMailNUmber: "); // + number); + return number; + } + + /** + * Retrieves the complete voice mail number. + * + * @hide + */ + public String getCompleteVoiceMailNumber() { + mContext.enforceCallingOrSelfPermission(CALL_PRIVILEGED, + "Requires CALL_PRIVILEGED"); + String number = mPhone.getVoiceMailNumber(); + Log.d(LOG_TAG, "VM: PhoneSubInfo.getCompleteVoiceMailNUmber: "); // + number); + return number; + } + + /** + * Retrieves the alpha identifier associated with the voice mail number. + */ + public String getVoiceMailAlphaTag() { + mContext.enforceCallingOrSelfPermission(READ_PHONE_STATE, "Requires READ_PHONE_STATE"); + return (String) mPhone.getVoiceMailAlphaTag(); + } + + /** + * Returns the IMS private user identity (IMPI) that was loaded from the ISIM. + * @return the IMPI, or null if not present or not loaded + */ + public String getIsimImpi() { + mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE, + "Requires READ_PRIVILEGED_PHONE_STATE"); + IsimRecords isim = mPhone.getIsimRecords(); + if (isim != null) { + return isim.getIsimImpi(); + } else { + return null; + } + } + + /** + * Returns the IMS home network domain name that was loaded from the ISIM. + * @return the IMS domain name, or null if not present or not loaded + */ + public String getIsimDomain() { + mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE, + "Requires READ_PRIVILEGED_PHONE_STATE"); + IsimRecords isim = mPhone.getIsimRecords(); + if (isim != null) { + return isim.getIsimDomain(); + } else { + return null; + } + } + + /** + * Returns the IMS public user identities (IMPU) that were loaded from the ISIM. + * @return an array of IMPU strings, with one IMPU per string, or null if + * not present or not loaded + */ + public String[] getIsimImpu() { + mContext.enforceCallingOrSelfPermission(READ_PRIVILEGED_PHONE_STATE, + "Requires READ_PRIVILEGED_PHONE_STATE"); + IsimRecords isim = mPhone.getIsimRecords(); + if (isim != null) { + return isim.getIsimImpu(); + } else { + return null; + } + } + + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) + != PackageManager.PERMISSION_GRANTED) { + pw.println("Permission Denial: can't dump PhoneSubInfo from from pid=" + + Binder.getCallingPid() + + ", uid=" + Binder.getCallingUid()); + return; + } + + pw.println("Phone Subscriber Info:"); + pw.println(" Phone Type = " + mPhone.getPhoneName()); + pw.println(" Device ID = " + mPhone.getDeviceId()); + } + +} diff --git a/src/java/com/android/internal/telephony/PhoneSubInfoProxy.java b/src/java/com/android/internal/telephony/PhoneSubInfoProxy.java new file mode 100755 index 0000000..bd130de --- /dev/null +++ b/src/java/com/android/internal/telephony/PhoneSubInfoProxy.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +import android.content.pm.PackageManager; +import android.os.Binder; +import android.os.ServiceManager; + + +public class PhoneSubInfoProxy extends IPhoneSubInfo.Stub { + private PhoneSubInfo mPhoneSubInfo; + + public PhoneSubInfoProxy(PhoneSubInfo phoneSubInfo) { + mPhoneSubInfo = phoneSubInfo; + if(ServiceManager.getService("iphonesubinfo") == null) { + ServiceManager.addService("iphonesubinfo", this); + } + } + + public void setmPhoneSubInfo(PhoneSubInfo phoneSubInfo) { + this.mPhoneSubInfo = phoneSubInfo; + } + + public String getDeviceId() { + return mPhoneSubInfo.getDeviceId(); + } + + public String getDeviceSvn() { + return mPhoneSubInfo.getDeviceSvn(); + } + + /** + * Retrieves the unique subscriber ID, e.g., IMSI for GSM phones. + */ + public String getSubscriberId() { + return mPhoneSubInfo.getSubscriberId(); + } + + /** + * Retrieves the serial number of the ICC, if applicable. + */ + public String getIccSerialNumber() { + return mPhoneSubInfo.getIccSerialNumber(); + } + + /** + * Retrieves the phone number string for line 1. + */ + public String getLine1Number() { + return mPhoneSubInfo.getLine1Number(); + } + + /** + * Retrieves the alpha identifier for line 1. + */ + public String getLine1AlphaTag() { + return mPhoneSubInfo.getLine1AlphaTag(); + } + + /** + * Retrieves the MSISDN Number. + */ + public String getMsisdn() { + return mPhoneSubInfo.getMsisdn(); + } + + /** + * Retrieves the voice mail number. + */ + public String getVoiceMailNumber() { + return mPhoneSubInfo.getVoiceMailNumber(); + } + + /** + * Retrieves the complete voice mail number. + */ + public String getCompleteVoiceMailNumber() { + return mPhoneSubInfo.getCompleteVoiceMailNumber(); + } + + /** + * Retrieves the alpha identifier associated with the voice mail number. + */ + public String getVoiceMailAlphaTag() { + return mPhoneSubInfo.getVoiceMailAlphaTag(); + } + + /** + * Returns the IMS private user identity (IMPI) that was loaded from the ISIM. + * @return the IMPI, or null if not present or not loaded + */ + public String getIsimImpi() { + return mPhoneSubInfo.getIsimImpi(); + } + + /** + * Returns the IMS home network domain name that was loaded from the ISIM. + * @return the IMS domain name, or null if not present or not loaded + */ + public String getIsimDomain() { + return mPhoneSubInfo.getIsimDomain(); + } + + /** + * Returns the IMS public user identities (IMPU) that were loaded from the ISIM. + * @return an array of IMPU strings, with one IMPU per string, or null if + * not present or not loaded + */ + public String[] getIsimImpu() { + return mPhoneSubInfo.getIsimImpu(); + } + + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mPhoneSubInfo.dump(fd, pw, args); + } +} diff --git a/src/java/com/android/internal/telephony/RIL.java b/src/java/com/android/internal/telephony/RIL.java new file mode 100644 index 0000000..b14f6c8 --- /dev/null +++ b/src/java/com/android/internal/telephony/RIL.java @@ -0,0 +1,3861 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import static com.android.internal.telephony.RILConstants.*; +import static android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN; +import static android.telephony.TelephonyManager.NETWORK_TYPE_EDGE; +import static android.telephony.TelephonyManager.NETWORK_TYPE_GPRS; +import static android.telephony.TelephonyManager.NETWORK_TYPE_UMTS; +import static android.telephony.TelephonyManager.NETWORK_TYPE_HSDPA; +import static android.telephony.TelephonyManager.NETWORK_TYPE_HSUPA; +import static android.telephony.TelephonyManager.NETWORK_TYPE_HSPA; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.PowerManager; +import android.os.SystemProperties; +import android.os.PowerManager.WakeLock; +import android.telephony.NeighboringCellInfo; +import android.telephony.PhoneNumberUtils; +import android.telephony.SmsManager; +import android.telephony.SmsMessage; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; +import com.android.internal.telephony.gsm.SuppServiceNotification; +import com.android.internal.telephony.cdma.CdmaCallWaitingNotification; +import com.android.internal.telephony.cdma.CdmaInformationRecords; +import com.android.internal.telephony.IccRefreshResponse; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * {@hide} + */ +class RILRequest { + static final String LOG_TAG = "RILJ"; + + //***** Class Variables + static int sNextSerial = 0; + static Object sSerialMonitor = new Object(); + private static Object sPoolSync = new Object(); + private static RILRequest sPool = null; + private static int sPoolSize = 0; + private static final int MAX_POOL_SIZE = 4; + + //***** Instance Variables + int mSerial; + int mRequest; + Message mResult; + Parcel mp; + RILRequest mNext; + + /** + * Retrieves a new RILRequest instance from the pool. + * + * @param request RIL_REQUEST_* + * @param result sent when operation completes + * @return a RILRequest instance from the pool. + */ + static RILRequest obtain(int request, Message result) { + RILRequest rr = null; + + synchronized(sPoolSync) { + if (sPool != null) { + rr = sPool; + sPool = rr.mNext; + rr.mNext = null; + sPoolSize--; + } + } + + if (rr == null) { + rr = new RILRequest(); + } + + synchronized(sSerialMonitor) { + rr.mSerial = sNextSerial++; + } + rr.mRequest = request; + rr.mResult = result; + rr.mp = Parcel.obtain(); + + if (result != null && result.getTarget() == null) { + throw new NullPointerException("Message target must not be null"); + } + + // first elements in any RIL Parcel + rr.mp.writeInt(request); + rr.mp.writeInt(rr.mSerial); + + return rr; + } + + /** + * Returns a RILRequest instance to the pool. + * + * Note: This should only be called once per use. + */ + void release() { + synchronized (sPoolSync) { + if (sPoolSize < MAX_POOL_SIZE) { + this.mNext = sPool; + sPool = this; + sPoolSize++; + mResult = null; + } + } + } + + private RILRequest() { + } + + static void + resetSerial() { + synchronized(sSerialMonitor) { + sNextSerial = 0; + } + } + + String + serialString() { + //Cheesy way to do %04d + StringBuilder sb = new StringBuilder(8); + String sn; + + sn = Integer.toString(mSerial); + + //sb.append("J["); + sb.append('['); + for (int i = 0, s = sn.length() ; i < 4 - s; i++) { + sb.append('0'); + } + + sb.append(sn); + sb.append(']'); + return sb.toString(); + } + + void + onError(int error, Object ret) { + CommandException ex; + + ex = CommandException.fromRilErrno(error); + + if (RIL.RILJ_LOGD) Log.d(LOG_TAG, serialString() + "< " + + RIL.requestToString(mRequest) + + " error: " + ex); + + if (mResult != null) { + AsyncResult.forMessage(mResult, ret, ex); + mResult.sendToTarget(); + } + + if (mp != null) { + mp.recycle(); + mp = null; + } + } +} + + +/** + * RIL implementation of the CommandsInterface. + * FIXME public only for testing + * + * {@hide} + */ +public final class RIL extends BaseCommands implements CommandsInterface { + static final String LOG_TAG = "RILJ"; + static final boolean RILJ_LOGD = true; + static final boolean RILJ_LOGV = false; // STOP SHIP if true + + /** + * Wake lock timeout should be longer than the longest timeout in + * the vendor ril. + */ + private static final int DEFAULT_WAKE_LOCK_TIMEOUT = 60000; + + //***** Instance Variables + + LocalSocket mSocket; + HandlerThread mSenderThread; + RILSender mSender; + Thread mReceiverThread; + RILReceiver mReceiver; + WakeLock mWakeLock; + int mWakeLockTimeout; + // The number of requests pending to be sent out, it increases before calling + // EVENT_SEND and decreases while handling EVENT_SEND. It gets cleared while + // WAKE_LOCK_TIMEOUT occurs. + int mRequestMessagesPending; + // The number of requests sent out but waiting for response. It increases while + // sending request and decreases while handling response. It should match + // mRequestList.size() unless there are requests no replied while + // WAKE_LOCK_TIMEOUT occurs. + int mRequestMessagesWaiting; + + //I'd rather this be LinkedList or something + ArrayList<RILRequest> mRequestsList = new ArrayList<RILRequest>(); + + Object mLastNITZTimeInfo; + + // When we are testing emergency calls + AtomicBoolean mTestingEmergencyCall = new AtomicBoolean(false); + + //***** Events + + static final int EVENT_SEND = 1; + static final int EVENT_WAKE_LOCK_TIMEOUT = 2; + + //***** Constants + + // match with constant in ril.cpp + static final int RIL_MAX_COMMAND_BYTES = (8 * 1024); + static final int RESPONSE_SOLICITED = 0; + static final int RESPONSE_UNSOLICITED = 1; + + static final String SOCKET_NAME_RIL = "rild"; + + static final int SOCKET_OPEN_RETRY_MILLIS = 4 * 1000; + + // The number of the required config values for broadcast SMS stored in the C struct + // RIL_CDMA_BroadcastServiceInfo + private static final int CDMA_BSI_NO_OF_INTS_STRUCT = 3; + + private static final int CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES = 31; + + BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) { + sendScreenState(true); + } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { + sendScreenState(false); + } else { + Log.w(LOG_TAG, "RIL received unexpected Intent: " + intent.getAction()); + } + } + }; + + class RILSender extends Handler implements Runnable { + public RILSender(Looper looper) { + super(looper); + } + + // Only allocated once + byte[] dataLength = new byte[4]; + + //***** Runnable implementation + public void + run() { + //setup if needed + } + + + //***** Handler implementation + @Override public void + handleMessage(Message msg) { + RILRequest rr = (RILRequest)(msg.obj); + RILRequest req = null; + + switch (msg.what) { + case EVENT_SEND: + /** + * mRequestMessagePending++ already happened for every + * EVENT_SEND, thus we must make sure + * mRequestMessagePending-- happens once and only once + */ + boolean alreadySubtracted = false; + try { + LocalSocket s; + + s = mSocket; + + if (s == null) { + rr.onError(RADIO_NOT_AVAILABLE, null); + rr.release(); + if (mRequestMessagesPending > 0) + mRequestMessagesPending--; + alreadySubtracted = true; + return; + } + + synchronized (mRequestsList) { + mRequestsList.add(rr); + mRequestMessagesWaiting++; + } + + if (mRequestMessagesPending > 0) + mRequestMessagesPending--; + alreadySubtracted = true; + + byte[] data; + + data = rr.mp.marshall(); + rr.mp.recycle(); + rr.mp = null; + + if (data.length > RIL_MAX_COMMAND_BYTES) { + throw new RuntimeException( + "Parcel larger than max bytes allowed! " + + data.length); + } + + // parcel length in big endian + dataLength[0] = dataLength[1] = 0; + dataLength[2] = (byte)((data.length >> 8) & 0xff); + dataLength[3] = (byte)((data.length) & 0xff); + + //Log.v(LOG_TAG, "writing packet: " + data.length + " bytes"); + + s.getOutputStream().write(dataLength); + s.getOutputStream().write(data); + } catch (IOException ex) { + Log.e(LOG_TAG, "IOException", ex); + req = findAndRemoveRequestFromList(rr.mSerial); + // make sure this request has not already been handled, + // eg, if RILReceiver cleared the list. + if (req != null || !alreadySubtracted) { + rr.onError(RADIO_NOT_AVAILABLE, null); + rr.release(); + } + } catch (RuntimeException exc) { + Log.e(LOG_TAG, "Uncaught exception ", exc); + req = findAndRemoveRequestFromList(rr.mSerial); + // make sure this request has not already been handled, + // eg, if RILReceiver cleared the list. + if (req != null || !alreadySubtracted) { + rr.onError(GENERIC_FAILURE, null); + rr.release(); + } + } finally { + // Note: We are "Done" only if there are no outstanding + // requests or replies. Thus this code path will only release + // the wake lock on errors. + releaseWakeLockIfDone(); + } + + if (!alreadySubtracted && mRequestMessagesPending > 0) { + mRequestMessagesPending--; + } + + break; + + case EVENT_WAKE_LOCK_TIMEOUT: + // Haven't heard back from the last request. Assume we're + // not getting a response and release the wake lock. + synchronized (mWakeLock) { + if (mWakeLock.isHeld()) { + // The timer of WAKE_LOCK_TIMEOUT is reset with each + // new send request. So when WAKE_LOCK_TIMEOUT occurs + // all requests in mRequestList already waited at + // least DEFAULT_WAKE_LOCK_TIMEOUT but no response. + // Reset mRequestMessagesWaiting to enable + // releaseWakeLockIfDone(). + // + // Note: Keep mRequestList so that delayed response + // can still be handled when response finally comes. + if (mRequestMessagesWaiting != 0) { + Log.d(LOG_TAG, "NOTE: mReqWaiting is NOT 0 but" + + mRequestMessagesWaiting + " at TIMEOUT, reset!" + + " There still msg waitng for response"); + + mRequestMessagesWaiting = 0; + + if (RILJ_LOGD) { + synchronized (mRequestsList) { + int count = mRequestsList.size(); + Log.d(LOG_TAG, "WAKE_LOCK_TIMEOUT " + + " mRequestList=" + count); + + for (int i = 0; i < count; i++) { + rr = mRequestsList.get(i); + Log.d(LOG_TAG, i + ": [" + rr.mSerial + "] " + + requestToString(rr.mRequest)); + } + } + } + } + // mRequestMessagesPending shows how many + // requests are waiting to be sent (and before + // to be added in request list) since star the + // WAKE_LOCK_TIMEOUT timer. Since WAKE_LOCK_TIMEOUT + // is the expected time to get response, all requests + // should already sent out (i.e. + // mRequestMessagesPending is 0 )while TIMEOUT occurs. + if (mRequestMessagesPending != 0) { + Log.e(LOG_TAG, "ERROR: mReqPending is NOT 0 but" + + mRequestMessagesPending + " at TIMEOUT, reset!"); + mRequestMessagesPending = 0; + + } + mWakeLock.release(); + } + } + break; + } + } + } + + /** + * Reads in a single RIL message off the wire. A RIL message consists + * of a 4-byte little-endian length and a subsequent series of bytes. + * The final message (length header omitted) is read into + * <code>buffer</code> and the length of the final message (less header) + * is returned. A return value of -1 indicates end-of-stream. + * + * @param is non-null; Stream to read from + * @param buffer Buffer to fill in. Must be as large as maximum + * message size, or an ArrayOutOfBounds exception will be thrown. + * @return Length of message less header, or -1 on end of stream. + * @throws IOException + */ + private static int readRilMessage(InputStream is, byte[] buffer) + throws IOException { + int countRead; + int offset; + int remaining; + int messageLength; + + // First, read in the length of the message + offset = 0; + remaining = 4; + do { + countRead = is.read(buffer, offset, remaining); + + if (countRead < 0 ) { + Log.e(LOG_TAG, "Hit EOS reading message length"); + return -1; + } + + offset += countRead; + remaining -= countRead; + } while (remaining > 0); + + messageLength = ((buffer[0] & 0xff) << 24) + | ((buffer[1] & 0xff) << 16) + | ((buffer[2] & 0xff) << 8) + | (buffer[3] & 0xff); + + // Then, re-use the buffer and read in the message itself + offset = 0; + remaining = messageLength; + do { + countRead = is.read(buffer, offset, remaining); + + if (countRead < 0 ) { + Log.e(LOG_TAG, "Hit EOS reading message. messageLength=" + messageLength + + " remaining=" + remaining); + return -1; + } + + offset += countRead; + remaining -= countRead; + } while (remaining > 0); + + return messageLength; + } + + class RILReceiver implements Runnable { + byte[] buffer; + + RILReceiver() { + buffer = new byte[RIL_MAX_COMMAND_BYTES]; + } + + public void + run() { + int retryCount = 0; + + try {for (;;) { + LocalSocket s = null; + LocalSocketAddress l; + + try { + s = new LocalSocket(); + l = new LocalSocketAddress(SOCKET_NAME_RIL, + LocalSocketAddress.Namespace.RESERVED); + s.connect(l); + } catch (IOException ex){ + try { + if (s != null) { + s.close(); + } + } catch (IOException ex2) { + //ignore failure to close after failure to connect + } + + // don't print an error message after the the first time + // or after the 8th time + + if (retryCount == 8) { + Log.e (LOG_TAG, + "Couldn't find '" + SOCKET_NAME_RIL + + "' socket after " + retryCount + + " times, continuing to retry silently"); + } else if (retryCount > 0 && retryCount < 8) { + Log.i (LOG_TAG, + "Couldn't find '" + SOCKET_NAME_RIL + + "' socket; retrying after timeout"); + } + + try { + Thread.sleep(SOCKET_OPEN_RETRY_MILLIS); + } catch (InterruptedException er) { + } + + retryCount++; + continue; + } + + retryCount = 0; + + mSocket = s; + Log.i(LOG_TAG, "Connected to '" + SOCKET_NAME_RIL + "' socket"); + + int length = 0; + try { + InputStream is = mSocket.getInputStream(); + + for (;;) { + Parcel p; + + length = readRilMessage(is, buffer); + + if (length < 0) { + // End-of-stream reached + break; + } + + p = Parcel.obtain(); + p.unmarshall(buffer, 0, length); + p.setDataPosition(0); + + //Log.v(LOG_TAG, "Read packet: " + length + " bytes"); + + processResponse(p); + p.recycle(); + } + } catch (java.io.IOException ex) { + Log.i(LOG_TAG, "'" + SOCKET_NAME_RIL + "' socket closed", + ex); + } catch (Throwable tr) { + Log.e(LOG_TAG, "Uncaught exception read length=" + length + + "Exception:" + tr.toString()); + } + + Log.i(LOG_TAG, "Disconnected from '" + SOCKET_NAME_RIL + + "' socket"); + + setRadioState (RadioState.RADIO_UNAVAILABLE); + + try { + mSocket.close(); + } catch (IOException ex) { + } + + mSocket = null; + RILRequest.resetSerial(); + + // Clear request list on close + clearRequestsList(RADIO_NOT_AVAILABLE, false); + }} catch (Throwable tr) { + Log.e(LOG_TAG,"Uncaught exception", tr); + } + + /* We're disconnected so we don't know the ril version */ + notifyRegistrantsRilConnectionChanged(-1); + } + } + + + + //***** Constructors + + public RIL(Context context, int preferredNetworkType, int cdmaSubscription) { + super(context); + if (RILJ_LOGD) { + riljLog("RIL(context, preferredNetworkType=" + preferredNetworkType + + " cdmaSubscription=" + cdmaSubscription + ")"); + } + mCdmaSubscription = cdmaSubscription; + mPreferredNetworkType = preferredNetworkType; + mPhoneType = RILConstants.NO_PHONE; + + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); + mWakeLock.setReferenceCounted(false); + mWakeLockTimeout = SystemProperties.getInt(TelephonyProperties.PROPERTY_WAKE_LOCK_TIMEOUT, + DEFAULT_WAKE_LOCK_TIMEOUT); + mRequestMessagesPending = 0; + mRequestMessagesWaiting = 0; + + mSenderThread = new HandlerThread("RILSender"); + mSenderThread.start(); + + Looper looper = mSenderThread.getLooper(); + mSender = new RILSender(looper); + + ConnectivityManager cm = (ConnectivityManager)context.getSystemService( + Context.CONNECTIVITY_SERVICE); + if (cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) { + riljLog("Not starting RILReceiver: wifi-only"); + } else { + riljLog("Starting RILReceiver"); + mReceiver = new RILReceiver(); + mReceiverThread = new Thread(mReceiver, "RILReceiver"); + mReceiverThread.start(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_SCREEN_OFF); + context.registerReceiver(mIntentReceiver, filter); + } + } + + //***** CommandsInterface implementation + + public void getVoiceRadioTechnology(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_VOICE_RADIO_TECH, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + + @Override public void + setOnNITZTime(Handler h, int what, Object obj) { + super.setOnNITZTime(h, what, obj); + + // Send the last NITZ time if we have it + if (mLastNITZTimeInfo != null) { + mNITZTimeRegistrant + .notifyRegistrant( + new AsyncResult (null, mLastNITZTimeInfo, null)); + mLastNITZTimeInfo = null; + } + } + + public void + getIccCardStatus(Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_SIM_STATUS, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + @Override public void + supplyIccPin(String pin, Message result) { + supplyIccPinForApp(pin, null, result); + } + + @Override public void + supplyIccPinForApp(String pin, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ENTER_SIM_PIN, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(2); + rr.mp.writeString(pin); + rr.mp.writeString(aid); + + send(rr); + } + + @Override public void + supplyIccPuk(String puk, String newPin, Message result) { + supplyIccPukForApp(puk, newPin, null, result); + } + + @Override public void + supplyIccPukForApp(String puk, String newPin, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ENTER_SIM_PUK, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(3); + rr.mp.writeString(puk); + rr.mp.writeString(newPin); + rr.mp.writeString(aid); + + send(rr); + } + + @Override public void + supplyIccPin2(String pin, Message result) { + supplyIccPin2ForApp(pin, null, result); + } + + @Override public void + supplyIccPin2ForApp(String pin, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ENTER_SIM_PIN2, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(2); + rr.mp.writeString(pin); + rr.mp.writeString(aid); + + send(rr); + } + + @Override public void + supplyIccPuk2(String puk2, String newPin2, Message result) { + supplyIccPuk2ForApp(puk2, newPin2, null, result); + } + + @Override public void + supplyIccPuk2ForApp(String puk, String newPin2, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ENTER_SIM_PUK2, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(3); + rr.mp.writeString(puk); + rr.mp.writeString(newPin2); + rr.mp.writeString(aid); + + send(rr); + } + + @Override public void + changeIccPin(String oldPin, String newPin, Message result) { + changeIccPinForApp(oldPin, newPin, null, result); + } + + @Override public void + changeIccPinForApp(String oldPin, String newPin, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CHANGE_SIM_PIN, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(3); + rr.mp.writeString(oldPin); + rr.mp.writeString(newPin); + rr.mp.writeString(aid); + + send(rr); + } + + @Override public void + changeIccPin2(String oldPin2, String newPin2, Message result) { + changeIccPin2ForApp(oldPin2, newPin2, null, result); + } + + @Override public void + changeIccPin2ForApp(String oldPin2, String newPin2, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CHANGE_SIM_PIN2, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(3); + rr.mp.writeString(oldPin2); + rr.mp.writeString(newPin2); + rr.mp.writeString(aid); + + send(rr); + } + + public void + changeBarringPassword(String facility, String oldPwd, String newPwd, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CHANGE_BARRING_PASSWORD, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(3); + rr.mp.writeString(facility); + rr.mp.writeString(oldPwd); + rr.mp.writeString(newPwd); + + send(rr); + } + + public void + supplyNetworkDepersonalization(String netpin, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeInt(1); + rr.mp.writeString(netpin); + + send(rr); + } + + public void + getCurrentCalls (Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_CURRENT_CALLS, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + @Deprecated public void + getPDPContextList(Message result) { + getDataCallList(result); + } + + public void + getDataCallList(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_DATA_CALL_LIST, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + dial (String address, int clirMode, Message result) { + dial(address, clirMode, null, result); + } + + public void + dial(String address, int clirMode, UUSInfo uusInfo, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_DIAL, result); + + rr.mp.writeString(address); + rr.mp.writeInt(clirMode); + rr.mp.writeInt(0); // UUS information is absent + + if (uusInfo == null) { + rr.mp.writeInt(0); // UUS information is absent + } else { + rr.mp.writeInt(1); // UUS information is present + rr.mp.writeInt(uusInfo.getType()); + rr.mp.writeInt(uusInfo.getDcs()); + rr.mp.writeByteArray(uusInfo.getUserData()); + } + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getIMSI(Message result) { + getIMSIForApp(null, result); + } + + public void + getIMSIForApp(String aid, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_IMSI, result); + + rr.mp.writeInt(1); + rr.mp.writeString(aid); + + if (RILJ_LOGD) riljLog(rr.serialString() + + "> getIMSI: " + requestToString(rr.mRequest) + + " aid: " + aid); + + send(rr); + } + + public void + getIMEI(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_IMEI, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getIMEISV(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_IMEISV, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + + public void + hangupConnection (int gsmIndex, Message result) { + if (RILJ_LOGD) riljLog("hangupConnection: gsmIndex=" + gsmIndex); + + RILRequest rr = RILRequest.obtain(RIL_REQUEST_HANGUP, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + " " + + gsmIndex); + + rr.mp.writeInt(1); + rr.mp.writeInt(gsmIndex); + + send(rr); + } + + public void + hangupWaitingOrBackground (Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND, + result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + hangupForegroundResumeBackground (Message result) { + RILRequest rr + = RILRequest.obtain( + RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND, + result); + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + switchWaitingOrHoldingAndActive (Message result) { + RILRequest rr + = RILRequest.obtain( + RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE, + result); + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + conference (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_CONFERENCE, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + + public void setPreferredVoicePrivacy(boolean enable, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE, + result); + + rr.mp.writeInt(1); + rr.mp.writeInt(enable ? 1:0); + + send(rr); + } + + public void getPreferredVoicePrivacy(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE, + result); + send(rr); + } + + public void + separateConnection (int gsmIndex, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SEPARATE_CONNECTION, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + gsmIndex); + + rr.mp.writeInt(1); + rr.mp.writeInt(gsmIndex); + + send(rr); + } + + public void + acceptCall (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_ANSWER, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + rejectCall (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_UDUB, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + explicitCallTransfer (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_EXPLICIT_CALL_TRANSFER, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getLastCallFailCause (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_LAST_CALL_FAIL_CAUSE, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * @deprecated + */ + public void + getLastPdpFailCause (Message result) { + getLastDataCallFailCause (result); + } + + /** + * The preferred new alternative to getLastPdpFailCause + */ + public void + getLastDataCallFailCause (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_LAST_DATA_CALL_FAIL_CAUSE, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + setMute (boolean enableMute, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_MUTE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + enableMute); + + rr.mp.writeInt(1); + rr.mp.writeInt(enableMute ? 1 : 0); + + send(rr); + } + + public void + getMute (Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_GET_MUTE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getSignalStrength (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SIGNAL_STRENGTH, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getVoiceRegistrationState (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_VOICE_REGISTRATION_STATE, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getDataRegistrationState (Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_DATA_REGISTRATION_STATE, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getOperator(Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_OPERATOR, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + sendDtmf(char c, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_DTMF, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeString(Character.toString(c)); + + send(rr); + } + + public void + startDtmf(char c, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_DTMF_START, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeString(Character.toString(c)); + + send(rr); + } + + public void + stopDtmf(Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_DTMF_STOP, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + sendBurstDtmf(String dtmfString, int on, int off, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_BURST_DTMF, result); + + rr.mp.writeInt(3); + rr.mp.writeString(dtmfString); + rr.mp.writeString(Integer.toString(on)); + rr.mp.writeString(Integer.toString(off)); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + dtmfString); + + send(rr); + } + + public void + sendSMS (String smscPDU, String pdu, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SEND_SMS, result); + + rr.mp.writeInt(2); + rr.mp.writeString(smscPDU); + rr.mp.writeString(pdu); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + sendCdmaSms(byte[] pdu, Message result) { + int address_nbr_of_digits; + int subaddr_nbr_of_digits; + int bearerDataLength; + ByteArrayInputStream bais = new ByteArrayInputStream(pdu); + DataInputStream dis = new DataInputStream(bais); + + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_CDMA_SEND_SMS, result); + + try { + rr.mp.writeInt(dis.readInt()); //teleServiceId + rr.mp.writeByte((byte) dis.readInt()); //servicePresent + rr.mp.writeInt(dis.readInt()); //serviceCategory + rr.mp.writeInt(dis.read()); //address_digit_mode + rr.mp.writeInt(dis.read()); //address_nbr_mode + rr.mp.writeInt(dis.read()); //address_ton + rr.mp.writeInt(dis.read()); //address_nbr_plan + address_nbr_of_digits = (byte) dis.read(); + rr.mp.writeByte((byte) address_nbr_of_digits); + for(int i=0; i < address_nbr_of_digits; i++){ + rr.mp.writeByte(dis.readByte()); // address_orig_bytes[i] + } + rr.mp.writeInt(dis.read()); //subaddressType + rr.mp.writeByte((byte) dis.read()); //subaddr_odd + subaddr_nbr_of_digits = (byte) dis.read(); + rr.mp.writeByte((byte) subaddr_nbr_of_digits); + for(int i=0; i < subaddr_nbr_of_digits; i++){ + rr.mp.writeByte(dis.readByte()); //subaddr_orig_bytes[i] + } + + bearerDataLength = dis.read(); + rr.mp.writeInt(bearerDataLength); + for(int i=0; i < bearerDataLength; i++){ + rr.mp.writeByte(dis.readByte()); //bearerData[i] + } + }catch (IOException ex){ + if (RILJ_LOGD) riljLog("sendSmsCdma: conversion from input stream to object failed: " + + ex); + } + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void deleteSmsOnSim(int index, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_DELETE_SMS_ON_SIM, + response); + + rr.mp.writeInt(1); + rr.mp.writeInt(index); + + if (false) { + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + + " " + index); + } + + send(rr); + } + + public void deleteSmsOnRuim(int index, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM, + response); + + rr.mp.writeInt(1); + rr.mp.writeInt(index); + + if (false) { + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + + " " + index); + } + + send(rr); + } + + public void writeSmsToSim(int status, String smsc, String pdu, Message response) { + status = translateStatus(status); + + RILRequest rr = RILRequest.obtain(RIL_REQUEST_WRITE_SMS_TO_SIM, + response); + + rr.mp.writeInt(status); + rr.mp.writeString(pdu); + rr.mp.writeString(smsc); + + if (false) { + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + + " " + status); + } + + send(rr); + } + + public void writeSmsToRuim(int status, String pdu, Message response) { + status = translateStatus(status); + + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM, + response); + + rr.mp.writeInt(status); + rr.mp.writeString(pdu); + + if (false) { + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + + " " + status); + } + + send(rr); + } + + /** + * Translates EF_SMS status bits to a status value compatible with + * SMS AT commands. See TS 27.005 3.1. + */ + private int translateStatus(int status) { + switch(status & 0x7) { + case SmsManager.STATUS_ON_ICC_READ: + return 1; + case SmsManager.STATUS_ON_ICC_UNREAD: + return 0; + case SmsManager.STATUS_ON_ICC_SENT: + return 3; + case SmsManager.STATUS_ON_ICC_UNSENT: + return 2; + } + + // Default to READ. + return 1; + } + + public void + setupDataCall(String radioTechnology, String profile, String apn, + String user, String password, String authType, String protocol, + Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SETUP_DATA_CALL, result); + + rr.mp.writeInt(7); + + rr.mp.writeString(radioTechnology); + rr.mp.writeString(profile); + rr.mp.writeString(apn); + rr.mp.writeString(user); + rr.mp.writeString(password); + rr.mp.writeString(authType); + rr.mp.writeString(protocol); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + " " + radioTechnology + " " + + profile + " " + apn + " " + user + " " + + password + " " + authType + " " + protocol); + + send(rr); + } + + public void + deactivateDataCall(int cid, int reason, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_DEACTIVATE_DATA_CALL, result); + + rr.mp.writeInt(2); + rr.mp.writeString(Integer.toString(cid)); + rr.mp.writeString(Integer.toString(reason)); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + " " + cid + " " + reason); + + send(rr); + } + + public void + setRadioPower(boolean on, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_RADIO_POWER, result); + + rr.mp.writeInt(1); + rr.mp.writeInt(on ? 1 : 0); + + if (RILJ_LOGD) { + riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + (on ? " on" : " off")); + } + + send(rr); + } + + public void + setSuppServiceNotifications(boolean enable, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION, result); + + rr.mp.writeInt(1); + rr.mp.writeInt(enable ? 1 : 0); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest)); + + send(rr); + } + + public void + acknowledgeLastIncomingGsmSms(boolean success, int cause, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SMS_ACKNOWLEDGE, result); + + rr.mp.writeInt(2); + rr.mp.writeInt(success ? 1 : 0); + rr.mp.writeInt(cause); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + success + " " + cause); + + send(rr); + } + + public void + acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE, result); + + rr.mp.writeInt(success ? 0 : 1); //RIL_CDMA_SMS_ErrorClass + // cause code according to X.S004-550E + rr.mp.writeInt(cause); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + success + " " + cause); + + send(rr); + } + + public void + acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU, result); + + rr.mp.writeInt(2); + rr.mp.writeString(success ? "1" : "0"); + rr.mp.writeString(ackPdu); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + ' ' + success + " [" + ackPdu + ']'); + + send(rr); + } + + public void + iccIO (int command, int fileid, String path, int p1, int p2, int p3, + String data, String pin2, Message result) { + iccIOForApp(command, fileid, path, p1, p2, p3, data, pin2, null, result); + } + public void + iccIOForApp (int command, int fileid, String path, int p1, int p2, int p3, + String data, String pin2, String aid, Message result) { + //Note: This RIL request has not been renamed to ICC, + // but this request is also valid for SIM and RUIM + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SIM_IO, result); + + rr.mp.writeInt(command); + rr.mp.writeInt(fileid); + rr.mp.writeString(path); + rr.mp.writeInt(p1); + rr.mp.writeInt(p2); + rr.mp.writeInt(p3); + rr.mp.writeString(data); + rr.mp.writeString(pin2); + rr.mp.writeString(aid); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> iccIO: " + + requestToString(rr.mRequest) + + " 0x" + Integer.toHexString(command) + + " 0x" + Integer.toHexString(fileid) + " " + + " path: " + path + "," + + p1 + "," + p2 + "," + p3 + + " aid: " + aid); + + send(rr); + } + + public void + getCLIR(Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_GET_CLIR, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + setCLIR(int clirMode, Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_CLIR, result); + + // count ints + rr.mp.writeInt(1); + + rr.mp.writeInt(clirMode); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + clirMode); + + send(rr); + } + + public void + queryCallWaiting(int serviceClass, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_CALL_WAITING, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(serviceClass); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + serviceClass); + + send(rr); + } + + public void + setCallWaiting(boolean enable, int serviceClass, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_CALL_WAITING, response); + + rr.mp.writeInt(2); + rr.mp.writeInt(enable ? 1 : 0); + rr.mp.writeInt(serviceClass); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + enable + ", " + serviceClass); + + send(rr); + } + + public void + setNetworkSelectionModeAutomatic(Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + setNetworkSelectionModeManual(String operatorNumeric, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + operatorNumeric); + + rr.mp.writeString(operatorNumeric); + + send(rr); + } + + public void + getNetworkSelectionMode(Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getAvailableNetworks(Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_AVAILABLE_NETWORKS, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + setCallForward(int action, int cfReason, int serviceClass, + String number, int timeSeconds, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_CALL_FORWARD, response); + + rr.mp.writeInt(action); + rr.mp.writeInt(cfReason); + rr.mp.writeInt(serviceClass); + rr.mp.writeInt(PhoneNumberUtils.toaFromString(number)); + rr.mp.writeString(number); + rr.mp.writeInt (timeSeconds); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + action + " " + cfReason + " " + serviceClass + + timeSeconds); + + send(rr); + } + + public void + queryCallForwardStatus(int cfReason, int serviceClass, + String number, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_CALL_FORWARD_STATUS, response); + + rr.mp.writeInt(2); // 2 is for query action, not in used anyway + rr.mp.writeInt(cfReason); + rr.mp.writeInt(serviceClass); + rr.mp.writeInt(PhoneNumberUtils.toaFromString(number)); + rr.mp.writeString(number); + rr.mp.writeInt (0); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + cfReason + " " + serviceClass); + + send(rr); + } + + public void + queryCLIP(Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_CLIP, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + + public void + getBasebandVersion (Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_BASEBAND_VERSION, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + @Override + public void + queryFacilityLock(String facility, String password, int serviceClass, + Message response) { + queryFacilityLockForApp(facility, password, serviceClass, null, response); + } + + @Override + public void + queryFacilityLockForApp(String facility, String password, int serviceClass, String appId, + Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_QUERY_FACILITY_LOCK, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + // count strings + rr.mp.writeInt(4); + + rr.mp.writeString(facility); + rr.mp.writeString(password); + + rr.mp.writeString(Integer.toString(serviceClass)); + rr.mp.writeString(appId); + + send(rr); + } + + @Override + public void + setFacilityLock (String facility, boolean lockState, String password, + int serviceClass, Message response) { + setFacilityLockForApp(facility, lockState, password, serviceClass, null, response); + } + + @Override + public void + setFacilityLockForApp(String facility, boolean lockState, String password, + int serviceClass, String appId, Message response) { + String lockString; + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_FACILITY_LOCK, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + // count strings + rr.mp.writeInt(5); + + rr.mp.writeString(facility); + lockString = (lockState)?"1":"0"; + rr.mp.writeString(lockString); + rr.mp.writeString(password); + rr.mp.writeString(Integer.toString(serviceClass)); + rr.mp.writeString(appId); + + send(rr); + + } + + public void + sendUSSD (String ussdString, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SEND_USSD, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + ussdString); + + rr.mp.writeString(ussdString); + + send(rr); + } + + // inherited javadoc suffices + public void cancelPendingUssd (Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_CANCEL_USSD, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + + public void resetRadio(Message result) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_RESET_RADIO, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void invokeOemRilRequestRaw(byte[] data, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_OEM_HOOK_RAW, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + "[" + IccUtils.bytesToHexString(data) + "]"); + + rr.mp.writeByteArray(data); + + send(rr); + + } + + public void invokeOemRilRequestStrings(String[] strings, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_OEM_HOOK_STRINGS, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeStringArray(strings); + + send(rr); + } + + /** + * Assign a specified band for RF configuration. + * + * @param bandMode one of BM_*_BAND + * @param response is callback message + */ + public void setBandMode (int bandMode, Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_SET_BAND_MODE, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(bandMode); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " " + bandMode); + + send(rr); + } + + /** + * Query the list of band mode supported by RF. + * + * @param response is callback message + * ((AsyncResult)response.obj).result is an int[] with every + * element representing one avialable BM_*_BAND + */ + public void queryAvailableBandMode (Message response) { + RILRequest rr + = RILRequest.obtain(RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void sendTerminalResponse(String contents, Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeString(contents); + send(rr); + } + + /** + * {@inheritDoc} + */ + public void sendEnvelope(String contents, Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + rr.mp.writeString(contents); + send(rr); + } + + /** + * {@inheritDoc} + */ + public void sendEnvelopeWithStatus(String contents, Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + '[' + contents + ']'); + + rr.mp.writeString(contents); + send(rr); + } + + /** + * {@inheritDoc} + */ + public void handleCallSetupRequestFromSim( + boolean accept, Message response) { + + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM, + response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + int[] param = new int[1]; + param[0] = accept ? 1 : 0; + rr.mp.writeIntArray(param); + send(rr); + } + + /** + * {@inheritDoc} + */ + @Override + public void setCurrentPreferredNetworkType() { + if (RILJ_LOGD) riljLog("setCurrentPreferredNetworkType: " + mSetPreferredNetworkType); + setPreferredNetworkType(mSetPreferredNetworkType, null); + } + private int mSetPreferredNetworkType; + + /** + * {@inheritDoc} + */ + public void setPreferredNetworkType(int networkType , Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(networkType); + + mSetPreferredNetworkType = networkType; + mPreferredNetworkType = networkType; + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + networkType); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void getPreferredNetworkType(Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void getNeighboringCids(Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_GET_NEIGHBORING_CELL_IDS, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setLocationUpdates(boolean enable, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_SET_LOCATION_UPDATES, response); + rr.mp.writeInt(1); + rr.mp.writeInt(enable ? 1 : 0); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + ": " + enable); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void getSmscAddress(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_SMSC_ADDRESS, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setSmscAddress(String address, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_SET_SMSC_ADDRESS, result); + + rr.mp.writeString(address); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + address); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void reportSmsMemoryStatus(boolean available, Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_REPORT_SMS_MEMORY_STATUS, result); + rr.mp.writeInt(1); + rr.mp.writeInt(available ? 1 : 0); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + + requestToString(rr.mRequest) + ": " + available); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void reportStkServiceIsRunning(Message result) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING, result); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void getGsmBroadcastConfig(Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GSM_GET_BROADCAST_CONFIG, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GSM_SET_BROADCAST_CONFIG, response); + + int numOfConfig = config.length; + rr.mp.writeInt(numOfConfig); + + for(int i = 0; i < numOfConfig; i++) { + rr.mp.writeInt(config[i].getFromServiceId()); + rr.mp.writeInt(config[i].getToServiceId()); + rr.mp.writeInt(config[i].getFromCodeScheme()); + rr.mp.writeInt(config[i].getToCodeScheme()); + rr.mp.writeInt(config[i].isSelected() ? 1 : 0); + } + + if (RILJ_LOGD) { + riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " with " + numOfConfig + " configs : "); + for (int i = 0; i < numOfConfig; i++) { + riljLog(config[i].toString()); + } + } + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setGsmBroadcastActivation(boolean activate, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_GSM_BROADCAST_ACTIVATION, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(activate ? 0 : 1); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + //***** Private Methods + + private void sendScreenState(boolean on) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_SCREEN_STATE, null); + rr.mp.writeInt(1); + rr.mp.writeInt(on ? 1 : 0); + + if (RILJ_LOGD) riljLog(rr.serialString() + + "> " + requestToString(rr.mRequest) + ": " + on); + + send(rr); + } + + protected void + onRadioAvailable() { + // In case screen state was lost (due to process crash), + // this ensures that the RIL knows the correct screen state. + + // TODO: Should query Power Manager and send the actual + // screen state. Just send true for now. + sendScreenState(true); + } + + private RadioState getRadioStateFromInt(int stateInt) { + RadioState state; + + /* RIL_RadioState ril.h */ + switch(stateInt) { + case 0: state = RadioState.RADIO_OFF; break; + case 1: state = RadioState.RADIO_UNAVAILABLE; break; + case 10: state = RadioState.RADIO_ON; break; + + default: + throw new RuntimeException( + "Unrecognized RIL_RadioState: " + stateInt); + } + return state; + } + + private void switchToRadioState(RadioState newState) { + setRadioState(newState); + } + + /** + * Holds a PARTIAL_WAKE_LOCK whenever + * a) There is outstanding RIL request sent to RIL deamon and no replied + * b) There is a request pending to be sent out. + * + * There is a WAKE_LOCK_TIMEOUT to release the lock, though it shouldn't + * happen often. + */ + + private void + acquireWakeLock() { + synchronized (mWakeLock) { + mWakeLock.acquire(); + mRequestMessagesPending++; + + mSender.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); + Message msg = mSender.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT); + mSender.sendMessageDelayed(msg, mWakeLockTimeout); + } + } + + private void + releaseWakeLockIfDone() { + synchronized (mWakeLock) { + if (mWakeLock.isHeld() && + (mRequestMessagesPending == 0) && + (mRequestMessagesWaiting == 0)) { + mSender.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); + mWakeLock.release(); + } + } + } + + private void + send(RILRequest rr) { + Message msg; + + if (mSocket == null) { + rr.onError(RADIO_NOT_AVAILABLE, null); + rr.release(); + return; + } + + msg = mSender.obtainMessage(EVENT_SEND, rr); + + acquireWakeLock(); + + msg.sendToTarget(); + } + + private void + processResponse (Parcel p) { + int type; + + type = p.readInt(); + + if (type == RESPONSE_UNSOLICITED) { + processUnsolicited (p); + } else if (type == RESPONSE_SOLICITED) { + processSolicited (p); + } + + releaseWakeLockIfDone(); + } + + /** + * Release each request in mReqeustsList then clear the list + * @param error is the RIL_Errno sent back + * @param loggable true means to print all requests in mRequestslist + */ + private void clearRequestsList(int error, boolean loggable) { + RILRequest rr; + synchronized (mRequestsList) { + int count = mRequestsList.size(); + if (RILJ_LOGD && loggable) { + Log.d(LOG_TAG, "WAKE_LOCK_TIMEOUT " + + " mReqPending=" + mRequestMessagesPending + + " mRequestList=" + count); + } + + for (int i = 0; i < count ; i++) { + rr = mRequestsList.get(i); + if (RILJ_LOGD && loggable) { + Log.d(LOG_TAG, i + ": [" + rr.mSerial + "] " + + requestToString(rr.mRequest)); + } + rr.onError(error, null); + rr.release(); + } + mRequestsList.clear(); + mRequestMessagesWaiting = 0; + } + } + + private RILRequest findAndRemoveRequestFromList(int serial) { + synchronized (mRequestsList) { + for (int i = 0, s = mRequestsList.size() ; i < s ; i++) { + RILRequest rr = mRequestsList.get(i); + + if (rr.mSerial == serial) { + mRequestsList.remove(i); + if (mRequestMessagesWaiting > 0) + mRequestMessagesWaiting--; + return rr; + } + } + } + + return null; + } + + private void + processSolicited (Parcel p) { + int serial, error; + boolean found = false; + + serial = p.readInt(); + error = p.readInt(); + + RILRequest rr; + + rr = findAndRemoveRequestFromList(serial); + + if (rr == null) { + Log.w(LOG_TAG, "Unexpected solicited response! sn: " + + serial + " error: " + error); + return; + } + + Object ret = null; + + if (error == 0 || p.dataAvail() > 0) { + // either command succeeds or command fails but with data payload + try {switch (rr.mRequest) { + /* + cat libs/telephony/ril_commands.h \ + | egrep "^ *{RIL_" \ + | sed -re 's/\{([^,]+),[^,]+,([^}]+).+/case \1: ret = \2(p); break;/' + */ + case RIL_REQUEST_GET_SIM_STATUS: ret = responseIccCardStatus(p); break; + case RIL_REQUEST_ENTER_SIM_PIN: ret = responseInts(p); break; + case RIL_REQUEST_ENTER_SIM_PUK: ret = responseInts(p); break; + case RIL_REQUEST_ENTER_SIM_PIN2: ret = responseInts(p); break; + case RIL_REQUEST_ENTER_SIM_PUK2: ret = responseInts(p); break; + case RIL_REQUEST_CHANGE_SIM_PIN: ret = responseInts(p); break; + case RIL_REQUEST_CHANGE_SIM_PIN2: ret = responseInts(p); break; + case RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION: ret = responseInts(p); break; + case RIL_REQUEST_GET_CURRENT_CALLS: ret = responseCallList(p); break; + case RIL_REQUEST_DIAL: ret = responseVoid(p); break; + case RIL_REQUEST_GET_IMSI: ret = responseString(p); break; + case RIL_REQUEST_HANGUP: ret = responseVoid(p); break; + case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND: ret = responseVoid(p); break; + case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND: { + if (mTestingEmergencyCall.getAndSet(false)) { + if (mEmergencyCallbackModeRegistrant != null) { + riljLog("testing emergency call, notify ECM Registrants"); + mEmergencyCallbackModeRegistrant.notifyRegistrant(); + } + } + ret = responseVoid(p); + break; + } + case RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE: ret = responseVoid(p); break; + case RIL_REQUEST_CONFERENCE: ret = responseVoid(p); break; + case RIL_REQUEST_UDUB: ret = responseVoid(p); break; + case RIL_REQUEST_LAST_CALL_FAIL_CAUSE: ret = responseInts(p); break; + case RIL_REQUEST_SIGNAL_STRENGTH: ret = responseSignalStrength(p); break; + case RIL_REQUEST_VOICE_REGISTRATION_STATE: ret = responseStrings(p); break; + case RIL_REQUEST_DATA_REGISTRATION_STATE: ret = responseStrings(p); break; + case RIL_REQUEST_OPERATOR: ret = responseStrings(p); break; + case RIL_REQUEST_RADIO_POWER: ret = responseVoid(p); break; + case RIL_REQUEST_DTMF: ret = responseVoid(p); break; + case RIL_REQUEST_SEND_SMS: ret = responseSMS(p); break; + case RIL_REQUEST_SEND_SMS_EXPECT_MORE: ret = responseSMS(p); break; + case RIL_REQUEST_SETUP_DATA_CALL: ret = responseSetupDataCall(p); break; + case RIL_REQUEST_SIM_IO: ret = responseICC_IO(p); break; + case RIL_REQUEST_SEND_USSD: ret = responseVoid(p); break; + case RIL_REQUEST_CANCEL_USSD: ret = responseVoid(p); break; + case RIL_REQUEST_GET_CLIR: ret = responseInts(p); break; + case RIL_REQUEST_SET_CLIR: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_CALL_FORWARD_STATUS: ret = responseCallForward(p); break; + case RIL_REQUEST_SET_CALL_FORWARD: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_CALL_WAITING: ret = responseInts(p); break; + case RIL_REQUEST_SET_CALL_WAITING: ret = responseVoid(p); break; + case RIL_REQUEST_SMS_ACKNOWLEDGE: ret = responseVoid(p); break; + case RIL_REQUEST_GET_IMEI: ret = responseString(p); break; + case RIL_REQUEST_GET_IMEISV: ret = responseString(p); break; + case RIL_REQUEST_ANSWER: ret = responseVoid(p); break; + case RIL_REQUEST_DEACTIVATE_DATA_CALL: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_FACILITY_LOCK: ret = responseInts(p); break; + case RIL_REQUEST_SET_FACILITY_LOCK: ret = responseInts(p); break; + case RIL_REQUEST_CHANGE_BARRING_PASSWORD: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE: ret = responseInts(p); break; + case RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC: ret = responseVoid(p); break; + case RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_AVAILABLE_NETWORKS : ret = responseOperatorInfos(p); break; + case RIL_REQUEST_DTMF_START: ret = responseVoid(p); break; + case RIL_REQUEST_DTMF_STOP: ret = responseVoid(p); break; + case RIL_REQUEST_BASEBAND_VERSION: ret = responseString(p); break; + case RIL_REQUEST_SEPARATE_CONNECTION: ret = responseVoid(p); break; + case RIL_REQUEST_SET_MUTE: ret = responseVoid(p); break; + case RIL_REQUEST_GET_MUTE: ret = responseInts(p); break; + case RIL_REQUEST_QUERY_CLIP: ret = responseInts(p); break; + case RIL_REQUEST_LAST_DATA_CALL_FAIL_CAUSE: ret = responseInts(p); break; + case RIL_REQUEST_DATA_CALL_LIST: ret = responseDataCallList(p); break; + case RIL_REQUEST_RESET_RADIO: ret = responseVoid(p); break; + case RIL_REQUEST_OEM_HOOK_RAW: ret = responseRaw(p); break; + case RIL_REQUEST_OEM_HOOK_STRINGS: ret = responseStrings(p); break; + case RIL_REQUEST_SCREEN_STATE: ret = responseVoid(p); break; + case RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION: ret = responseVoid(p); break; + case RIL_REQUEST_WRITE_SMS_TO_SIM: ret = responseInts(p); break; + case RIL_REQUEST_DELETE_SMS_ON_SIM: ret = responseVoid(p); break; + case RIL_REQUEST_SET_BAND_MODE: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE: ret = responseInts(p); break; + case RIL_REQUEST_STK_GET_PROFILE: ret = responseString(p); break; + case RIL_REQUEST_STK_SET_PROFILE: ret = responseVoid(p); break; + case RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND: ret = responseString(p); break; + case RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE: ret = responseVoid(p); break; + case RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM: ret = responseInts(p); break; + case RIL_REQUEST_EXPLICIT_CALL_TRANSFER: ret = responseVoid(p); break; + case RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE: ret = responseVoid(p); break; + case RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE: ret = responseGetPreferredNetworkType(p); break; + case RIL_REQUEST_GET_NEIGHBORING_CELL_IDS: ret = responseCellList(p); break; + case RIL_REQUEST_SET_LOCATION_UPDATES: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE: ret = responseInts(p); break; + case RIL_REQUEST_SET_TTY_MODE: ret = responseVoid(p); break; + case RIL_REQUEST_QUERY_TTY_MODE: ret = responseInts(p); break; + case RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE: ret = responseInts(p); break; + case RIL_REQUEST_CDMA_FLASH: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_BURST_DTMF: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_SEND_SMS: ret = responseSMS(p); break; + case RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE: ret = responseVoid(p); break; + case RIL_REQUEST_GSM_GET_BROADCAST_CONFIG: ret = responseGmsBroadcastConfig(p); break; + case RIL_REQUEST_GSM_SET_BROADCAST_CONFIG: ret = responseVoid(p); break; + case RIL_REQUEST_GSM_BROADCAST_ACTIVATION: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG: ret = responseCdmaBroadcastConfig(p); break; + case RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_BROADCAST_ACTIVATION: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_SUBSCRIPTION: ret = responseStrings(p); break; + case RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM: ret = responseInts(p); break; + case RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM: ret = responseVoid(p); break; + case RIL_REQUEST_DEVICE_IDENTITY: ret = responseStrings(p); break; + case RIL_REQUEST_GET_SMSC_ADDRESS: ret = responseString(p); break; + case RIL_REQUEST_SET_SMSC_ADDRESS: ret = responseVoid(p); break; + case RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE: ret = responseVoid(p); break; + case RIL_REQUEST_REPORT_SMS_MEMORY_STATUS: ret = responseVoid(p); break; + case RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING: ret = responseVoid(p); break; + case RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE: ret = responseInts(p); break; + case RIL_REQUEST_ISIM_AUTHENTICATION: ret = responseString(p); break; + case RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU: ret = responseVoid(p); break; + case RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS: ret = responseICC_IO(p); break; + case RIL_REQUEST_VOICE_RADIO_TECH: ret = responseInts(p); break; + default: + throw new RuntimeException("Unrecognized solicited response: " + rr.mRequest); + //break; + }} catch (Throwable tr) { + // Exceptions here usually mean invalid RIL responses + + Log.w(LOG_TAG, rr.serialString() + "< " + + requestToString(rr.mRequest) + + " exception, possible invalid RIL response", tr); + + if (rr.mResult != null) { + AsyncResult.forMessage(rr.mResult, null, tr); + rr.mResult.sendToTarget(); + } + rr.release(); + return; + } + } + + if (error != 0) { + rr.onError(error, ret); + rr.release(); + return; + } + + if (RILJ_LOGD) riljLog(rr.serialString() + "< " + requestToString(rr.mRequest) + + " " + retToString(rr.mRequest, ret)); + + if (rr.mResult != null) { + AsyncResult.forMessage(rr.mResult, ret, null); + rr.mResult.sendToTarget(); + } + + rr.release(); + } + + private String + retToString(int req, Object ret) { + if (ret == null) return ""; + switch (req) { + // Don't log these return values, for privacy's sake. + case RIL_REQUEST_GET_IMSI: + case RIL_REQUEST_GET_IMEI: + case RIL_REQUEST_GET_IMEISV: + if (!RILJ_LOGV) { + // If not versbose logging just return and don't display IMSI and IMEI, IMEISV + return ""; + } + } + + StringBuilder sb; + String s; + int length; + if (ret instanceof int[]){ + int[] intArray = (int[]) ret; + length = intArray.length; + sb = new StringBuilder("{"); + if (length > 0) { + int i = 0; + sb.append(intArray[i++]); + while ( i < length) { + sb.append(", ").append(intArray[i++]); + } + } + sb.append("}"); + s = sb.toString(); + } else if (ret instanceof String[]) { + String[] strings = (String[]) ret; + length = strings.length; + sb = new StringBuilder("{"); + if (length > 0) { + int i = 0; + sb.append(strings[i++]); + while ( i < length) { + sb.append(", ").append(strings[i++]); + } + } + sb.append("}"); + s = sb.toString(); + }else if (req == RIL_REQUEST_GET_CURRENT_CALLS) { + ArrayList<DriverCall> calls = (ArrayList<DriverCall>) ret; + sb = new StringBuilder(" "); + for (DriverCall dc : calls) { + sb.append("[").append(dc).append("] "); + } + s = sb.toString(); + } else if (req == RIL_REQUEST_GET_NEIGHBORING_CELL_IDS) { + ArrayList<NeighboringCellInfo> cells; + cells = (ArrayList<NeighboringCellInfo>) ret; + sb = new StringBuilder(" "); + for (NeighboringCellInfo cell : cells) { + sb.append(cell).append(" "); + } + s = sb.toString(); + } else { + s = ret.toString(); + } + return s; + } + + private void + processUnsolicited (Parcel p) { + int response; + Object ret; + + response = p.readInt(); + + try {switch(response) { +/* + cat libs/telephony/ril_unsol_commands.h \ + | egrep "^ *{RIL_" \ + | sed -re 's/\{([^,]+),[^,]+,([^}]+).+/case \1: \2(rr, p); break;/' +*/ + + case RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED: ret = responseVoid(p); break; + case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED: ret = responseVoid(p); break; + case RIL_UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED: ret = responseVoid(p); break; + case RIL_UNSOL_RESPONSE_NEW_SMS: ret = responseString(p); break; + case RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT: ret = responseString(p); break; + case RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM: ret = responseInts(p); break; + case RIL_UNSOL_ON_USSD: ret = responseStrings(p); break; + case RIL_UNSOL_NITZ_TIME_RECEIVED: ret = responseString(p); break; + case RIL_UNSOL_SIGNAL_STRENGTH: ret = responseSignalStrength(p); break; + case RIL_UNSOL_DATA_CALL_LIST_CHANGED: ret = responseDataCallList(p);break; + case RIL_UNSOL_SUPP_SVC_NOTIFICATION: ret = responseSuppServiceNotification(p); break; + case RIL_UNSOL_STK_SESSION_END: ret = responseVoid(p); break; + case RIL_UNSOL_STK_PROACTIVE_COMMAND: ret = responseString(p); break; + case RIL_UNSOL_STK_EVENT_NOTIFY: ret = responseString(p); break; + case RIL_UNSOL_STK_CALL_SETUP: ret = responseInts(p); break; + case RIL_UNSOL_SIM_SMS_STORAGE_FULL: ret = responseVoid(p); break; + case RIL_UNSOL_SIM_REFRESH: ret = responseSimRefresh(p); break; + case RIL_UNSOL_CALL_RING: ret = responseCallRing(p); break; + case RIL_UNSOL_RESTRICTED_STATE_CHANGED: ret = responseInts(p); break; + case RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED: ret = responseVoid(p); break; + case RIL_UNSOL_RESPONSE_CDMA_NEW_SMS: ret = responseCdmaSms(p); break; + case RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS: ret = responseRaw(p); break; + case RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL: ret = responseVoid(p); break; + case RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE: ret = responseVoid(p); break; + case RIL_UNSOL_CDMA_CALL_WAITING: ret = responseCdmaCallWaiting(p); break; + case RIL_UNSOL_CDMA_OTA_PROVISION_STATUS: ret = responseInts(p); break; + case RIL_UNSOL_CDMA_INFO_REC: ret = responseCdmaInformationRecord(p); break; + case RIL_UNSOL_OEM_HOOK_RAW: ret = responseRaw(p); break; + case RIL_UNSOL_RINGBACK_TONE: ret = responseInts(p); break; + case RIL_UNSOL_RESEND_INCALL_MUTE: ret = responseVoid(p); break; + case RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED: ret = responseInts(p); break; + case RIL_UNSOl_CDMA_PRL_CHANGED: ret = responseInts(p); break; + case RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE: ret = responseVoid(p); break; + case RIL_UNSOL_RIL_CONNECTED: ret = responseInts(p); break; + case RIL_UNSOL_VOICE_RADIO_TECH_CHANGED: ret = responseInts(p); break; + + default: + throw new RuntimeException("Unrecognized unsol response: " + response); + //break; (implied) + }} catch (Throwable tr) { + Log.e(LOG_TAG, "Exception processing unsol response: " + response + + "Exception:" + tr.toString()); + return; + } + + switch(response) { + case RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED: + /* has bonus radio state int */ + RadioState newState = getRadioStateFromInt(p.readInt()); + if (RILJ_LOGD) unsljLogMore(response, newState.toString()); + + switchToRadioState(newState); + break; + case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED: + if (RILJ_LOGD) unsljLog(response); + + mCallStateRegistrants + .notifyRegistrants(new AsyncResult(null, null, null)); + break; + case RIL_UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED: + if (RILJ_LOGD) unsljLog(response); + + mVoiceNetworkStateRegistrants + .notifyRegistrants(new AsyncResult(null, null, null)); + break; + case RIL_UNSOL_RESPONSE_NEW_SMS: { + if (RILJ_LOGD) unsljLog(response); + + // FIXME this should move up a layer + String a[] = new String[2]; + + a[1] = (String)ret; + + SmsMessage sms; + + sms = SmsMessage.newFromCMT(a); + if (mGsmSmsRegistrant != null) { + mGsmSmsRegistrant + .notifyRegistrant(new AsyncResult(null, sms, null)); + } + break; + } + case RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mSmsStatusRegistrant != null) { + mSmsStatusRegistrant.notifyRegistrant( + new AsyncResult(null, ret, null)); + } + break; + case RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM: + if (RILJ_LOGD) unsljLogRet(response, ret); + + int[] smsIndex = (int[])ret; + + if(smsIndex.length == 1) { + if (mSmsOnSimRegistrant != null) { + mSmsOnSimRegistrant. + notifyRegistrant(new AsyncResult(null, smsIndex, null)); + } + } else { + if (RILJ_LOGD) riljLog(" NEW_SMS_ON_SIM ERROR with wrong length " + + smsIndex.length); + } + break; + case RIL_UNSOL_ON_USSD: + String[] resp = (String[])ret; + + if (resp.length < 2) { + resp = new String[2]; + resp[0] = ((String[])ret)[0]; + resp[1] = null; + } + if (RILJ_LOGD) unsljLogMore(response, resp[0]); + if (mUSSDRegistrant != null) { + mUSSDRegistrant.notifyRegistrant( + new AsyncResult (null, resp, null)); + } + break; + case RIL_UNSOL_NITZ_TIME_RECEIVED: + if (RILJ_LOGD) unsljLogRet(response, ret); + + // has bonus long containing milliseconds since boot that the NITZ + // time was received + long nitzReceiveTime = p.readLong(); + + Object[] result = new Object[2]; + + result[0] = ret; + result[1] = Long.valueOf(nitzReceiveTime); + + boolean ignoreNitz = SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_IGNORE_NITZ, false); + + if (ignoreNitz) { + if (RILJ_LOGD) riljLog("ignoring UNSOL_NITZ_TIME_RECEIVED"); + } else { + if (mNITZTimeRegistrant != null) { + + mNITZTimeRegistrant + .notifyRegistrant(new AsyncResult (null, result, null)); + } else { + // in case NITZ time registrant isnt registered yet + mLastNITZTimeInfo = result; + } + } + break; + + case RIL_UNSOL_SIGNAL_STRENGTH: + // Note this is set to "verbose" because it happens + // frequently + if (RILJ_LOGV) unsljLogvRet(response, ret); + + if (mSignalStrengthRegistrant != null) { + mSignalStrengthRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + case RIL_UNSOL_DATA_CALL_LIST_CHANGED: + if (RILJ_LOGD) unsljLogRet(response, ret); + + mDataNetworkStateRegistrants.notifyRegistrants(new AsyncResult(null, ret, null)); + break; + + case RIL_UNSOL_SUPP_SVC_NOTIFICATION: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mSsnRegistrant != null) { + mSsnRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_STK_SESSION_END: + if (RILJ_LOGD) unsljLog(response); + + if (mCatSessionEndRegistrant != null) { + mCatSessionEndRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_STK_PROACTIVE_COMMAND: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCatProCmdRegistrant != null) { + mCatProCmdRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_STK_EVENT_NOTIFY: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCatEventRegistrant != null) { + mCatEventRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_STK_CALL_SETUP: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCatCallSetUpRegistrant != null) { + mCatCallSetUpRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_SIM_SMS_STORAGE_FULL: + if (RILJ_LOGD) unsljLog(response); + + if (mIccSmsFullRegistrant != null) { + mIccSmsFullRegistrant.notifyRegistrant(); + } + break; + + case RIL_UNSOL_SIM_REFRESH: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mIccRefreshRegistrants != null) { + mIccRefreshRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_CALL_RING: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mRingRegistrant != null) { + mRingRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_RESTRICTED_STATE_CHANGED: + if (RILJ_LOGD) unsljLogvRet(response, ret); + if (mRestrictedStateRegistrant != null) { + mRestrictedStateRegistrant.notifyRegistrant( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED: + if (RILJ_LOGD) unsljLog(response); + + if (mIccStatusChangedRegistrants != null) { + mIccStatusChangedRegistrants.notifyRegistrants(); + } + break; + + case RIL_UNSOL_RESPONSE_CDMA_NEW_SMS: + if (RILJ_LOGD) unsljLog(response); + + SmsMessage sms = (SmsMessage) ret; + + if (mCdmaSmsRegistrant != null) { + mCdmaSmsRegistrant + .notifyRegistrant(new AsyncResult(null, sms, null)); + } + break; + + case RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS: + if (RILJ_LOGD) unsljLog(response); + + if (mGsmBroadcastSmsRegistrant != null) { + mGsmBroadcastSmsRegistrant + .notifyRegistrant(new AsyncResult(null, ret, null)); + } + break; + + case RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL: + if (RILJ_LOGD) unsljLog(response); + + if (mIccSmsFullRegistrant != null) { + mIccSmsFullRegistrant.notifyRegistrant(); + } + break; + + case RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE: + if (RILJ_LOGD) unsljLog(response); + + if (mEmergencyCallbackModeRegistrant != null) { + mEmergencyCallbackModeRegistrant.notifyRegistrant(); + } + break; + + case RIL_UNSOL_CDMA_CALL_WAITING: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCallWaitingInfoRegistrants != null) { + mCallWaitingInfoRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_CDMA_OTA_PROVISION_STATUS: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mOtaProvisionRegistrants != null) { + mOtaProvisionRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_CDMA_INFO_REC: + ArrayList<CdmaInformationRecords> listInfoRecs; + + try { + listInfoRecs = (ArrayList<CdmaInformationRecords>)ret; + } catch (ClassCastException e) { + Log.e(LOG_TAG, "Unexpected exception casting to listInfoRecs", e); + break; + } + + for (CdmaInformationRecords rec : listInfoRecs) { + if (RILJ_LOGD) unsljLogRet(response, rec); + notifyRegistrantsCdmaInfoRec(rec); + } + break; + + case RIL_UNSOL_OEM_HOOK_RAW: + if (RILJ_LOGD) unsljLogvRet(response, IccUtils.bytesToHexString((byte[])ret)); + if (mUnsolOemHookRawRegistrant != null) { + mUnsolOemHookRawRegistrant.notifyRegistrant(new AsyncResult(null, ret, null)); + } + break; + + case RIL_UNSOL_RINGBACK_TONE: + if (RILJ_LOGD) unsljLogvRet(response, ret); + if (mRingbackToneRegistrants != null) { + boolean playtone = (((int[])ret)[0] == 1); + mRingbackToneRegistrants.notifyRegistrants( + new AsyncResult (null, playtone, null)); + } + break; + + case RIL_UNSOL_RESEND_INCALL_MUTE: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mResendIncallMuteRegistrants != null) { + mResendIncallMuteRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_VOICE_RADIO_TECH_CHANGED: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mVoiceRadioTechChangedRegistrants != null) { + mVoiceRadioTechChangedRegistrants.notifyRegistrants( + new AsyncResult(null, ret, null)); + } + break; + + case RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCdmaSubscriptionChangedRegistrants != null) { + mCdmaSubscriptionChangedRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOl_CDMA_PRL_CHANGED: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mCdmaPrlChangedRegistrants != null) { + mCdmaPrlChangedRegistrants.notifyRegistrants( + new AsyncResult (null, ret, null)); + } + break; + + case RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE: + if (RILJ_LOGD) unsljLogRet(response, ret); + + if (mExitEmergencyCallbackModeRegistrants != null) { + mExitEmergencyCallbackModeRegistrants.notifyRegistrants( + new AsyncResult (null, null, null)); + } + break; + + case RIL_UNSOL_RIL_CONNECTED: { + if (RILJ_LOGD) unsljLogRet(response, ret); + + // Initial conditions + setRadioPower(false, null); + setPreferredNetworkType(mPreferredNetworkType, null); + setCdmaSubscriptionSource(mCdmaSubscription, null); + notifyRegistrantsRilConnectionChanged(((int[])ret)[0]); + break; + } + } + } + + /** + * Notifiy all registrants that the ril has connected or disconnected. + * + * @param rilVer is the version of the ril or -1 if disconnected. + */ + private void notifyRegistrantsRilConnectionChanged(int rilVer) { + mRilVersion = rilVer; + if (mRilConnectedRegistrants != null) { + mRilConnectedRegistrants.notifyRegistrants( + new AsyncResult (null, new Integer(rilVer), null)); + } + } + + private Object + responseInts(Parcel p) { + int numInts; + int response[]; + + numInts = p.readInt(); + + response = new int[numInts]; + + for (int i = 0 ; i < numInts ; i++) { + response[i] = p.readInt(); + } + + return response; + } + + + private Object + responseVoid(Parcel p) { + return null; + } + + private Object + responseCallForward(Parcel p) { + int numInfos; + CallForwardInfo infos[]; + + numInfos = p.readInt(); + + infos = new CallForwardInfo[numInfos]; + + for (int i = 0 ; i < numInfos ; i++) { + infos[i] = new CallForwardInfo(); + + infos[i].status = p.readInt(); + infos[i].reason = p.readInt(); + infos[i].serviceClass = p.readInt(); + infos[i].toa = p.readInt(); + infos[i].number = p.readString(); + infos[i].timeSeconds = p.readInt(); + } + + return infos; + } + + private Object + responseSuppServiceNotification(Parcel p) { + SuppServiceNotification notification = new SuppServiceNotification(); + + notification.notificationType = p.readInt(); + notification.code = p.readInt(); + notification.index = p.readInt(); + notification.type = p.readInt(); + notification.number = p.readString(); + + return notification; + } + + private Object + responseCdmaSms(Parcel p) { + SmsMessage sms; + sms = SmsMessage.newFromParcel(p); + + return sms; + } + + private Object + responseString(Parcel p) { + String response; + + response = p.readString(); + + return response; + } + + private Object + responseStrings(Parcel p) { + int num; + String response[]; + + response = p.readStringArray(); + + if (false) { + num = p.readInt(); + + response = new String[num]; + for (int i = 0; i < num; i++) { + response[i] = p.readString(); + } + } + + return response; + } + + private Object + responseRaw(Parcel p) { + int num; + byte response[]; + + response = p.createByteArray(); + + return response; + } + + private Object + responseSMS(Parcel p) { + int messageRef, errorCode; + String ackPDU; + + messageRef = p.readInt(); + ackPDU = p.readString(); + errorCode = p.readInt(); + + SmsResponse response = new SmsResponse(messageRef, ackPDU, errorCode); + + return response; + } + + + private Object + responseICC_IO(Parcel p) { + int sw1, sw2; + byte data[] = null; + Message ret; + + sw1 = p.readInt(); + sw2 = p.readInt(); + + String s = p.readString(); + + if (RILJ_LOGV) riljLog("< iccIO: " + + " 0x" + Integer.toHexString(sw1) + + " 0x" + Integer.toHexString(sw2) + " " + + s); + + return new IccIoResult(sw1, sw2, s); + } + + private Object + responseIccCardStatus(Parcel p) { + IccCardApplication ca; + + IccCardStatus status = new IccCardStatus(); + status.setCardState(p.readInt()); + status.setUniversalPinState(p.readInt()); + status.setGsmUmtsSubscriptionAppIndex(p.readInt()); + status.setCdmaSubscriptionAppIndex(p.readInt()); + status.setImsSubscriptionAppIndex(p.readInt()); + int numApplications = p.readInt(); + + // limit to maximum allowed applications + if (numApplications > IccCardStatus.CARD_MAX_APPS) { + numApplications = IccCardStatus.CARD_MAX_APPS; + } + status.setNumApplications(numApplications); + + for (int i = 0 ; i < numApplications ; i++) { + ca = new IccCardApplication(); + ca.app_type = ca.AppTypeFromRILInt(p.readInt()); + ca.app_state = ca.AppStateFromRILInt(p.readInt()); + ca.perso_substate = ca.PersoSubstateFromRILInt(p.readInt()); + ca.aid = p.readString(); + ca.app_label = p.readString(); + ca.pin1_replaced = p.readInt(); + ca.pin1 = ca.PinStateFromRILInt(p.readInt()); + ca.pin2 = ca.PinStateFromRILInt(p.readInt()); + status.addApplication(ca); + } + return status; + } + + private Object + responseSimRefresh(Parcel p) { + IccRefreshResponse response = new IccRefreshResponse(); + + response.refreshResult = p.readInt(); + response.efId = p.readInt(); + response.aid = p.readString(); + return response; + } + + private Object + responseCallList(Parcel p) { + int num; + int voiceSettings; + ArrayList<DriverCall> response; + DriverCall dc; + + num = p.readInt(); + response = new ArrayList<DriverCall>(num); + + if (RILJ_LOGV) { + riljLog("responseCallList: num=" + num + + " mEmergencyCallbackModeRegistrant=" + mEmergencyCallbackModeRegistrant + + " mTestingEmergencyCall=" + mTestingEmergencyCall.get()); + } + for (int i = 0 ; i < num ; i++) { + dc = new DriverCall(); + + dc.state = DriverCall.stateFromCLCC(p.readInt()); + dc.index = p.readInt(); + dc.TOA = p.readInt(); + dc.isMpty = (0 != p.readInt()); + dc.isMT = (0 != p.readInt()); + dc.als = p.readInt(); + voiceSettings = p.readInt(); + dc.isVoice = (0 == voiceSettings) ? false : true; + dc.isVoicePrivacy = (0 != p.readInt()); + dc.number = p.readString(); + int np = p.readInt(); + dc.numberPresentation = DriverCall.presentationFromCLIP(np); + dc.name = p.readString(); + dc.namePresentation = p.readInt(); + int uusInfoPresent = p.readInt(); + if (uusInfoPresent == 1) { + dc.uusInfo = new UUSInfo(); + dc.uusInfo.setType(p.readInt()); + dc.uusInfo.setDcs(p.readInt()); + byte[] userData = p.createByteArray(); + dc.uusInfo.setUserData(userData); + riljLogv(String.format("Incoming UUS : type=%d, dcs=%d, length=%d", + dc.uusInfo.getType(), dc.uusInfo.getDcs(), + dc.uusInfo.getUserData().length)); + riljLogv("Incoming UUS : data (string)=" + + new String(dc.uusInfo.getUserData())); + riljLogv("Incoming UUS : data (hex): " + + IccUtils.bytesToHexString(dc.uusInfo.getUserData())); + } else { + riljLogv("Incoming UUS : NOT present!"); + } + + // Make sure there's a leading + on addresses with a TOA of 145 + dc.number = PhoneNumberUtils.stringFromStringAndTOA(dc.number, dc.TOA); + + response.add(dc); + + if (dc.isVoicePrivacy) { + mVoicePrivacyOnRegistrants.notifyRegistrants(); + riljLog("InCall VoicePrivacy is enabled"); + } else { + mVoicePrivacyOffRegistrants.notifyRegistrants(); + riljLog("InCall VoicePrivacy is disabled"); + } + } + + Collections.sort(response); + + if ((num == 0) && mTestingEmergencyCall.getAndSet(false)) { + if (mEmergencyCallbackModeRegistrant != null) { + riljLog("responseCallList: call ended, testing emergency call," + + " notify ECM Registrants"); + mEmergencyCallbackModeRegistrant.notifyRegistrant(); + } + } + + return response; + } + + private DataCallState getDataCallState(Parcel p, int version) { + DataCallState dataCall = new DataCallState(); + + dataCall.version = version; + if (version < 5) { + dataCall.cid = p.readInt(); + dataCall.active = p.readInt(); + dataCall.type = p.readString(); + String addresses = p.readString(); + if (!TextUtils.isEmpty(addresses)) { + dataCall.addresses = addresses.split(" "); + } + } else { + dataCall.status = p.readInt(); + dataCall.suggestedRetryTime = p.readInt(); + dataCall.cid = p.readInt(); + dataCall.active = p.readInt(); + dataCall.type = p.readString(); + dataCall.ifname = p.readString(); + if ((dataCall.status == DataConnection.FailCause.NONE.getErrorCode()) && + TextUtils.isEmpty(dataCall.ifname)) { + throw new RuntimeException("getDataCallState, no ifname"); + } + String addresses = p.readString(); + if (!TextUtils.isEmpty(addresses)) { + dataCall.addresses = addresses.split(" "); + } + String dnses = p.readString(); + if (!TextUtils.isEmpty(dnses)) { + dataCall.dnses = dnses.split(" "); + } + String gateways = p.readString(); + if (!TextUtils.isEmpty(gateways)) { + dataCall.gateways = gateways.split(" "); + } + } + return dataCall; + } + + private Object + responseDataCallList(Parcel p) { + ArrayList<DataCallState> response; + + int ver = p.readInt(); + int num = p.readInt(); + riljLog("responseDataCallList ver=" + ver + " num=" + num); + + response = new ArrayList<DataCallState>(num); + for (int i = 0; i < num; i++) { + response.add(getDataCallState(p, ver)); + } + + return response; + } + + private Object + responseSetupDataCall(Parcel p) { + int ver = p.readInt(); + int num = p.readInt(); + if (RILJ_LOGV) riljLog("responseSetupDataCall ver=" + ver + " num=" + num); + + DataCallState dataCall; + + if (ver < 5) { + dataCall = new DataCallState(); + dataCall.version = ver; + dataCall.cid = Integer.parseInt(p.readString()); + dataCall.ifname = p.readString(); + if (TextUtils.isEmpty(dataCall.ifname)) { + throw new RuntimeException( + "RIL_REQUEST_SETUP_DATA_CALL response, no ifname"); + } + String addresses = p.readString(); + if (!TextUtils.isEmpty(addresses)) { + dataCall.addresses = addresses.split(" "); + } + if (num >= 4) { + String dnses = p.readString(); + if (RILJ_LOGD) riljLog("responseSetupDataCall got dnses=" + dnses); + if (!TextUtils.isEmpty(dnses)) { + dataCall.dnses = dnses.split(" "); + } + } + if (num >= 5) { + String gateways = p.readString(); + if (RILJ_LOGD) riljLog("responseSetupDataCall got gateways=" + gateways); + if (!TextUtils.isEmpty(gateways)) { + dataCall.gateways = gateways.split(" "); + } + } + } else { + if (num != 1) { + throw new RuntimeException( + "RIL_REQUEST_SETUP_DATA_CALL response expecting 1 RIL_Data_Call_response_v5" + + " got " + num); + } + dataCall = getDataCallState(p, ver); + } + + return dataCall; + } + + private Object + responseOperatorInfos(Parcel p) { + String strings[] = (String [])responseStrings(p); + ArrayList<OperatorInfo> ret; + + if (strings.length % 4 != 0) { + throw new RuntimeException( + "RIL_REQUEST_QUERY_AVAILABLE_NETWORKS: invalid response. Got " + + strings.length + " strings, expected multible of 4"); + } + + ret = new ArrayList<OperatorInfo>(strings.length / 4); + + for (int i = 0 ; i < strings.length ; i += 4) { + ret.add ( + new OperatorInfo( + strings[i+0], + strings[i+1], + strings[i+2], + strings[i+3])); + } + + return ret; + } + + private Object + responseCellList(Parcel p) { + int num, rssi; + String location; + ArrayList<NeighboringCellInfo> response; + NeighboringCellInfo cell; + + num = p.readInt(); + response = new ArrayList<NeighboringCellInfo>(); + + // Determine the radio access type + String radioString = SystemProperties.get( + TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, "unknown"); + int radioType; + if (radioString.equals("GPRS")) { + radioType = NETWORK_TYPE_GPRS; + } else if (radioString.equals("EDGE")) { + radioType = NETWORK_TYPE_EDGE; + } else if (radioString.equals("UMTS")) { + radioType = NETWORK_TYPE_UMTS; + } else if (radioString.equals("HSDPA")) { + radioType = NETWORK_TYPE_HSDPA; + } else if (radioString.equals("HSUPA")) { + radioType = NETWORK_TYPE_HSUPA; + } else if (radioString.equals("HSPA")) { + radioType = NETWORK_TYPE_HSPA; + } else { + radioType = NETWORK_TYPE_UNKNOWN; + } + + // Interpret the location based on radio access type + if (radioType != NETWORK_TYPE_UNKNOWN) { + for (int i = 0 ; i < num ; i++) { + rssi = p.readInt(); + location = p.readString(); + cell = new NeighboringCellInfo(rssi, location, radioType); + response.add(cell); + } + } + return response; + } + + private Object responseGetPreferredNetworkType(Parcel p) { + int [] response = (int[]) responseInts(p); + + if (response.length >= 1) { + // Since this is the response for getPreferredNetworkType + // we'll assume that it should be the value we want the + // vendor ril to take if we reestablish a connection to it. + mPreferredNetworkType = response[0]; + } + return response; + } + + private Object responseGmsBroadcastConfig(Parcel p) { + int num; + ArrayList<SmsBroadcastConfigInfo> response; + SmsBroadcastConfigInfo info; + + num = p.readInt(); + response = new ArrayList<SmsBroadcastConfigInfo>(num); + + for (int i = 0; i < num; i++) { + int fromId = p.readInt(); + int toId = p.readInt(); + int fromScheme = p.readInt(); + int toScheme = p.readInt(); + boolean selected = (p.readInt() == 1); + + info = new SmsBroadcastConfigInfo(fromId, toId, fromScheme, + toScheme, selected); + response.add(info); + } + return response; + } + + private Object + responseCdmaBroadcastConfig(Parcel p) { + int numServiceCategories; + int response[]; + + numServiceCategories = p.readInt(); + + if (numServiceCategories == 0) { + // TODO: The logic of providing default values should + // not be done by this transport layer. And needs to + // be done by the vendor ril or application logic. + int numInts; + numInts = CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES * CDMA_BSI_NO_OF_INTS_STRUCT + 1; + response = new int[numInts]; + + // Faking a default record for all possible records. + response[0] = CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES; + + // Loop over CDMA_BROADCAST_SMS_NO_OF_SERVICE_CATEGORIES set 'english' as + // default language and selection status to false for all. + for (int i = 1; i < numInts; i += CDMA_BSI_NO_OF_INTS_STRUCT ) { + response[i + 0] = i / CDMA_BSI_NO_OF_INTS_STRUCT; + response[i + 1] = 1; + response[i + 2] = 0; + } + } else { + int numInts; + numInts = (numServiceCategories * CDMA_BSI_NO_OF_INTS_STRUCT) + 1; + response = new int[numInts]; + + response[0] = numServiceCategories; + for (int i = 1 ; i < numInts; i++) { + response[i] = p.readInt(); + } + } + + return response; + } + + private Object + responseSignalStrength(Parcel p) { + int numInts = 12; + int response[]; + + /* TODO: Add SignalStrength class to match RIL_SignalStrength */ + response = new int[numInts]; + for (int i = 0 ; i < numInts ; i++) { + response[i] = p.readInt(); + } + + return response; + } + + private ArrayList<CdmaInformationRecords> + responseCdmaInformationRecord(Parcel p) { + int numberOfInfoRecs; + ArrayList<CdmaInformationRecords> response; + + /** + * Loop through all of the information records unmarshalling them + * and converting them to Java Objects. + */ + numberOfInfoRecs = p.readInt(); + response = new ArrayList<CdmaInformationRecords>(numberOfInfoRecs); + + for (int i = 0; i < numberOfInfoRecs; i++) { + CdmaInformationRecords InfoRec = new CdmaInformationRecords(p); + response.add(InfoRec); + } + + return response; + } + + private Object + responseCdmaCallWaiting(Parcel p) { + CdmaCallWaitingNotification notification = new CdmaCallWaitingNotification(); + + notification.number = p.readString(); + notification.numberPresentation = notification.presentationFromCLIP(p.readInt()); + notification.name = p.readString(); + notification.namePresentation = notification.numberPresentation; + notification.isPresent = p.readInt(); + notification.signalType = p.readInt(); + notification.alertPitch = p.readInt(); + notification.signal = p.readInt(); + notification.numberType = p.readInt(); + notification.numberPlan = p.readInt(); + + return notification; + } + + private Object + responseCallRing(Parcel p){ + char response[] = new char[4]; + + response[0] = (char) p.readInt(); // isPresent + response[1] = (char) p.readInt(); // signalType + response[2] = (char) p.readInt(); // alertPitch + response[3] = (char) p.readInt(); // signal + + return response; + } + + private void + notifyRegistrantsCdmaInfoRec(CdmaInformationRecords infoRec) { + int response = RIL_UNSOL_CDMA_INFO_REC; + if (infoRec.record instanceof CdmaInformationRecords.CdmaDisplayInfoRec) { + if (mDisplayInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mDisplayInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaSignalInfoRec) { + if (mSignalInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mSignalInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaNumberInfoRec) { + if (mNumberInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mNumberInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaRedirectingNumberInfoRec) { + if (mRedirNumInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mRedirNumInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaLineControlInfoRec) { + if (mLineControlInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mLineControlInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaT53ClirInfoRec) { + if (mT53ClirInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mT53ClirInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } else if (infoRec.record instanceof CdmaInformationRecords.CdmaT53AudioControlInfoRec) { + if (mT53AudCntrlInfoRegistrants != null) { + if (RILJ_LOGD) unsljLogRet(response, infoRec.record); + mT53AudCntrlInfoRegistrants.notifyRegistrants( + new AsyncResult (null, infoRec.record, null)); + } + } + } + + static String + requestToString(int request) { +/* + cat libs/telephony/ril_commands.h \ + | egrep "^ *{RIL_" \ + | sed -re 's/\{RIL_([^,]+),[^,]+,([^}]+).+/case RIL_\1: return "\1";/' +*/ + switch(request) { + case RIL_REQUEST_GET_SIM_STATUS: return "GET_SIM_STATUS"; + case RIL_REQUEST_ENTER_SIM_PIN: return "ENTER_SIM_PIN"; + case RIL_REQUEST_ENTER_SIM_PUK: return "ENTER_SIM_PUK"; + case RIL_REQUEST_ENTER_SIM_PIN2: return "ENTER_SIM_PIN2"; + case RIL_REQUEST_ENTER_SIM_PUK2: return "ENTER_SIM_PUK2"; + case RIL_REQUEST_CHANGE_SIM_PIN: return "CHANGE_SIM_PIN"; + case RIL_REQUEST_CHANGE_SIM_PIN2: return "CHANGE_SIM_PIN2"; + case RIL_REQUEST_ENTER_NETWORK_DEPERSONALIZATION: return "ENTER_NETWORK_DEPERSONALIZATION"; + case RIL_REQUEST_GET_CURRENT_CALLS: return "GET_CURRENT_CALLS"; + case RIL_REQUEST_DIAL: return "DIAL"; + case RIL_REQUEST_GET_IMSI: return "GET_IMSI"; + case RIL_REQUEST_HANGUP: return "HANGUP"; + case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND: return "HANGUP_WAITING_OR_BACKGROUND"; + case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND: return "HANGUP_FOREGROUND_RESUME_BACKGROUND"; + case RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE: return "REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE"; + case RIL_REQUEST_CONFERENCE: return "CONFERENCE"; + case RIL_REQUEST_UDUB: return "UDUB"; + case RIL_REQUEST_LAST_CALL_FAIL_CAUSE: return "LAST_CALL_FAIL_CAUSE"; + case RIL_REQUEST_SIGNAL_STRENGTH: return "SIGNAL_STRENGTH"; + case RIL_REQUEST_VOICE_REGISTRATION_STATE: return "VOICE_REGISTRATION_STATE"; + case RIL_REQUEST_DATA_REGISTRATION_STATE: return "DATA_REGISTRATION_STATE"; + case RIL_REQUEST_OPERATOR: return "OPERATOR"; + case RIL_REQUEST_RADIO_POWER: return "RADIO_POWER"; + case RIL_REQUEST_DTMF: return "DTMF"; + case RIL_REQUEST_SEND_SMS: return "SEND_SMS"; + case RIL_REQUEST_SEND_SMS_EXPECT_MORE: return "SEND_SMS_EXPECT_MORE"; + case RIL_REQUEST_SETUP_DATA_CALL: return "SETUP_DATA_CALL"; + case RIL_REQUEST_SIM_IO: return "SIM_IO"; + case RIL_REQUEST_SEND_USSD: return "SEND_USSD"; + case RIL_REQUEST_CANCEL_USSD: return "CANCEL_USSD"; + case RIL_REQUEST_GET_CLIR: return "GET_CLIR"; + case RIL_REQUEST_SET_CLIR: return "SET_CLIR"; + case RIL_REQUEST_QUERY_CALL_FORWARD_STATUS: return "QUERY_CALL_FORWARD_STATUS"; + case RIL_REQUEST_SET_CALL_FORWARD: return "SET_CALL_FORWARD"; + case RIL_REQUEST_QUERY_CALL_WAITING: return "QUERY_CALL_WAITING"; + case RIL_REQUEST_SET_CALL_WAITING: return "SET_CALL_WAITING"; + case RIL_REQUEST_SMS_ACKNOWLEDGE: return "SMS_ACKNOWLEDGE"; + case RIL_REQUEST_GET_IMEI: return "GET_IMEI"; + case RIL_REQUEST_GET_IMEISV: return "GET_IMEISV"; + case RIL_REQUEST_ANSWER: return "ANSWER"; + case RIL_REQUEST_DEACTIVATE_DATA_CALL: return "DEACTIVATE_DATA_CALL"; + case RIL_REQUEST_QUERY_FACILITY_LOCK: return "QUERY_FACILITY_LOCK"; + case RIL_REQUEST_SET_FACILITY_LOCK: return "SET_FACILITY_LOCK"; + case RIL_REQUEST_CHANGE_BARRING_PASSWORD: return "CHANGE_BARRING_PASSWORD"; + case RIL_REQUEST_QUERY_NETWORK_SELECTION_MODE: return "QUERY_NETWORK_SELECTION_MODE"; + case RIL_REQUEST_SET_NETWORK_SELECTION_AUTOMATIC: return "SET_NETWORK_SELECTION_AUTOMATIC"; + case RIL_REQUEST_SET_NETWORK_SELECTION_MANUAL: return "SET_NETWORK_SELECTION_MANUAL"; + case RIL_REQUEST_QUERY_AVAILABLE_NETWORKS : return "QUERY_AVAILABLE_NETWORKS "; + case RIL_REQUEST_DTMF_START: return "DTMF_START"; + case RIL_REQUEST_DTMF_STOP: return "DTMF_STOP"; + case RIL_REQUEST_BASEBAND_VERSION: return "BASEBAND_VERSION"; + case RIL_REQUEST_SEPARATE_CONNECTION: return "SEPARATE_CONNECTION"; + case RIL_REQUEST_SET_MUTE: return "SET_MUTE"; + case RIL_REQUEST_GET_MUTE: return "GET_MUTE"; + case RIL_REQUEST_QUERY_CLIP: return "QUERY_CLIP"; + case RIL_REQUEST_LAST_DATA_CALL_FAIL_CAUSE: return "LAST_DATA_CALL_FAIL_CAUSE"; + case RIL_REQUEST_DATA_CALL_LIST: return "DATA_CALL_LIST"; + case RIL_REQUEST_RESET_RADIO: return "RESET_RADIO"; + case RIL_REQUEST_OEM_HOOK_RAW: return "OEM_HOOK_RAW"; + case RIL_REQUEST_OEM_HOOK_STRINGS: return "OEM_HOOK_STRINGS"; + case RIL_REQUEST_SCREEN_STATE: return "SCREEN_STATE"; + case RIL_REQUEST_SET_SUPP_SVC_NOTIFICATION: return "SET_SUPP_SVC_NOTIFICATION"; + case RIL_REQUEST_WRITE_SMS_TO_SIM: return "WRITE_SMS_TO_SIM"; + case RIL_REQUEST_DELETE_SMS_ON_SIM: return "DELETE_SMS_ON_SIM"; + case RIL_REQUEST_SET_BAND_MODE: return "SET_BAND_MODE"; + case RIL_REQUEST_QUERY_AVAILABLE_BAND_MODE: return "QUERY_AVAILABLE_BAND_MODE"; + case RIL_REQUEST_STK_GET_PROFILE: return "REQUEST_STK_GET_PROFILE"; + case RIL_REQUEST_STK_SET_PROFILE: return "REQUEST_STK_SET_PROFILE"; + case RIL_REQUEST_STK_SEND_ENVELOPE_COMMAND: return "REQUEST_STK_SEND_ENVELOPE_COMMAND"; + case RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE: return "REQUEST_STK_SEND_TERMINAL_RESPONSE"; + case RIL_REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM: return "REQUEST_STK_HANDLE_CALL_SETUP_REQUESTED_FROM_SIM"; + case RIL_REQUEST_EXPLICIT_CALL_TRANSFER: return "REQUEST_EXPLICIT_CALL_TRANSFER"; + case RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE: return "REQUEST_SET_PREFERRED_NETWORK_TYPE"; + case RIL_REQUEST_GET_PREFERRED_NETWORK_TYPE: return "REQUEST_GET_PREFERRED_NETWORK_TYPE"; + case RIL_REQUEST_GET_NEIGHBORING_CELL_IDS: return "REQUEST_GET_NEIGHBORING_CELL_IDS"; + case RIL_REQUEST_SET_LOCATION_UPDATES: return "REQUEST_SET_LOCATION_UPDATES"; + case RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE: return "RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE"; + case RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE: return "RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE"; + case RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE: return "RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE"; + case RIL_REQUEST_SET_TTY_MODE: return "RIL_REQUEST_SET_TTY_MODE"; + case RIL_REQUEST_QUERY_TTY_MODE: return "RIL_REQUEST_QUERY_TTY_MODE"; + case RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE: return "RIL_REQUEST_CDMA_SET_PREFERRED_VOICE_PRIVACY_MODE"; + case RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE: return "RIL_REQUEST_CDMA_QUERY_PREFERRED_VOICE_PRIVACY_MODE"; + case RIL_REQUEST_CDMA_FLASH: return "RIL_REQUEST_CDMA_FLASH"; + case RIL_REQUEST_CDMA_BURST_DTMF: return "RIL_REQUEST_CDMA_BURST_DTMF"; + case RIL_REQUEST_CDMA_SEND_SMS: return "RIL_REQUEST_CDMA_SEND_SMS"; + case RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE: return "RIL_REQUEST_CDMA_SMS_ACKNOWLEDGE"; + case RIL_REQUEST_GSM_GET_BROADCAST_CONFIG: return "RIL_REQUEST_GSM_GET_BROADCAST_CONFIG"; + case RIL_REQUEST_GSM_SET_BROADCAST_CONFIG: return "RIL_REQUEST_GSM_SET_BROADCAST_CONFIG"; + case RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG: return "RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG"; + case RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG: return "RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG"; + case RIL_REQUEST_GSM_BROADCAST_ACTIVATION: return "RIL_REQUEST_GSM_BROADCAST_ACTIVATION"; + case RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY: return "RIL_REQUEST_CDMA_VALIDATE_AND_WRITE_AKEY"; + case RIL_REQUEST_CDMA_BROADCAST_ACTIVATION: return "RIL_REQUEST_CDMA_BROADCAST_ACTIVATION"; + case RIL_REQUEST_CDMA_SUBSCRIPTION: return "RIL_REQUEST_CDMA_SUBSCRIPTION"; + case RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM: return "RIL_REQUEST_CDMA_WRITE_SMS_TO_RUIM"; + case RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM: return "RIL_REQUEST_CDMA_DELETE_SMS_ON_RUIM"; + case RIL_REQUEST_DEVICE_IDENTITY: return "RIL_REQUEST_DEVICE_IDENTITY"; + case RIL_REQUEST_GET_SMSC_ADDRESS: return "RIL_REQUEST_GET_SMSC_ADDRESS"; + case RIL_REQUEST_SET_SMSC_ADDRESS: return "RIL_REQUEST_SET_SMSC_ADDRESS"; + case RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE: return "REQUEST_EXIT_EMERGENCY_CALLBACK_MODE"; + case RIL_REQUEST_REPORT_SMS_MEMORY_STATUS: return "RIL_REQUEST_REPORT_SMS_MEMORY_STATUS"; + case RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING: return "RIL_REQUEST_REPORT_STK_SERVICE_IS_RUNNING"; + case RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE: return "RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE"; + case RIL_REQUEST_ISIM_AUTHENTICATION: return "RIL_REQUEST_ISIM_AUTHENTICATION"; + case RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU: return "RIL_REQUEST_ACKNOWLEDGE_INCOMING_GSM_SMS_WITH_PDU"; + case RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS: return "RIL_REQUEST_STK_SEND_ENVELOPE_WITH_STATUS"; + case RIL_REQUEST_VOICE_RADIO_TECH: return "RIL_REQUEST_VOICE_RADIO_TECH"; + default: return "<unknown request>"; + } + } + + static String + responseToString(int request) + { +/* + cat libs/telephony/ril_unsol_commands.h \ + | egrep "^ *{RIL_" \ + | sed -re 's/\{RIL_([^,]+),[^,]+,([^}]+).+/case RIL_\1: return "\1";/' +*/ + switch(request) { + case RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED: return "UNSOL_RESPONSE_RADIO_STATE_CHANGED"; + case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED: return "UNSOL_RESPONSE_CALL_STATE_CHANGED"; + case RIL_UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED: return "UNSOL_RESPONSE_VOICE_NETWORK_STATE_CHANGED"; + case RIL_UNSOL_RESPONSE_NEW_SMS: return "UNSOL_RESPONSE_NEW_SMS"; + case RIL_UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT: return "UNSOL_RESPONSE_NEW_SMS_STATUS_REPORT"; + case RIL_UNSOL_RESPONSE_NEW_SMS_ON_SIM: return "UNSOL_RESPONSE_NEW_SMS_ON_SIM"; + case RIL_UNSOL_ON_USSD: return "UNSOL_ON_USSD"; + case RIL_UNSOL_ON_USSD_REQUEST: return "UNSOL_ON_USSD_REQUEST"; + case RIL_UNSOL_NITZ_TIME_RECEIVED: return "UNSOL_NITZ_TIME_RECEIVED"; + case RIL_UNSOL_SIGNAL_STRENGTH: return "UNSOL_SIGNAL_STRENGTH"; + case RIL_UNSOL_DATA_CALL_LIST_CHANGED: return "UNSOL_DATA_CALL_LIST_CHANGED"; + case RIL_UNSOL_SUPP_SVC_NOTIFICATION: return "UNSOL_SUPP_SVC_NOTIFICATION"; + case RIL_UNSOL_STK_SESSION_END: return "UNSOL_STK_SESSION_END"; + case RIL_UNSOL_STK_PROACTIVE_COMMAND: return "UNSOL_STK_PROACTIVE_COMMAND"; + case RIL_UNSOL_STK_EVENT_NOTIFY: return "UNSOL_STK_EVENT_NOTIFY"; + case RIL_UNSOL_STK_CALL_SETUP: return "UNSOL_STK_CALL_SETUP"; + case RIL_UNSOL_SIM_SMS_STORAGE_FULL: return "UNSOL_SIM_SMS_STORAGE_FULL"; + case RIL_UNSOL_SIM_REFRESH: return "UNSOL_SIM_REFRESH"; + case RIL_UNSOL_CALL_RING: return "UNSOL_CALL_RING"; + case RIL_UNSOL_RESPONSE_SIM_STATUS_CHANGED: return "UNSOL_RESPONSE_SIM_STATUS_CHANGED"; + case RIL_UNSOL_RESPONSE_CDMA_NEW_SMS: return "UNSOL_RESPONSE_CDMA_NEW_SMS"; + case RIL_UNSOL_RESPONSE_NEW_BROADCAST_SMS: return "UNSOL_RESPONSE_NEW_BROADCAST_SMS"; + case RIL_UNSOL_CDMA_RUIM_SMS_STORAGE_FULL: return "UNSOL_CDMA_RUIM_SMS_STORAGE_FULL"; + case RIL_UNSOL_RESTRICTED_STATE_CHANGED: return "UNSOL_RESTRICTED_STATE_CHANGED"; + case RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE: return "UNSOL_ENTER_EMERGENCY_CALLBACK_MODE"; + case RIL_UNSOL_CDMA_CALL_WAITING: return "UNSOL_CDMA_CALL_WAITING"; + case RIL_UNSOL_CDMA_OTA_PROVISION_STATUS: return "UNSOL_CDMA_OTA_PROVISION_STATUS"; + case RIL_UNSOL_CDMA_INFO_REC: return "UNSOL_CDMA_INFO_REC"; + case RIL_UNSOL_OEM_HOOK_RAW: return "UNSOL_OEM_HOOK_RAW"; + case RIL_UNSOL_RINGBACK_TONE: return "UNSOL_RINGBACK_TONG"; + case RIL_UNSOL_RESEND_INCALL_MUTE: return "UNSOL_RESEND_INCALL_MUTE"; + case RIL_UNSOL_CDMA_SUBSCRIPTION_SOURCE_CHANGED: return "CDMA_SUBSCRIPTION_SOURCE_CHANGED"; + case RIL_UNSOl_CDMA_PRL_CHANGED: return "UNSOL_CDMA_PRL_CHANGED"; + case RIL_UNSOL_EXIT_EMERGENCY_CALLBACK_MODE: return "UNSOL_EXIT_EMERGENCY_CALLBACK_MODE"; + case RIL_UNSOL_RIL_CONNECTED: return "UNSOL_RIL_CONNECTED"; + case RIL_UNSOL_VOICE_RADIO_TECH_CHANGED: return "UNSOL_VOICE_RADIO_TECH_CHANGED"; + default: return "<unknown reponse>"; + } + } + + private void riljLog(String msg) { + Log.d(LOG_TAG, msg); + } + + private void riljLogv(String msg) { + Log.v(LOG_TAG, msg); + } + + private void unsljLog(int response) { + riljLog("[UNSL]< " + responseToString(response)); + } + + private void unsljLogMore(int response, String more) { + riljLog("[UNSL]< " + responseToString(response) + " " + more); + } + + private void unsljLogRet(int response, Object ret) { + riljLog("[UNSL]< " + responseToString(response) + " " + retToString(response, ret)); + } + + private void unsljLogvRet(int response, Object ret) { + riljLogv("[UNSL]< " + responseToString(response) + " " + retToString(response, ret)); + } + + + // ***** Methods for CDMA support + public void + getDeviceIdentity(Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_DEVICE_IDENTITY, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void + getCDMASubscription(Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_SUBSCRIPTION, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + @Override + public void setPhoneType(int phoneType) { // Called by CDMAPhone and GSMPhone constructor + if (RILJ_LOGD) riljLog("setPhoneType=" + phoneType + " old value=" + mPhoneType); + mPhoneType = phoneType; + } + + /** + * {@inheritDoc} + */ + public void queryCdmaRoamingPreference(Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_CDMA_QUERY_ROAMING_PREFERENCE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_CDMA_SET_ROAMING_PREFERENCE, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(cdmaRoamingType); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + cdmaRoamingType); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setCdmaSubscriptionSource(int cdmaSubscription , Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_CDMA_SET_SUBSCRIPTION_SOURCE, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(cdmaSubscription); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + cdmaSubscription); + + send(rr); + } + + /** + * {@inheritDoc} + */ + @Override + public void getCdmaSubscriptionSource(Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_CDMA_GET_SUBSCRIPTION_SOURCE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void queryTTYMode(Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_QUERY_TTY_MODE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void setTTYMode(int ttyMode, Message response) { + RILRequest rr = RILRequest.obtain( + RILConstants.RIL_REQUEST_SET_TTY_MODE, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(ttyMode); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + ttyMode); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void + sendCDMAFeatureCode(String FeatureCode, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_FLASH, response); + + rr.mp.writeString(FeatureCode); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest) + + " : " + FeatureCode); + + send(rr); + } + + public void getCdmaBroadcastConfig(Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_GET_BROADCAST_CONFIG, response); + + send(rr); + } + + // TODO: Change the configValuesArray to a RIL_BroadcastSMSConfig + public void setCdmaBroadcastConfig(int[] configValuesArray, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_SET_BROADCAST_CONFIG, response); + + for(int i = 0; i < configValuesArray.length; i++) { + rr.mp.writeInt(configValuesArray[i]); + } + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void setCdmaBroadcastActivation(boolean activate, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_CDMA_BROADCAST_ACTIVATION, response); + + rr.mp.writeInt(1); + rr.mp.writeInt(activate ? 0 :1); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /** + * {@inheritDoc} + */ + public void exitEmergencyCallbackMode(Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_EXIT_EMERGENCY_CALLBACK_MODE, response); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + public void requestIsimAuthentication(String nonce, Message response) { + RILRequest rr = RILRequest.obtain(RIL_REQUEST_ISIM_AUTHENTICATION, response); + + rr.mp.writeString(nonce); + + if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest)); + + send(rr); + } + + /* (non-Javadoc) + * @see com.android.internal.telephony.BaseCommands#testingEmergencyCall() + */ + @Override + public void testingEmergencyCall() { + if (RILJ_LOGD) riljLog("testingEmergencyCall"); + mTestingEmergencyCall.set(true); + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("RIL:"); + pw.println(" mSocket=" + mSocket); + pw.println(" mSenderThread=" + mSenderThread); + pw.println(" mSender=" + mSender); + pw.println(" mReceiverThread=" + mReceiverThread); + pw.println(" mReceiver=" + mReceiver); + pw.println(" mWakeLock=" + mWakeLock); + pw.println(" mWakeLockTimeout=" + mWakeLockTimeout); + synchronized (mRequestsList) { + pw.println(" mRequestMessagesPending=" + mRequestMessagesPending); + pw.println(" mRequestMessagesWaiting=" + mRequestMessagesWaiting); + int count = mRequestsList.size(); + pw.println(" mRequestList count=" + count); + for (int i = 0; i < count; i++) { + RILRequest rr = mRequestsList.get(i); + pw.println(" [" + rr.mSerial + "] " + requestToString(rr.mRequest)); + } + } + pw.println(" mLastNITZTimeInfo=" + mLastNITZTimeInfo); + pw.println(" mTestingEmergencyCall=" + mTestingEmergencyCall.get()); + } +} diff --git a/src/java/com/android/internal/telephony/RestrictedState.java b/src/java/com/android/internal/telephony/RestrictedState.java new file mode 100644 index 0000000..ad2b88d --- /dev/null +++ b/src/java/com/android/internal/telephony/RestrictedState.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.telephony.ServiceState; + +public class RestrictedState { + + /** + * Set true to block packet data access due to restriction + */ + private boolean mPsRestricted; + /** + * Set true to block all normal voice/SMS/USSD/SS/AV64 due to restriction + */ + private boolean mCsNormalRestricted; + /** + * Set true to block emergency call due to restriction + */ + private boolean mCsEmergencyRestricted; + + public RestrictedState() { + setPsRestricted(false); + setCsNormalRestricted(false); + setCsEmergencyRestricted(false); + } + + /** + * @param csEmergencyRestricted the csEmergencyRestricted to set + */ + public void setCsEmergencyRestricted(boolean csEmergencyRestricted) { + mCsEmergencyRestricted = csEmergencyRestricted; + } + + /** + * @return the csEmergencyRestricted + */ + public boolean isCsEmergencyRestricted() { + return mCsEmergencyRestricted; + } + + /** + * @param csNormalRestricted the csNormalRestricted to set + */ + public void setCsNormalRestricted(boolean csNormalRestricted) { + mCsNormalRestricted = csNormalRestricted; + } + + /** + * @return the csNormalRestricted + */ + public boolean isCsNormalRestricted() { + return mCsNormalRestricted; + } + + /** + * @param psRestricted the psRestricted to set + */ + public void setPsRestricted(boolean psRestricted) { + mPsRestricted = psRestricted; + } + + /** + * @return the psRestricted + */ + public boolean isPsRestricted() { + return mPsRestricted; + } + + public boolean isCsRestricted() { + return mCsNormalRestricted && mCsEmergencyRestricted; + } + + @Override + public boolean equals (Object o) { + RestrictedState s; + + try { + s = (RestrictedState) o; + } catch (ClassCastException ex) { + return false; + } + + if (o == null) { + return false; + } + + return mPsRestricted == s.mPsRestricted + && mCsNormalRestricted == s.mCsNormalRestricted + && mCsEmergencyRestricted == s.mCsEmergencyRestricted; + } + + @Override + public String toString() { + String csString = "none"; + + if (mCsEmergencyRestricted && mCsNormalRestricted) { + csString = "all"; + } else if (mCsEmergencyRestricted && !mCsNormalRestricted) { + csString = "emergency"; + } else if (!mCsEmergencyRestricted && mCsNormalRestricted) { + csString = "normal call"; + } + + return "Restricted State CS: " + csString + " PS:" + mPsRestricted; + } + +} diff --git a/src/java/com/android/internal/telephony/RetryManager.java b/src/java/com/android/internal/telephony/RetryManager.java new file mode 100644 index 0000000..250d99e --- /dev/null +++ b/src/java/com/android/internal/telephony/RetryManager.java @@ -0,0 +1,410 @@ +/** + * Copyright (C) 2009 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.internal.telephony; + +import android.util.Log; +import android.util.Pair; +import android.text.TextUtils; + +import java.util.Random; +import java.util.ArrayList; + +/** + * Retry manager allows a simple way to declare a series of + * retry timeouts. After creating a RetryManager the configure + * method is used to define the sequence. A simple linear series + * may be initialized using configure with three integer parameters + * The other configure method allows a series to be declared using + * a string. + *<p> + * The format of the configuration string is a series of parameters + * separated by a comma. There are two name value pair parameters plus a series + * of delay times. The units of of these delay times is unspecified. + * The name value pairs which may be specified are: + *<ul> + *<li>max_retries=<value> + *<li>default_randomizationTime=<value> + *</ul> + *<p> + * max_retries is the number of times that incrementRetryCount + * maybe called before isRetryNeeded will return false. if value + * is infinite then isRetryNeeded will always return true. + * + * default_randomizationTime will be used as the randomizationTime + * for delay times which have no supplied randomizationTime. If + * default_randomizationTime is not defined it defaults to 0. + *<p> + * The other parameters define The series of delay times and each + * may have an optional randomization value separated from the + * delay time by a colon. + *<p> + * Examples: + * <ul> + * <li>3 retries with no randomization value which means its 0: + * <ul><li><code>"1000, 2000, 3000"</code></ul> + * + * <li>10 retries with a 500 default randomization value for each and + * the 4..10 retries all using 3000 as the delay: + * <ul><li><code>"max_retries=10, default_randomization=500, 1000, 2000, 3000"</code></ul> + * + * <li>4 retries with a 100 as the default randomization value for the first 2 values and + * the other two having specified values of 500: + * <ul><li><code>"default_randomization=100, 1000, 2000, 4000:500, 5000:500"</code></ul> + * + * <li>Infinite number of retries with the first one at 1000, the second at 2000 all + * others will be at 3000. + * <ul><li><code>"max_retries=infinite,1000,2000,3000</code></ul> + * </ul> + * + * {@hide} + */ +public class RetryManager { + static public final String LOG_TAG = "GSM"; + static public final boolean DBG = true; + static public final boolean VDBG = false; + + /** + * Retry record with times in milli-seconds + */ + private static class RetryRec { + RetryRec(int delayTime, int randomizationTime) { + mDelayTime = delayTime; + mRandomizationTime = randomizationTime; + } + + int mDelayTime; + int mRandomizationTime; + } + + /** The array of retry records */ + private ArrayList<RetryRec> mRetryArray = new ArrayList<RetryRec>(); + + /** When true isRetryNeeded() will always return true */ + private boolean mRetryForever; + + /** + * The maximum number of retries to attempt before + * isRetryNeeded returns false + */ + private int mMaxRetryCount; + + /** The current number of retries */ + private int mRetryCount; + + /** Random number generator */ + private Random rng = new Random(); + + private String mConfig; + + /** Constructor */ + public RetryManager() { + if (VDBG) log("constructor"); + } + + public String toString() { + String ret = "RetryManager: forever=" + mRetryForever + ", maxRetry=" + mMaxRetryCount + + ", retry=" + mRetryCount + ",\n " + mConfig; + for (RetryRec r : mRetryArray) { + ret += "\n " + r.mDelayTime + ":" + r.mRandomizationTime; + } + return ret; + } + + /** + * Configure for a simple linear sequence of times plus + * a random value. + * + * @param maxRetryCount is the maximum number of retries + * before isRetryNeeded returns false. + * @param retryTime is a time that will be returned by getRetryTime. + * @param randomizationTime a random value between 0 and + * randomizationTime will be added to retryTime. this + * parameter may be 0. + * @return true if successful + */ + public boolean configure(int maxRetryCount, int retryTime, int randomizationTime) { + Pair<Boolean, Integer> value; + + if (VDBG) log("configure: " + maxRetryCount + ", " + retryTime + "," + randomizationTime); + + if (!validateNonNegativeInt("maxRetryCount", maxRetryCount)) { + return false; + } + + if (!validateNonNegativeInt("retryTime", retryTime)) { + return false; + } + + if (!validateNonNegativeInt("randomizationTime", randomizationTime)) { + return false; + } + + mMaxRetryCount = maxRetryCount; + resetRetryCount(); + mRetryArray.clear(); + mRetryArray.add(new RetryRec(retryTime, randomizationTime)); + + return true; + } + + /** + * Configure for using string which allow arbitrary + * sequences of times. See class comments for the + * string format. + * + * @return true if successful + */ + public boolean configure(String configStr) { + // Strip quotes if present. + if ((configStr.startsWith("\"") && configStr.endsWith("\""))) { + configStr = configStr.substring(1, configStr.length()-1); + } + if (VDBG) log("configure: '" + configStr + "'"); + mConfig = configStr; + + if (!TextUtils.isEmpty(configStr)) { + int defaultRandomization = 0; + + if (VDBG) log("configure: not empty"); + + mMaxRetryCount = 0; + resetRetryCount(); + mRetryArray.clear(); + + String strArray[] = configStr.split(","); + for (int i = 0; i < strArray.length; i++) { + if (VDBG) log("configure: strArray[" + i + "]='" + strArray[i] + "'"); + Pair<Boolean, Integer> value; + String splitStr[] = strArray[i].split("=", 2); + splitStr[0] = splitStr[0].trim(); + if (VDBG) log("configure: splitStr[0]='" + splitStr[0] + "'"); + if (splitStr.length > 1) { + splitStr[1] = splitStr[1].trim(); + if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'"); + if (TextUtils.equals(splitStr[0], "default_randomization")) { + value = parseNonNegativeInt(splitStr[0], splitStr[1]); + if (!value.first) return false; + defaultRandomization = value.second; + } else if (TextUtils.equals(splitStr[0], "max_retries")) { + if (TextUtils.equals("infinite",splitStr[1])) { + mRetryForever = true; + } else { + value = parseNonNegativeInt(splitStr[0], splitStr[1]); + if (!value.first) return false; + mMaxRetryCount = value.second; + } + } else { + Log.e(LOG_TAG, "Unrecognized configuration name value pair: " + + strArray[i]); + return false; + } + } else { + /** + * Assume a retry time with an optional randomization value + * following a ":" + */ + splitStr = strArray[i].split(":", 2); + splitStr[0] = splitStr[0].trim(); + RetryRec rr = new RetryRec(0, 0); + value = parseNonNegativeInt("delayTime", splitStr[0]); + if (!value.first) return false; + rr.mDelayTime = value.second; + + // Check if optional randomization value present + if (splitStr.length > 1) { + splitStr[1] = splitStr[1].trim(); + if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'"); + value = parseNonNegativeInt("randomizationTime", splitStr[1]); + if (!value.first) return false; + rr.mRandomizationTime = value.second; + } else { + rr.mRandomizationTime = defaultRandomization; + } + mRetryArray.add(rr); + } + } + if (mRetryArray.size() > mMaxRetryCount) { + mMaxRetryCount = mRetryArray.size(); + if (VDBG) log("configure: setting mMaxRetryCount=" + mMaxRetryCount); + } + if (VDBG) log("configure: true"); + return true; + } else { + if (VDBG) log("configure: false it's empty"); + return false; + } + } + + /** + * Report whether data reconnection should be retried + * + * @return {@code true} if the max retries has not been reached. {@code + * false} otherwise. + */ + public boolean isRetryNeeded() { + boolean retVal = mRetryForever || (mRetryCount < mMaxRetryCount); + if (DBG) log("isRetryNeeded: " + retVal); + return retVal; + } + + /** + * Return the timer that should be used to trigger the data reconnection + */ + public int getRetryTimer() { + int index; + if (mRetryCount < mRetryArray.size()) { + index = mRetryCount; + } else { + index = mRetryArray.size() - 1; + } + + int retVal; + if ((index >= 0) && (index < mRetryArray.size())) { + retVal = mRetryArray.get(index).mDelayTime + nextRandomizationTime(index); + } else { + retVal = 0; + } + + if (DBG) log("getRetryTimer: " + retVal); + return retVal; + } + + /** + * @return retry count + */ + public int getRetryCount() { + if (DBG) log("getRetryCount: " + mRetryCount); + return mRetryCount; + } + + /** + * Increase the retry counter, does not change retry forever. + */ + public void increaseRetryCount() { + mRetryCount++; + if (mRetryCount > mMaxRetryCount) { + mRetryCount = mMaxRetryCount; + } + if (DBG) log("increaseRetryCount: " + mRetryCount); + } + + /** + * Set retry count to the specified value + */ + public void setRetryCount(int count) { + mRetryCount = count; + if (mRetryCount > mMaxRetryCount) { + mRetryCount = mMaxRetryCount; + } + + if (mRetryCount < 0) { + mRetryCount = 0; + } + + if (DBG) log("setRetryCount: " + mRetryCount); + } + + /** + * Set retry forever to the specified value + */ + public void setRetryForever(boolean retryForever) { + mRetryForever = retryForever; + if (DBG) log("setRetryForever: " + mRetryForever); + } + + /** + * Clear the data-retry counter + */ + public void resetRetryCount() { + mRetryCount = 0; + if (DBG) log("resetRetryCount: " + mRetryCount); + } + + /** + * Retry forever using last timeout time. + */ + public void retryForeverUsingLastTimeout() { + mRetryCount = mMaxRetryCount; + mRetryForever = true; + if (DBG) log("retryForeverUsingLastTimeout: " + mRetryForever + ", " + mRetryCount); + } + + /** + * @return true if retrying forever + */ + public boolean isRetryForever() { + if (DBG) log("isRetryForever: " + mRetryForever); + return mRetryForever; + } + + /** + * Parse an integer validating the value is not negative. + * + * @param name + * @param stringValue + * @return Pair.first == true if stringValue an integer >= 0 + */ + private Pair<Boolean, Integer> parseNonNegativeInt(String name, String stringValue) { + int value; + Pair<Boolean, Integer> retVal; + try { + value = Integer.parseInt(stringValue); + retVal = new Pair<Boolean, Integer>(validateNonNegativeInt(name, value), value); + } catch (NumberFormatException e) { + Log.e(LOG_TAG, name + " bad value: " + stringValue, e); + retVal = new Pair<Boolean, Integer>(false, 0); + } + if (VDBG) log("parseNonNetativeInt: " + name + ", " + stringValue + ", " + + retVal.first + ", " + retVal.second); + return retVal; + } + + /** + * Validate an integer is >= 0 and logs an error if not + * + * @param name + * @param value + * @return Pair.first + */ + private boolean validateNonNegativeInt(String name, int value) { + boolean retVal; + if (value < 0) { + Log.e(LOG_TAG, name + " bad value: is < 0"); + retVal = false; + } else { + retVal = true; + } + if (VDBG) log("validateNonNegative: " + name + ", " + value + ", " + retVal); + return retVal; + } + + /** + * Return next random number for the index + */ + private int nextRandomizationTime(int index) { + int randomTime = mRetryArray.get(index).mRandomizationTime; + if (randomTime == 0) { + return 0; + } else { + return rng.nextInt(randomTime); + } + } + + private void log(String s) { + Log.d(LOG_TAG, "[RM] " + s); + } +} diff --git a/src/java/com/android/internal/telephony/SMSDispatcher.java b/src/java/com/android/internal/telephony/SMSDispatcher.java new file mode 100644 index 0000000..4a6c5dc --- /dev/null +++ b/src/java/com/android/internal/telephony/SMSDispatcher.java @@ -0,0 +1,1252 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Binder; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemProperties; +import android.provider.Telephony; +import android.provider.Telephony.Sms.Intents; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.telephony.SmsCbMessage; +import android.telephony.SmsMessage; +import android.telephony.TelephonyManager; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; +import android.view.WindowManager; + +import com.android.internal.R; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Random; + +import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE; +import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE; +import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED; +import static android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE; +import static android.telephony.SmsManager.RESULT_ERROR_NULL_PDU; +import static android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF; + +public abstract class SMSDispatcher extends Handler { + static final String TAG = "SMS"; // accessed from inner class + private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg"; + + /** Permission required to receive SMS and SMS-CB messages. */ + public static final String RECEIVE_SMS_PERMISSION = "android.permission.RECEIVE_SMS"; + + /** Permission required to receive ETWS and CMAS emergency broadcasts. */ + public static final String RECEIVE_EMERGENCY_BROADCAST_PERMISSION = + "android.permission.RECEIVE_EMERGENCY_BROADCAST"; + + /** Permission required to send SMS to short codes without user confirmation. */ + private static final String SEND_SMS_NO_CONFIRMATION_PERMISSION = + "android.permission.SEND_SMS_NO_CONFIRMATION"; + + /** Query projection for checking for duplicate message segments. */ + private static final String[] PDU_PROJECTION = new String[] { + "pdu" + }; + + /** Query projection for combining concatenated message segments. */ + private static final String[] PDU_SEQUENCE_PORT_PROJECTION = new String[] { + "pdu", + "sequence", + "destination_port" + }; + + private static final int PDU_COLUMN = 0; + private static final int SEQUENCE_COLUMN = 1; + private static final int DESTINATION_PORT_COLUMN = 2; + + /** New SMS received. */ + protected static final int EVENT_NEW_SMS = 1; + + /** SMS send complete. */ + protected static final int EVENT_SEND_SMS_COMPLETE = 2; + + /** Retry sending a previously failed SMS message */ + private static final int EVENT_SEND_RETRY = 3; + + /** Confirmation required for sending a large number of messages. */ + private static final int EVENT_SEND_LIMIT_REACHED_CONFIRMATION = 4; + + /** Send the user confirmed SMS */ + static final int EVENT_SEND_CONFIRMED_SMS = 5; // accessed from inner class + + /** Don't send SMS (user did not confirm). */ + static final int EVENT_STOP_SENDING = 7; // accessed from inner class + + /** Confirmation required for third-party apps sending to an SMS short code. */ + private static final int EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE = 8; + + /** Confirmation required for third-party apps sending to an SMS short code. */ + private static final int EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE = 9; + + protected final Phone mPhone; + protected final Context mContext; + protected final ContentResolver mResolver; + protected final CommandsInterface mCm; + protected final SmsStorageMonitor mStorageMonitor; + protected final TelephonyManager mTelephonyManager; + + protected final WapPushOverSms mWapPush; + + protected static final Uri mRawUri = Uri.withAppendedPath(Telephony.Sms.CONTENT_URI, "raw"); + + /** Maximum number of times to retry sending a failed SMS. */ + private static final int MAX_SEND_RETRIES = 3; + /** Delay before next send attempt on a failed SMS, in milliseconds. */ + private static final int SEND_RETRY_DELAY = 2000; + /** single part SMS */ + private static final int SINGLE_PART_SMS = 1; + /** Message sending queue limit */ + private static final int MO_MSG_QUEUE_LIMIT = 5; + + /** + * Message reference for a CONCATENATED_8_BIT_REFERENCE or + * CONCATENATED_16_BIT_REFERENCE message set. Should be + * incremented for each set of concatenated messages. + * Static field shared by all dispatcher objects. + */ + private static int sConcatenatedRef = new Random().nextInt(256); + + /** Outgoing message counter. Shared by all dispatchers. */ + private final SmsUsageMonitor mUsageMonitor; + + /** Number of outgoing SmsTrackers waiting for user confirmation. */ + private int mPendingTrackerCount; + + /** Wake lock to ensure device stays awake while dispatching the SMS intent. */ + private PowerManager.WakeLock mWakeLock; + + /** + * Hold the wake lock for 5 seconds, which should be enough time for + * any receiver(s) to grab its own wake lock. + */ + private static final int WAKE_LOCK_TIMEOUT = 5000; + + /* Flags indicating whether the current device allows sms service */ + protected boolean mSmsCapable = true; + protected boolean mSmsReceiveDisabled; + protected boolean mSmsSendDisabled; + + protected int mRemainingMessages = -1; + + protected static int getNextConcatenatedRef() { + sConcatenatedRef += 1; + return sConcatenatedRef; + } + + /** + * Create a new SMS dispatcher. + * @param phone the Phone to use + * @param storageMonitor the SmsStorageMonitor to use + * @param usageMonitor the SmsUsageMonitor to use + */ + protected SMSDispatcher(PhoneBase phone, SmsStorageMonitor storageMonitor, + SmsUsageMonitor usageMonitor) { + mPhone = phone; + mWapPush = new WapPushOverSms(phone, this); + mContext = phone.getContext(); + mResolver = mContext.getContentResolver(); + mCm = phone.mCM; + mStorageMonitor = storageMonitor; + mUsageMonitor = usageMonitor; + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + + createWakelock(); + + mSmsCapable = mContext.getResources().getBoolean( + com.android.internal.R.bool.config_sms_capable); + mSmsReceiveDisabled = !SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_SMS_RECEIVE, mSmsCapable); + mSmsSendDisabled = !SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_SMS_SEND, mSmsCapable); + Log.d(TAG, "SMSDispatcher: ctor mSmsCapable=" + mSmsCapable + " format=" + getFormat() + + " mSmsReceiveDisabled=" + mSmsReceiveDisabled + + " mSmsSendDisabled=" + mSmsSendDisabled); + } + + /** Unregister for incoming SMS events. */ + public abstract void dispose(); + + /** + * The format of the message PDU in the associated broadcast intent. + * This will be either "3gpp" for GSM/UMTS/LTE messages in 3GPP format + * or "3gpp2" for CDMA/LTE messages in 3GPP2 format. + * + * Note: All applications which handle incoming SMS messages by processing the + * SMS_RECEIVED_ACTION broadcast intent MUST pass the "format" extra from the intent + * into the new methods in {@link android.telephony.SmsMessage} which take an + * extra format parameter. This is required in order to correctly decode the PDU on + * devices which require support for both 3GPP and 3GPP2 formats at the same time, + * such as CDMA/LTE devices and GSM/CDMA world phones. + * + * @return the format of the message PDU + */ + protected abstract String getFormat(); + + @Override + protected void finalize() { + Log.d(TAG, "SMSDispatcher finalized"); + } + + + /* TODO: Need to figure out how to keep track of status report routing in a + * persistent manner. If the phone process restarts (reboot or crash), + * we will lose this list and any status reports that come in after + * will be dropped. + */ + /** Sent messages awaiting a delivery status report. */ + protected final ArrayList<SmsTracker> deliveryPendingList = new ArrayList<SmsTracker>(); + + /** + * Handles events coming from the phone stack. Overridden from handler. + * + * @param msg the message to handle + */ + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_NEW_SMS: + // A new SMS has been received by the device + if (false) { + Log.d(TAG, "New SMS Message Received"); + } + + SmsMessage sms; + + ar = (AsyncResult) msg.obj; + + if (ar.exception != null) { + Log.e(TAG, "Exception processing incoming SMS. Exception:" + ar.exception); + return; + } + + sms = (SmsMessage) ar.result; + try { + int result = dispatchMessage(sms.mWrappedSmsMessage); + if (result != Activity.RESULT_OK) { + // RESULT_OK means that message was broadcast for app(s) to handle. + // Any other result, we should ack here. + boolean handled = (result == Intents.RESULT_SMS_HANDLED); + notifyAndAcknowledgeLastIncomingSms(handled, result, null); + } + } catch (RuntimeException ex) { + Log.e(TAG, "Exception dispatching message", ex); + notifyAndAcknowledgeLastIncomingSms(false, Intents.RESULT_SMS_GENERIC_ERROR, null); + } + + break; + + case EVENT_SEND_SMS_COMPLETE: + // An outbound SMS has been successfully transferred, or failed. + handleSendComplete((AsyncResult) msg.obj); + break; + + case EVENT_SEND_RETRY: + sendSms((SmsTracker) msg.obj); + break; + + case EVENT_SEND_LIMIT_REACHED_CONFIRMATION: + handleReachSentLimit((SmsTracker)(msg.obj)); + break; + + case EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE: + handleConfirmShortCode(false, (SmsTracker)(msg.obj)); + break; + + case EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE: + handleConfirmShortCode(true, (SmsTracker)(msg.obj)); + break; + + case EVENT_SEND_CONFIRMED_SMS: + { + SmsTracker tracker = (SmsTracker) msg.obj; + if (tracker.isMultipart()) { + sendMultipartSms(tracker); + } else { + sendSms(tracker); + } + mPendingTrackerCount--; + break; + } + + case EVENT_STOP_SENDING: + { + SmsTracker tracker = (SmsTracker) msg.obj; + if (tracker.mSentIntent != null) { + try { + tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); + } catch (CanceledException ex) { + Log.e(TAG, "failed to send RESULT_ERROR_LIMIT_EXCEEDED"); + } + } + mPendingTrackerCount--; + break; + } + } + } + + private void createWakelock() { + PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SMSDispatcher"); + mWakeLock.setReferenceCounted(true); + } + + /** + * Grabs a wake lock and sends intent as an ordered broadcast. + * The resultReceiver will check for errors and ACK/NACK back + * to the RIL. + * + * @param intent intent to broadcast + * @param permission Receivers are required to have this permission + */ + public void dispatch(Intent intent, String permission) { + // Hold a wake lock for WAKE_LOCK_TIMEOUT seconds, enough to give any + // receivers time to take their own wake locks. + mWakeLock.acquire(WAKE_LOCK_TIMEOUT); + mContext.sendOrderedBroadcast(intent, permission, mResultReceiver, + this, Activity.RESULT_OK, null, null); + } + + /** + * Called when SMS send completes. Broadcasts a sentIntent on success. + * On failure, either sets up retries or broadcasts a sentIntent with + * the failure in the result code. + * + * @param ar AsyncResult passed into the message handler. ar.result should + * an SmsResponse instance if send was successful. ar.userObj + * should be an SmsTracker instance. + */ + protected void handleSendComplete(AsyncResult ar) { + SmsTracker tracker = (SmsTracker) ar.userObj; + PendingIntent sentIntent = tracker.mSentIntent; + + if (ar.exception == null) { + if (false) { + Log.d(TAG, "SMS send complete. Broadcasting " + + "intent: " + sentIntent); + } + + if (tracker.mDeliveryIntent != null) { + // Expecting a status report. Add it to the list. + int messageRef = ((SmsResponse)ar.result).messageRef; + tracker.mMessageRef = messageRef; + deliveryPendingList.add(tracker); + } + + if (sentIntent != null) { + try { + if (mRemainingMessages > -1) { + mRemainingMessages--; + } + + if (mRemainingMessages == 0) { + Intent sendNext = new Intent(); + sendNext.putExtra(SEND_NEXT_MSG_EXTRA, true); + sentIntent.send(mContext, Activity.RESULT_OK, sendNext); + } else { + sentIntent.send(Activity.RESULT_OK); + } + } catch (CanceledException ex) {} + } + } else { + if (false) { + Log.d(TAG, "SMS send failed"); + } + + int ss = mPhone.getServiceState().getState(); + + if (ss != ServiceState.STATE_IN_SERVICE) { + handleNotInService(ss, tracker.mSentIntent); + } else if ((((CommandException)(ar.exception)).getCommandError() + == CommandException.Error.SMS_FAIL_RETRY) && + tracker.mRetryCount < MAX_SEND_RETRIES) { + // Retry after a delay if needed. + // TODO: According to TS 23.040, 9.2.3.6, we should resend + // with the same TP-MR as the failed message, and + // TP-RD set to 1. However, we don't have a means of + // knowing the MR for the failed message (EF_SMSstatus + // may or may not have the MR corresponding to this + // message, depending on the failure). Also, in some + // implementations this retry is handled by the baseband. + tracker.mRetryCount++; + Message retryMsg = obtainMessage(EVENT_SEND_RETRY, tracker); + sendMessageDelayed(retryMsg, SEND_RETRY_DELAY); + } else if (tracker.mSentIntent != null) { + int error = RESULT_ERROR_GENERIC_FAILURE; + + if (((CommandException)(ar.exception)).getCommandError() + == CommandException.Error.FDN_CHECK_FAILURE) { + error = RESULT_ERROR_FDN_CHECK_FAILURE; + } + // Done retrying; return an error to the app. + try { + Intent fillIn = new Intent(); + if (ar.result != null) { + fillIn.putExtra("errorCode", ((SmsResponse)ar.result).errorCode); + } + if (mRemainingMessages > -1) { + mRemainingMessages--; + } + + if (mRemainingMessages == 0) { + fillIn.putExtra(SEND_NEXT_MSG_EXTRA, true); + } + + tracker.mSentIntent.send(mContext, error, fillIn); + } catch (CanceledException ex) {} + } + } + } + + /** + * Handles outbound message when the phone is not in service. + * + * @param ss Current service state. Valid values are: + * OUT_OF_SERVICE + * EMERGENCY_ONLY + * POWER_OFF + * @param sentIntent the PendingIntent to send the error to + */ + protected static void handleNotInService(int ss, PendingIntent sentIntent) { + if (sentIntent != null) { + try { + if (ss == ServiceState.STATE_POWER_OFF) { + sentIntent.send(RESULT_ERROR_RADIO_OFF); + } else { + sentIntent.send(RESULT_ERROR_NO_SERVICE); + } + } catch (CanceledException ex) {} + } + } + + /** + * Dispatches an incoming SMS messages. + * + * @param sms the incoming message from the phone + * @return a result code from {@link Telephony.Sms.Intents}, or + * {@link Activity#RESULT_OK} if the message has been broadcast + * to applications + */ + public abstract int dispatchMessage(SmsMessageBase sms); + + /** + * Dispatch a normal incoming SMS. This is called from the format-specific + * {@link #dispatchMessage(SmsMessageBase)} if no format-specific handling is required. + * + * @param sms + * @return + */ + protected int dispatchNormalMessage(SmsMessageBase sms) { + SmsHeader smsHeader = sms.getUserDataHeader(); + + // See if message is partial or port addressed. + if ((smsHeader == null) || (smsHeader.concatRef == null)) { + // Message is not partial (not part of concatenated sequence). + byte[][] pdus = new byte[1][]; + pdus[0] = sms.getPdu(); + + if (smsHeader != null && smsHeader.portAddrs != null) { + if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { + // GSM-style WAP indication + return mWapPush.dispatchWapPdu(sms.getUserData()); + } else { + // The message was sent to a port, so concoct a URI for it. + dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); + } + } else { + // Normal short and non-port-addressed message, dispatch it. + dispatchPdus(pdus); + } + return Activity.RESULT_OK; + } else { + // Process the message part. + SmsHeader.ConcatRef concatRef = smsHeader.concatRef; + SmsHeader.PortAddrs portAddrs = smsHeader.portAddrs; + return processMessagePart(sms.getPdu(), sms.getOriginatingAddress(), + concatRef.refNumber, concatRef.seqNumber, concatRef.msgCount, + sms.getTimestampMillis(), (portAddrs != null ? portAddrs.destPort : -1), false); + } + } + + /** + * If this is the last part send the parts out to the application, otherwise + * the part is stored for later processing. Handles both 3GPP concatenated messages + * as well as 3GPP2 format WAP push messages processed by + * {@link com.android.internal.telephony.cdma.CdmaSMSDispatcher#processCdmaWapPdu}. + * + * @param pdu the message PDU, or the datagram portion of a CDMA WDP datagram segment + * @param address the originating address + * @param referenceNumber distinguishes concatenated messages from the same sender + * @param sequenceNumber the order of this segment in the message + * (starting at 0 for CDMA WDP datagrams and 1 for concatenated messages). + * @param messageCount the number of segments in the message + * @param timestamp the service center timestamp in millis + * @param destPort the destination port for the message, or -1 for no destination port + * @param isCdmaWapPush true if pdu is a CDMA WDP datagram segment and not an SM PDU + * + * @return a result code from {@link Telephony.Sms.Intents}, or + * {@link Activity#RESULT_OK} if the message has been broadcast + * to applications + */ + protected int processMessagePart(byte[] pdu, String address, int referenceNumber, + int sequenceNumber, int messageCount, long timestamp, int destPort, + boolean isCdmaWapPush) { + byte[][] pdus = null; + Cursor cursor = null; + try { + // used by several query selection arguments + String refNumber = Integer.toString(referenceNumber); + String seqNumber = Integer.toString(sequenceNumber); + + // Check for duplicate message segment + cursor = mResolver.query(mRawUri, PDU_PROJECTION, + "address=? AND reference_number=? AND sequence=?", + new String[] {address, refNumber, seqNumber}, null); + + // moveToNext() returns false if no duplicates were found + if (cursor.moveToNext()) { + Log.w(TAG, "Discarding duplicate message segment from address=" + address + + " refNumber=" + refNumber + " seqNumber=" + seqNumber); + String oldPduString = cursor.getString(PDU_COLUMN); + byte[] oldPdu = HexDump.hexStringToByteArray(oldPduString); + if (!Arrays.equals(oldPdu, pdu)) { + Log.e(TAG, "Warning: dup message segment PDU of length " + pdu.length + + " is different from existing PDU of length " + oldPdu.length); + } + return Intents.RESULT_SMS_HANDLED; + } + cursor.close(); + + // not a dup, query for all other segments of this concatenated message + String where = "address=? AND reference_number=?"; + String[] whereArgs = new String[] {address, refNumber}; + cursor = mResolver.query(mRawUri, PDU_SEQUENCE_PORT_PROJECTION, where, whereArgs, null); + + int cursorCount = cursor.getCount(); + if (cursorCount != messageCount - 1) { + // We don't have all the parts yet, store this one away + ContentValues values = new ContentValues(); + values.put("date", timestamp); + values.put("pdu", HexDump.toHexString(pdu)); + values.put("address", address); + values.put("reference_number", referenceNumber); + values.put("count", messageCount); + values.put("sequence", sequenceNumber); + if (destPort != -1) { + values.put("destination_port", destPort); + } + mResolver.insert(mRawUri, values); + return Intents.RESULT_SMS_HANDLED; + } + + // All the parts are in place, deal with them + pdus = new byte[messageCount][]; + for (int i = 0; i < cursorCount; i++) { + cursor.moveToNext(); + int cursorSequence = cursor.getInt(SEQUENCE_COLUMN); + // GSM sequence numbers start at 1; CDMA WDP datagram sequence numbers start at 0 + if (!isCdmaWapPush) { + cursorSequence--; + } + pdus[cursorSequence] = HexDump.hexStringToByteArray( + cursor.getString(PDU_COLUMN)); + + // Read the destination port from the first segment (needed for CDMA WAP PDU). + // It's not a bad idea to prefer the port from the first segment for 3GPP as well. + if (cursorSequence == 0 && !cursor.isNull(DESTINATION_PORT_COLUMN)) { + destPort = cursor.getInt(DESTINATION_PORT_COLUMN); + } + } + // This one isn't in the DB, so add it + // GSM sequence numbers start at 1; CDMA WDP datagram sequence numbers start at 0 + if (isCdmaWapPush) { + pdus[sequenceNumber] = pdu; + } else { + pdus[sequenceNumber - 1] = pdu; + } + + // Remove the parts from the database + mResolver.delete(mRawUri, where, whereArgs); + } catch (SQLException e) { + Log.e(TAG, "Can't access multipart SMS database", e); + return Intents.RESULT_SMS_GENERIC_ERROR; + } finally { + if (cursor != null) cursor.close(); + } + + // Special handling for CDMA WDP datagrams + if (isCdmaWapPush) { + // Build up the data stream + ByteArrayOutputStream output = new ByteArrayOutputStream(); + for (int i = 0; i < messageCount; i++) { + // reassemble the (WSP-)pdu + output.write(pdus[i], 0, pdus[i].length); + } + byte[] datagram = output.toByteArray(); + + // Dispatch the PDU to applications + if (destPort == SmsHeader.PORT_WAP_PUSH) { + // Handle the PUSH + return mWapPush.dispatchWapPdu(datagram); + } else { + pdus = new byte[1][]; + pdus[0] = datagram; + // The messages were sent to any other WAP port + dispatchPortAddressedPdus(pdus, destPort); + return Activity.RESULT_OK; + } + } + + // Dispatch the PDUs to applications + if (destPort != -1) { + if (destPort == SmsHeader.PORT_WAP_PUSH) { + // Build up the data stream + ByteArrayOutputStream output = new ByteArrayOutputStream(); + for (int i = 0; i < messageCount; i++) { + SmsMessage msg = SmsMessage.createFromPdu(pdus[i], getFormat()); + byte[] data = msg.getUserData(); + output.write(data, 0, data.length); + } + // Handle the PUSH + return mWapPush.dispatchWapPdu(output.toByteArray()); + } else { + // The messages were sent to a port, so concoct a URI for it + dispatchPortAddressedPdus(pdus, destPort); + } + } else { + // The messages were not sent to a port + dispatchPdus(pdus); + } + return Activity.RESULT_OK; + } + + /** + * Dispatches standard PDUs to interested applications + * + * @param pdus The raw PDUs making up the message + */ + protected void dispatchPdus(byte[][] pdus) { + Intent intent = new Intent(Intents.SMS_RECEIVED_ACTION); + intent.putExtra("pdus", pdus); + intent.putExtra("format", getFormat()); + dispatch(intent, RECEIVE_SMS_PERMISSION); + } + + /** + * Dispatches port addressed PDUs to interested applications + * + * @param pdus The raw PDUs making up the message + * @param port The destination port of the messages + */ + protected void dispatchPortAddressedPdus(byte[][] pdus, int port) { + Uri uri = Uri.parse("sms://localhost:" + port); + Intent intent = new Intent(Intents.DATA_SMS_RECEIVED_ACTION, uri); + intent.putExtra("pdus", pdus); + intent.putExtra("format", getFormat()); + dispatch(intent, RECEIVE_SMS_PERMISSION); + } + + /** + * Send a data based SMS to a specific application port. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param destPort the port to deliver the message to + * @param data the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * <code>RESULT_ERROR_NO_SERVICE</code><br>. + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + protected abstract void sendData(String destAddr, String scAddr, int destPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent); + + /** + * Send a text based SMS. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param text the body of the message to send + * @param sentIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors:<br> + * <code>RESULT_ERROR_GENERIC_FAILURE</code><br> + * <code>RESULT_ERROR_RADIO_OFF</code><br> + * <code>RESULT_ERROR_NULL_PDU</code><br> + * <code>RESULT_ERROR_NO_SERVICE</code><br>. + * For <code>RESULT_ERROR_GENERIC_FAILURE</code> the sentIntent may include + * the extra "errorCode" containing a radio technology specific value, + * generally only useful for troubleshooting.<br> + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>PendingIntent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + */ + protected abstract void sendText(String destAddr, String scAddr, + String text, PendingIntent sentIntent, PendingIntent deliveryIntent); + + /** + * Calculate the number of septets needed to encode the message. + * + * @param messageBody the message to encode + * @param use7bitOnly ignore (but still count) illegal characters if true + * @return TextEncodingDetails + */ + protected abstract TextEncodingDetails calculateLength(CharSequence messageBody, + boolean use7bitOnly); + + /** + * Send a multi-part text based SMS. + * + * @param destAddr the address to send the message to + * @param scAddr is the service center address or null to use + * the current default SMSC + * @param parts an <code>ArrayList</code> of strings that, in order, + * comprise the original message + * @param sentIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been sent. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code> + * <code>RESULT_ERROR_NO_SERVICE</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntents if not null, an <code>ArrayList</code> of + * <code>PendingIntent</code>s (one for each message part) that is + * broadcast when the corresponding message part has been delivered + * to the recipient. The raw pdu of the status report is in the + * extended data ("pdu"). + */ + protected void sendMultipartText(String destAddr, String scAddr, + ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, + ArrayList<PendingIntent> deliveryIntents) { + + int refNumber = getNextConcatenatedRef() & 0x00FF; + int msgCount = parts.size(); + int encoding = SmsConstants.ENCODING_UNKNOWN; + + mRemainingMessages = msgCount; + + TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount]; + for (int i = 0; i < msgCount; i++) { + TextEncodingDetails details = calculateLength(parts.get(i), false); + if (encoding != details.codeUnitSize + && (encoding == SmsConstants.ENCODING_UNKNOWN + || encoding == SmsConstants.ENCODING_7BIT)) { + encoding = details.codeUnitSize; + } + encodingForParts[i] = details; + } + + for (int i = 0; i < msgCount; i++) { + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = refNumber; + concatRef.seqNumber = i + 1; // 1-based sequence + concatRef.msgCount = msgCount; + // TODO: We currently set this to true since our messaging app will never + // send more than 255 parts (it converts the message to MMS well before that). + // However, we should support 3rd party messaging apps that might need 16-bit + // references + // Note: It's not sufficient to just flip this bit to true; it will have + // ripple effects (several calculations assume 8-bit ref). + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + + // Set the national language tables for 3GPP 7-bit encoding, if enabled. + if (encoding == SmsConstants.ENCODING_7BIT) { + smsHeader.languageTable = encodingForParts[i].languageTable; + smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable; + } + + PendingIntent sentIntent = null; + if (sentIntents != null && sentIntents.size() > i) { + sentIntent = sentIntents.get(i); + } + + PendingIntent deliveryIntent = null; + if (deliveryIntents != null && deliveryIntents.size() > i) { + deliveryIntent = deliveryIntents.get(i); + } + + sendNewSubmitPdu(destAddr, scAddr, parts.get(i), smsHeader, encoding, + sentIntent, deliveryIntent, (i == (msgCount - 1))); + } + + } + + /** + * Create a new SubmitPdu and send it. + */ + protected abstract void sendNewSubmitPdu(String destinationAddress, String scAddress, + String message, SmsHeader smsHeader, int encoding, + PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart); + + /** + * Send a SMS + * + * @param smsc the SMSC to send the message through, or NULL for the + * default SMSC + * @param pdu the raw PDU to send + * @param sentIntent if not NULL this <code>Intent</code> is + * broadcast when the message is successfully sent, or failed. + * The result code will be <code>Activity.RESULT_OK<code> for success, + * or one of these errors: + * <code>RESULT_ERROR_GENERIC_FAILURE</code> + * <code>RESULT_ERROR_RADIO_OFF</code> + * <code>RESULT_ERROR_NULL_PDU</code> + * <code>RESULT_ERROR_NO_SERVICE</code>. + * The per-application based SMS control checks sentIntent. If sentIntent + * is NULL the caller will be checked against all unknown applications, + * which cause smaller number of SMS to be sent in checking period. + * @param deliveryIntent if not NULL this <code>Intent</code> is + * broadcast when the message is delivered to the recipient. The + * raw pdu of the status report is in the extended data ("pdu"). + * @param destAddr the destination phone number (for short code confirmation) + */ + protected void sendRawPdu(byte[] smsc, byte[] pdu, PendingIntent sentIntent, + PendingIntent deliveryIntent, String destAddr) { + if (mSmsSendDisabled) { + if (sentIntent != null) { + try { + sentIntent.send(RESULT_ERROR_NO_SERVICE); + } catch (CanceledException ex) {} + } + Log.d(TAG, "Device does not support sending sms."); + return; + } + + if (pdu == null) { + if (sentIntent != null) { + try { + sentIntent.send(RESULT_ERROR_NULL_PDU); + } catch (CanceledException ex) {} + } + return; + } + + HashMap<String, Object> map = new HashMap<String, Object>(); + map.put("smsc", smsc); + map.put("pdu", pdu); + + // Get calling app package name via UID from Binder call + PackageManager pm = mContext.getPackageManager(); + String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid()); + + if (packageNames == null || packageNames.length == 0) { + // Refuse to send SMS if we can't get the calling package name. + Log.e(TAG, "Can't get calling app package name: refusing to send SMS"); + if (sentIntent != null) { + try { + sentIntent.send(RESULT_ERROR_GENERIC_FAILURE); + } catch (CanceledException ex) { + Log.e(TAG, "failed to send error result"); + } + } + return; + } + + String appPackage = packageNames[0]; + + // Strip non-digits from destination phone number before checking for short codes + // and before displaying the number to the user if confirmation is required. + SmsTracker tracker = new SmsTracker(map, sentIntent, deliveryIntent, appPackage, + PhoneNumberUtils.extractNetworkPortion(destAddr)); + + // checkDestination() returns true if the destination is not a premium short code or the + // sending app is approved to send to short codes. Otherwise, a message is sent to our + // handler with the SmsTracker to request user confirmation before sending. + if (checkDestination(tracker)) { + // check for excessive outgoing SMS usage by this app + if (!mUsageMonitor.check(appPackage, SINGLE_PART_SMS)) { + sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker)); + return; + } + + int ss = mPhone.getServiceState().getState(); + + if (ss != ServiceState.STATE_IN_SERVICE) { + handleNotInService(ss, tracker.mSentIntent); + } else { + sendSms(tracker); + } + } + } + + /** + * Check if destination is a potential premium short code and sender is not pre-approved to + * send to short codes. + * + * @param tracker the tracker for the SMS to send + * @return true if the destination is approved; false if user confirmation event was sent + */ + boolean checkDestination(SmsTracker tracker) { + if (mContext.checkCallingOrSelfPermission(SEND_SMS_NO_CONFIRMATION_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + return true; // app is pre-approved to send to short codes + } else { + String countryIso = mTelephonyManager.getSimCountryIso(); + if (countryIso == null || countryIso.length() != 2) { + Log.e(TAG, "Can't get SIM country code: trying network country code"); + countryIso = mTelephonyManager.getNetworkCountryIso(); + } + + switch (mUsageMonitor.checkDestination(tracker.mDestAddress, countryIso)) { + case SmsUsageMonitor.CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE: + sendMessage(obtainMessage(EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE, + tracker)); + return false; // wait for user confirmation before sending + + case SmsUsageMonitor.CATEGORY_PREMIUM_SHORT_CODE: + sendMessage(obtainMessage(EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE, + tracker)); + return false; // wait for user confirmation before sending + + case SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE: + case SmsUsageMonitor.CATEGORY_FREE_SHORT_CODE: + case SmsUsageMonitor.CATEGORY_STANDARD_SHORT_CODE: + default: + return true; // destination is not a premium short code + } + } + } + + /** + * Deny sending an SMS if the outgoing queue limit is reached. Used when the message + * must be confirmed by the user due to excessive usage or potential premium SMS detected. + * @param tracker the SmsTracker for the message to send + * @return true if the message was denied; false to continue with send confirmation + */ + private boolean denyIfQueueLimitReached(SmsTracker tracker) { + if (mPendingTrackerCount >= MO_MSG_QUEUE_LIMIT) { + // Deny sending message when the queue limit is reached. + try { + tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); + } catch (CanceledException ex) { + Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); + } + return true; + } + mPendingTrackerCount++; + return false; + } + + /** + * Returns the label for the specified app package name. + * @param appPackage the package name of the app requesting to send an SMS + * @return the label for the specified app, or the package name if getApplicationInfo() fails + */ + private CharSequence getAppLabel(String appPackage) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo appInfo = pm.getApplicationInfo(appPackage, 0); + return appInfo.loadLabel(pm); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "PackageManager Name Not Found for package " + appPackage); + return appPackage; // fall back to package name if we can't get app label + } + } + + /** + * Post an alert when SMS needs confirmation due to excessive usage. + * @param tracker an SmsTracker for the current message. + */ + protected void handleReachSentLimit(SmsTracker tracker) { + if (denyIfQueueLimitReached(tracker)) { + return; // queue limit reached; error was returned to caller + } + + CharSequence appLabel = getAppLabel(tracker.mAppPackage); + Resources r = Resources.getSystem(); + Spanned messageText = Html.fromHtml(r.getString(R.string.sms_control_message, appLabel)); + + ConfirmDialogListener listener = new ConfirmDialogListener(tracker); + + AlertDialog d = new AlertDialog.Builder(mContext) + .setTitle(R.string.sms_control_title) + .setIcon(R.drawable.stat_sys_warning) + .setMessage(messageText) + .setPositiveButton(r.getString(R.string.sms_control_yes), listener) + .setNegativeButton(r.getString(R.string.sms_control_no), listener) + .setOnCancelListener(listener) + .create(); + + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.show(); + } + + /** + * Post an alert for user confirmation when sending to a potential short code. + * @param isPremium true if the destination is known to be a premium short code + * @param tracker the SmsTracker for the current message. + */ + protected void handleConfirmShortCode(boolean isPremium, SmsTracker tracker) { + if (denyIfQueueLimitReached(tracker)) { + return; // queue limit reached; error was returned to caller + } + + int messageId; + int titleId; + if (isPremium) { + messageId = R.string.sms_premium_short_code_confirm_message; + titleId = R.string.sms_premium_short_code_confirm_title; + } else { + messageId = R.string.sms_short_code_confirm_message; + titleId = R.string.sms_short_code_confirm_title; + } + + CharSequence appLabel = getAppLabel(tracker.mAppPackage); + Resources r = Resources.getSystem(); + Spanned messageText = Html.fromHtml(r.getString(messageId, appLabel, tracker.mDestAddress)); + + ConfirmDialogListener listener = new ConfirmDialogListener(tracker); + + AlertDialog d = new AlertDialog.Builder(mContext) + .setTitle(titleId) + .setIcon(R.drawable.stat_sys_warning) + .setMessage(messageText) + .setPositiveButton(r.getString(R.string.sms_short_code_confirm_allow), listener) + .setNegativeButton(r.getString(R.string.sms_short_code_confirm_deny), listener) +// TODO: add third button for "Report malicious app" feature +// .setNeutralButton(r.getString(R.string.sms_short_code_confirm_report), listener) + .setOnCancelListener(listener) + .create(); + + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.show(); + } + + /** + * Send the message along to the radio. + * + * @param tracker holds the SMS message to send + */ + protected abstract void sendSms(SmsTracker tracker); + + /** + * Send the multi-part SMS based on multipart Sms tracker + * + * @param tracker holds the multipart Sms tracker ready to be sent + */ + private void sendMultipartSms(SmsTracker tracker) { + ArrayList<String> parts; + ArrayList<PendingIntent> sentIntents; + ArrayList<PendingIntent> deliveryIntents; + + HashMap<String, Object> map = tracker.mData; + + String destinationAddress = (String) map.get("destination"); + String scAddress = (String) map.get("scaddress"); + + parts = (ArrayList<String>) map.get("parts"); + sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents"); + deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents"); + + // check if in service + int ss = mPhone.getServiceState().getState(); + if (ss != ServiceState.STATE_IN_SERVICE) { + for (int i = 0, count = parts.size(); i < count; i++) { + PendingIntent sentIntent = null; + if (sentIntents != null && sentIntents.size() > i) { + sentIntent = sentIntents.get(i); + } + handleNotInService(ss, sentIntent); + } + return; + } + + sendMultipartText(destinationAddress, scAddress, parts, sentIntents, deliveryIntents); + } + + /** + * Send an acknowledge message. + * @param success indicates that last message was successfully received. + * @param result result code indicating any error + * @param response callback message sent when operation completes. + */ + protected abstract void acknowledgeLastIncomingSms(boolean success, + int result, Message response); + + /** + * Notify interested apps if the framework has rejected an incoming SMS, + * and send an acknowledge message to the network. + * @param success indicates that last message was successfully received. + * @param result result code indicating any error + * @param response callback message sent when operation completes. + */ + private void notifyAndAcknowledgeLastIncomingSms(boolean success, + int result, Message response) { + if (!success) { + // broadcast SMS_REJECTED_ACTION intent + Intent intent = new Intent(Intents.SMS_REJECTED_ACTION); + intent.putExtra("result", result); + mWakeLock.acquire(WAKE_LOCK_TIMEOUT); + mContext.sendBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + acknowledgeLastIncomingSms(success, result, response); + } + + /** + * Keeps track of an SMS that has been sent to the RIL, until it has + * successfully been sent, or we're done trying. + * + */ + protected static final class SmsTracker { + // fields need to be public for derived SmsDispatchers + public final HashMap<String, Object> mData; + public int mRetryCount; + public int mMessageRef; + + public final PendingIntent mSentIntent; + public final PendingIntent mDeliveryIntent; + + public final String mAppPackage; + public final String mDestAddress; + + public SmsTracker(HashMap<String, Object> data, PendingIntent sentIntent, + PendingIntent deliveryIntent, String appPackage, String destAddr) { + mData = data; + mSentIntent = sentIntent; + mDeliveryIntent = deliveryIntent; + mRetryCount = 0; + mAppPackage = appPackage; + mDestAddress = destAddr; + } + + /** + * Returns whether this tracker holds a multi-part SMS. + * @return true if the tracker holds a multi-part SMS; false otherwise + */ + protected boolean isMultipart() { + HashMap map = mData; + return map.containsKey("parts"); + } + } + + /** + * Dialog listener for SMS confirmation dialog. + */ + private final class ConfirmDialogListener + implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { + + private final SmsTracker mTracker; + + ConfirmDialogListener(SmsTracker tracker) { + mTracker = tracker; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + Log.d(TAG, "CONFIRM sending SMS"); + sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS, mTracker)); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + Log.d(TAG, "DENY sending SMS"); + sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); + } + } + + @Override + public void onCancel(DialogInterface dialog) { + Log.d(TAG, "dialog dismissed: don't send SMS"); + sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); + } + } + + private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Assume the intent is one of the SMS receive intents that + // was sent as an ordered broadcast. Check result and ACK. + int rc = getResultCode(); + boolean success = (rc == Activity.RESULT_OK) + || (rc == Intents.RESULT_SMS_HANDLED); + + // For a multi-part message, this only ACKs the last part. + // Previous parts were ACK'd as they were received. + acknowledgeLastIncomingSms(success, rc, null); + } + }; + + protected void dispatchBroadcastMessage(SmsCbMessage message) { + if (message.isEmergencyMessage()) { + Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", message); + Log.d(TAG, "Dispatching emergency SMS CB"); + dispatch(intent, RECEIVE_EMERGENCY_BROADCAST_PERMISSION); + } else { + Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); + intent.putExtra("message", message); + Log.d(TAG, "Dispatching SMS CB"); + dispatch(intent, RECEIVE_SMS_PERMISSION); + } + } +} diff --git a/src/java/com/android/internal/telephony/ServiceStateTracker.java b/src/java/com/android/internal/telephony/ServiceStateTracker.java new file mode 100644 index 0000000..e4cfb23 --- /dev/null +++ b/src/java/com/android/internal/telephony/ServiceStateTracker.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.util.TimeUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * {@hide} + */ +public abstract class ServiceStateTracker extends Handler { + + protected CommandsInterface cm; + + public ServiceState ss; + protected ServiceState newSS; + + public SignalStrength mSignalStrength; + + // TODO - this should not be public + public RestrictedState mRestrictedState = new RestrictedState(); + + /* The otaspMode passed to PhoneStateListener#onOtaspChanged */ + static public final int OTASP_UNINITIALIZED = 0; + static public final int OTASP_UNKNOWN = 1; + static public final int OTASP_NEEDED = 2; + static public final int OTASP_NOT_NEEDED = 3; + + /** + * A unique identifier to track requests associated with a poll + * and ignore stale responses. The value is a count-down of + * expected responses in this pollingContext. + */ + protected int[] pollingContext; + protected boolean mDesiredPowerState; + + /** + * Values correspond to ServiceState.RIL_RADIO_TECHNOLOGY_ definitions. + */ + protected int mRilRadioTechnology = 0; + protected int mNewRilRadioTechnology = 0; + + /** + * By default, strength polling is enabled. However, if we're + * getting unsolicited signal strength updates from the radio, set + * value to true and don't bother polling any more. + */ + protected boolean dontPollSignalStrength = false; + + protected RegistrantList mRoamingOnRegistrants = new RegistrantList(); + protected RegistrantList mRoamingOffRegistrants = new RegistrantList(); + protected RegistrantList mAttachedRegistrants = new RegistrantList(); + protected RegistrantList mDetachedRegistrants = new RegistrantList(); + protected RegistrantList mNetworkAttachedRegistrants = new RegistrantList(); + protected RegistrantList mPsRestrictEnabledRegistrants = new RegistrantList(); + protected RegistrantList mPsRestrictDisabledRegistrants = new RegistrantList(); + + /* Radio power off pending flag and tag counter */ + private boolean mPendingRadioPowerOffAfterDataOff = false; + private int mPendingRadioPowerOffAfterDataOffTag = 0; + + protected static final boolean DBG = true; + + /** Signal strength poll rate. */ + protected static final int POLL_PERIOD_MILLIS = 20 * 1000; + + /** Waiting period before recheck gprs and voice registration. */ + public static final int DEFAULT_GPRS_CHECK_PERIOD_MILLIS = 60 * 1000; + + /** GSM events */ + protected static final int EVENT_RADIO_STATE_CHANGED = 1; + protected static final int EVENT_NETWORK_STATE_CHANGED = 2; + protected static final int EVENT_GET_SIGNAL_STRENGTH = 3; + protected static final int EVENT_POLL_STATE_REGISTRATION = 4; + protected static final int EVENT_POLL_STATE_GPRS = 5; + protected static final int EVENT_POLL_STATE_OPERATOR = 6; + protected static final int EVENT_POLL_SIGNAL_STRENGTH = 10; + protected static final int EVENT_NITZ_TIME = 11; + protected static final int EVENT_SIGNAL_STRENGTH_UPDATE = 12; + protected static final int EVENT_RADIO_AVAILABLE = 13; + protected static final int EVENT_POLL_STATE_NETWORK_SELECTION_MODE = 14; + protected static final int EVENT_GET_LOC_DONE = 15; + protected static final int EVENT_SIM_RECORDS_LOADED = 16; + protected static final int EVENT_SIM_READY = 17; + protected static final int EVENT_LOCATION_UPDATES_ENABLED = 18; + protected static final int EVENT_GET_PREFERRED_NETWORK_TYPE = 19; + protected static final int EVENT_SET_PREFERRED_NETWORK_TYPE = 20; + protected static final int EVENT_RESET_PREFERRED_NETWORK_TYPE = 21; + protected static final int EVENT_CHECK_REPORT_GPRS = 22; + protected static final int EVENT_RESTRICTED_STATE_CHANGED = 23; + + /** CDMA events */ + protected static final int EVENT_POLL_STATE_REGISTRATION_CDMA = 24; + protected static final int EVENT_POLL_STATE_OPERATOR_CDMA = 25; + protected static final int EVENT_RUIM_READY = 26; + protected static final int EVENT_RUIM_RECORDS_LOADED = 27; + protected static final int EVENT_POLL_SIGNAL_STRENGTH_CDMA = 28; + protected static final int EVENT_GET_SIGNAL_STRENGTH_CDMA = 29; + protected static final int EVENT_NETWORK_STATE_CHANGED_CDMA = 30; + protected static final int EVENT_GET_LOC_DONE_CDMA = 31; + protected static final int EVENT_SIGNAL_STRENGTH_UPDATE_CDMA = 32; + protected static final int EVENT_NV_LOADED = 33; + protected static final int EVENT_POLL_STATE_CDMA_SUBSCRIPTION = 34; + protected static final int EVENT_NV_READY = 35; + protected static final int EVENT_ERI_FILE_LOADED = 36; + protected static final int EVENT_OTA_PROVISION_STATUS_CHANGE = 37; + protected static final int EVENT_SET_RADIO_POWER_OFF = 38; + protected static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 39; + protected static final int EVENT_CDMA_PRL_VERSION_CHANGED = 40; + protected static final int EVENT_RADIO_ON = 41; + + + protected static final String TIMEZONE_PROPERTY = "persist.sys.timezone"; + + /** + * List of ISO codes for countries that can have an offset of + * GMT+0 when not in daylight savings time. This ignores some + * small places such as the Canary Islands (Spain) and + * Danmarkshavn (Denmark). The list must be sorted by code. + */ + protected static final String[] GMT_COUNTRY_CODES = { + "bf", // Burkina Faso + "ci", // Cote d'Ivoire + "eh", // Western Sahara + "fo", // Faroe Islands, Denmark + "gb", // United Kingdom of Great Britain and Northern Ireland + "gh", // Ghana + "gm", // Gambia + "gn", // Guinea + "gw", // Guinea Bissau + "ie", // Ireland + "lr", // Liberia + "is", // Iceland + "ma", // Morocco + "ml", // Mali + "mr", // Mauritania + "pt", // Portugal + "sl", // Sierra Leone + "sn", // Senegal + "st", // Sao Tome and Principe + "tg", // Togo + }; + + /** Reason for registration denial. */ + protected static final String REGISTRATION_DENIED_GEN = "General"; + protected static final String REGISTRATION_DENIED_AUTH = "Authentication Failure"; + + public ServiceStateTracker() { + } + + public boolean getDesiredPowerState() { + return mDesiredPowerState; + } + + /** + * Registration point for combined roaming on + * combined roaming is true when roaming is true and ONS differs SPN + * + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForRoamingOn(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mRoamingOnRegistrants.add(r); + + if (ss.getRoaming()) { + r.notifyRegistrant(); + } + } + + public void unregisterForRoamingOn(Handler h) { + mRoamingOnRegistrants.remove(h); + } + + /** + * Registration point for combined roaming off + * combined roaming is true when roaming is true and ONS differs SPN + * + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForRoamingOff(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mRoamingOffRegistrants.add(r); + + if (!ss.getRoaming()) { + r.notifyRegistrant(); + } + } + + public void unregisterForRoamingOff(Handler h) { + mRoamingOffRegistrants.remove(h); + } + + /** + * Re-register network by toggling preferred network type. + * This is a work-around to deregister and register network since there is + * no ril api to set COPS=2 (deregister) only. + * + * @param onComplete is dispatched when this is complete. it will be + * an AsyncResult, and onComplete.obj.exception will be non-null + * on failure. + */ + public void reRegisterNetwork(Message onComplete) { + cm.getPreferredNetworkType( + obtainMessage(EVENT_GET_PREFERRED_NETWORK_TYPE, onComplete)); + } + + public void + setRadioPower(boolean power) { + mDesiredPowerState = power; + + setPowerStateToDesired(); + } + + /** + * These two flags manage the behavior of the cell lock -- the + * lock should be held if either flag is true. The intention is + * to allow temporary acquisition of the lock to get a single + * update. Such a lock grab and release can thus be made to not + * interfere with more permanent lock holds -- in other words, the + * lock will only be released if both flags are false, and so + * releases by temporary users will only affect the lock state if + * there is no continuous user. + */ + private boolean mWantContinuousLocationUpdates; + private boolean mWantSingleLocationUpdate; + + public void enableSingleLocationUpdate() { + if (mWantSingleLocationUpdate || mWantContinuousLocationUpdates) return; + mWantSingleLocationUpdate = true; + cm.setLocationUpdates(true, obtainMessage(EVENT_LOCATION_UPDATES_ENABLED)); + } + + public void enableLocationUpdates() { + if (mWantSingleLocationUpdate || mWantContinuousLocationUpdates) return; + mWantContinuousLocationUpdates = true; + cm.setLocationUpdates(true, obtainMessage(EVENT_LOCATION_UPDATES_ENABLED)); + } + + protected void disableSingleLocationUpdate() { + mWantSingleLocationUpdate = false; + if (!mWantSingleLocationUpdate && !mWantContinuousLocationUpdates) { + cm.setLocationUpdates(false, null); + } + } + + public void disableLocationUpdates() { + mWantContinuousLocationUpdates = false; + if (!mWantSingleLocationUpdate && !mWantContinuousLocationUpdates) { + cm.setLocationUpdates(false, null); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_SET_RADIO_POWER_OFF: + synchronized(this) { + if (mPendingRadioPowerOffAfterDataOff && + (msg.arg1 == mPendingRadioPowerOffAfterDataOffTag)) { + if (DBG) log("EVENT_SET_RADIO_OFF, turn radio off now."); + hangupAndPowerOff(); + mPendingRadioPowerOffAfterDataOffTag += 1; + mPendingRadioPowerOffAfterDataOff = false; + } else { + log("EVENT_SET_RADIO_OFF is stale arg1=" + msg.arg1 + + "!= tag=" + mPendingRadioPowerOffAfterDataOffTag); + } + } + break; + + default: + log("Unhandled message with number: " + msg.what); + break; + } + } + + protected abstract Phone getPhone(); + protected abstract void handlePollStateResult(int what, AsyncResult ar); + protected abstract void updateSpnDisplay(); + protected abstract void setPowerStateToDesired(); + protected abstract void log(String s); + protected abstract void loge(String s); + + public abstract int getCurrentDataConnectionState(); + public abstract boolean isConcurrentVoiceAndDataAllowed(); + + /** + * Registration point for transition into DataConnection attached. + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForDataConnectionAttached(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mAttachedRegistrants.add(r); + + if (getCurrentDataConnectionState() == ServiceState.STATE_IN_SERVICE) { + r.notifyRegistrant(); + } + } + public void unregisterForDataConnectionAttached(Handler h) { + mAttachedRegistrants.remove(h); + } + + /** + * Registration point for transition into DataConnection detached. + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForDataConnectionDetached(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mDetachedRegistrants.add(r); + + if (getCurrentDataConnectionState() != ServiceState.STATE_IN_SERVICE) { + r.notifyRegistrant(); + } + } + public void unregisterForDataConnectionDetached(Handler h) { + mDetachedRegistrants.remove(h); + } + + /** + * Registration point for transition into network attached. + * @param h handler to notify + * @param what what code of message when delivered + * @param obj in Message.obj + */ + public void registerForNetworkAttached(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + + mNetworkAttachedRegistrants.add(r); + if (ss.getState() == ServiceState.STATE_IN_SERVICE) { + r.notifyRegistrant(); + } + } + public void unregisterForNetworkAttached(Handler h) { + mNetworkAttachedRegistrants.remove(h); + } + + /** + * Registration point for transition into packet service restricted zone. + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForPsRestrictedEnabled(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mPsRestrictEnabledRegistrants.add(r); + + if (mRestrictedState.isPsRestricted()) { + r.notifyRegistrant(); + } + } + + public void unregisterForPsRestrictedEnabled(Handler h) { + mPsRestrictEnabledRegistrants.remove(h); + } + + /** + * Registration point for transition out of packet service restricted zone. + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForPsRestrictedDisabled(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + mPsRestrictDisabledRegistrants.add(r); + + if (mRestrictedState.isPsRestricted()) { + r.notifyRegistrant(); + } + } + + public void unregisterForPsRestrictedDisabled(Handler h) { + mPsRestrictDisabledRegistrants.remove(h); + } + + /** + * Clean up existing voice and data connection then turn off radio power. + * + * Hang up the existing voice calls to decrease call drop rate. + */ + public void powerOffRadioSafely(DataConnectionTracker dcTracker) { + synchronized (this) { + if (!mPendingRadioPowerOffAfterDataOff) { + // To minimize race conditions we call cleanUpAllConnections on + // both if else paths instead of before this isDisconnected test. + if (dcTracker.isDisconnected()) { + // To minimize race conditions we do this after isDisconnected + dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF); + if (DBG) log("Data disconnected, turn off radio right away."); + hangupAndPowerOff(); + } else { + dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF); + Message msg = Message.obtain(this); + msg.what = EVENT_SET_RADIO_POWER_OFF; + msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag; + if (sendMessageDelayed(msg, 30000)) { + if (DBG) log("Wait upto 30s for data to disconnect, then turn off radio."); + mPendingRadioPowerOffAfterDataOff = true; + } else { + log("Cannot send delayed Msg, turn off radio right away."); + hangupAndPowerOff(); + } + } + } + } + } + + /** + * process the pending request to turn radio off after data is disconnected + * + * return true if there is pending request to process; false otherwise. + */ + public boolean processPendingRadioPowerOffAfterDataOff() { + synchronized(this) { + if (mPendingRadioPowerOffAfterDataOff) { + if (DBG) log("Process pending request to turn radio off."); + mPendingRadioPowerOffAfterDataOffTag += 1; + hangupAndPowerOff(); + mPendingRadioPowerOffAfterDataOff = false; + return true; + } + return false; + } + } + + /** + * Hang up all voice call and turn off radio. Implemented by derived class. + */ + protected abstract void hangupAndPowerOff(); + + /** Cancel a pending (if any) pollState() operation */ + protected void cancelPollState() { + // This will effectively cancel the rest of the poll requests. + pollingContext = new int[1]; + } + + /** + * Return true if time zone needs fixing. + * + * @param phoneBase + * @param operatorNumeric + * @param prevOperatorNumeric + * @param needToFixTimeZone + * @return true if time zone needs to be fixed + */ + protected boolean shouldFixTimeZoneNow(PhoneBase phoneBase, String operatorNumeric, + String prevOperatorNumeric, boolean needToFixTimeZone) { + // Return false if the mcc isn't valid as we don't know where we are. + // Return true if we have an IccCard and the mcc changed or we + // need to fix it because when the NITZ time came in we didn't + // know the country code. + + // If mcc is invalid then we'll return false + int mcc; + try { + mcc = Integer.parseInt(operatorNumeric.substring(0, 3)); + } catch (Exception e) { + if (DBG) { + log("shouldFixTimeZoneNow: no mcc, operatorNumeric=" + operatorNumeric + + " retVal=false"); + } + return false; + } + + // If prevMcc is invalid will make it different from mcc + // so we'll return true if the card exists. + int prevMcc; + try { + prevMcc = Integer.parseInt(prevOperatorNumeric.substring(0, 3)); + } catch (Exception e) { + prevMcc = mcc + 1; + } + + // Determine if the Icc card exists + IccCard iccCard = phoneBase.getIccCard(); + boolean iccCardExist = (iccCard != null) && iccCard.getState().iccCardExist(); + + // Determine retVal + boolean retVal = ((iccCardExist && (mcc != prevMcc)) || needToFixTimeZone); + if (DBG) { + long ctm = System.currentTimeMillis(); + log("shouldFixTimeZoneNow: retVal=" + retVal + + " iccCard=" + iccCard + + " iccCard.state=" + (iccCard == null ? "null" : iccCard.getState().toString()) + + " iccCardExist=" + iccCardExist + + " operatorNumeric=" + operatorNumeric + " mcc=" + mcc + + " prevOperatorNumeric=" + prevOperatorNumeric + " prevMcc=" + prevMcc + + " needToFixTimeZone=" + needToFixTimeZone + + " ltod=" + TimeUtils.logTimeOfDay(ctm)); + } + return retVal; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("ServiceStateTracker:"); + pw.println(" ss=" + ss); + pw.println(" newSS=" + newSS); + pw.println(" mSignalStrength=" + mSignalStrength); + pw.println(" mRestrictedState=" + mRestrictedState); + pw.println(" pollingContext=" + pollingContext); + pw.println(" mDesiredPowerState=" + mDesiredPowerState); + pw.println(" mRilRadioTechnology=" + mRilRadioTechnology); + pw.println(" mNewRilRadioTechnology=" + mNewRilRadioTechnology); + pw.println(" dontPollSignalStrength=" + dontPollSignalStrength); + pw.println(" mPendingRadioPowerOffAfterDataOff=" + mPendingRadioPowerOffAfterDataOff); + pw.println(" mPendingRadioPowerOffAfterDataOffTag=" + mPendingRadioPowerOffAfterDataOffTag); + } +} diff --git a/src/java/com/android/internal/telephony/SmsAddress.java b/src/java/com/android/internal/telephony/SmsAddress.java new file mode 100644 index 0000000..b3892cb --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsAddress.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +public abstract class SmsAddress { + // From TS 23.040 9.1.2.5 and TS 24.008 table 10.5.118 + // and C.S0005-D table 2.7.1.3.2.4-2 + public static final int TON_UNKNOWN = 0; + public static final int TON_INTERNATIONAL = 1; + public static final int TON_NATIONAL = 2; + public static final int TON_NETWORK = 3; + public static final int TON_SUBSCRIBER = 4; + public static final int TON_ALPHANUMERIC = 5; + public static final int TON_ABBREVIATED = 6; + + public int ton; + public String address; + public byte[] origBytes; + + /** + * Returns the address of the SMS message in String form or null if unavailable + */ + public String getAddressString() { + return address; + } + + /** + * Returns true if this is an alphanumeric address + */ + public boolean isAlphanumeric() { + return ton == TON_ALPHANUMERIC; + } + + /** + * Returns true if this is a network address + */ + public boolean isNetworkSpecific() { + return ton == TON_NETWORK; + } + + public boolean couldBeEmailGateway() { + // Some carriers seems to send email gateway messages in this form: + // from: an UNKNOWN TON, 3 or 4 digits long, beginning with a 5 + // PID: 0x00, Data coding scheme 0x03 + // So we just attempt to treat any message from an address length <= 4 + // as an email gateway + + return address.length() <= 4; + } + +} diff --git a/src/java/com/android/internal/telephony/SmsHeader.java b/src/java/com/android/internal/telephony/SmsHeader.java new file mode 100644 index 0000000..30d57b8 --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsHeader.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.SmsConstants; +import com.android.internal.util.HexDump; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import java.util.ArrayList; + +/** + * SMS user data header, as specified in TS 23.040 9.2.3.24. + */ +public class SmsHeader { + + // TODO(cleanup): this data structure is generally referred to as + // the 'user data header' or UDH, and so the class name should + // change to reflect this... + + /** SMS user data header information element identifiers. + * (see TS 23.040 9.2.3.24) + */ + public static final int ELT_ID_CONCATENATED_8_BIT_REFERENCE = 0x00; + public static final int ELT_ID_SPECIAL_SMS_MESSAGE_INDICATION = 0x01; + public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT = 0x04; + public static final int ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT = 0x05; + public static final int ELT_ID_SMSC_CONTROL_PARAMS = 0x06; + public static final int ELT_ID_UDH_SOURCE_INDICATION = 0x07; + public static final int ELT_ID_CONCATENATED_16_BIT_REFERENCE = 0x08; + public static final int ELT_ID_WIRELESS_CTRL_MSG_PROTOCOL = 0x09; + public static final int ELT_ID_TEXT_FORMATTING = 0x0A; + public static final int ELT_ID_PREDEFINED_SOUND = 0x0B; + public static final int ELT_ID_USER_DEFINED_SOUND = 0x0C; + public static final int ELT_ID_PREDEFINED_ANIMATION = 0x0D; + public static final int ELT_ID_LARGE_ANIMATION = 0x0E; + public static final int ELT_ID_SMALL_ANIMATION = 0x0F; + public static final int ELT_ID_LARGE_PICTURE = 0x10; + public static final int ELT_ID_SMALL_PICTURE = 0x11; + public static final int ELT_ID_VARIABLE_PICTURE = 0x12; + public static final int ELT_ID_USER_PROMPT_INDICATOR = 0x13; + public static final int ELT_ID_EXTENDED_OBJECT = 0x14; + public static final int ELT_ID_REUSED_EXTENDED_OBJECT = 0x15; + public static final int ELT_ID_COMPRESSION_CONTROL = 0x16; + public static final int ELT_ID_OBJECT_DISTR_INDICATOR = 0x17; + public static final int ELT_ID_STANDARD_WVG_OBJECT = 0x18; + public static final int ELT_ID_CHARACTER_SIZE_WVG_OBJECT = 0x19; + public static final int ELT_ID_EXTENDED_OBJECT_DATA_REQUEST_CMD = 0x1A; + public static final int ELT_ID_RFC_822_EMAIL_HEADER = 0x20; + public static final int ELT_ID_HYPERLINK_FORMAT_ELEMENT = 0x21; + public static final int ELT_ID_REPLY_ADDRESS_ELEMENT = 0x22; + public static final int ELT_ID_ENHANCED_VOICE_MAIL_INFORMATION = 0x23; + public static final int ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT = 0x24; + public static final int ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT = 0x25; + + public static final int PORT_WAP_PUSH = 2948; + public static final int PORT_WAP_WSP = 9200; + + public static class PortAddrs { + public int destPort; + public int origPort; + public boolean areEightBits; + } + + public static class ConcatRef { + public int refNumber; + public int seqNumber; + public int msgCount; + public boolean isEightBits; + } + + /** + * A header element that is not explicitly parsed, meaning not + * PortAddrs or ConcatRef. + */ + public static class MiscElt { + public int id; + public byte[] data; + } + + public PortAddrs portAddrs; + public ConcatRef concatRef; + public ArrayList<MiscElt> miscEltList = new ArrayList<MiscElt>(); + + /** 7 bit national language locking shift table, or 0 for GSM default 7 bit alphabet. */ + public int languageTable; + + /** 7 bit national language single shift table, or 0 for GSM default 7 bit extension table. */ + public int languageShiftTable; + + public SmsHeader() {} + + /** + * Create structured SmsHeader object from serialized byte array representation. + * (see TS 23.040 9.2.3.24) + * @param data is user data header bytes + * @return SmsHeader object + */ + public static SmsHeader fromByteArray(byte[] data) { + ByteArrayInputStream inStream = new ByteArrayInputStream(data); + SmsHeader smsHeader = new SmsHeader(); + while (inStream.available() > 0) { + /** + * NOTE: as defined in the spec, ConcatRef and PortAddr + * fields should not reoccur, but if they do the last + * occurrence is to be used. Also, for ConcatRef + * elements, if the count is zero, sequence is zero, or + * sequence is larger than count, the entire element is to + * be ignored. + */ + int id = inStream.read(); + int length = inStream.read(); + ConcatRef concatRef; + PortAddrs portAddrs; + switch (id) { + case ELT_ID_CONCATENATED_8_BIT_REFERENCE: + concatRef = new ConcatRef(); + concatRef.refNumber = inStream.read(); + concatRef.msgCount = inStream.read(); + concatRef.seqNumber = inStream.read(); + concatRef.isEightBits = true; + if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 && + concatRef.seqNumber <= concatRef.msgCount) { + smsHeader.concatRef = concatRef; + } + break; + case ELT_ID_CONCATENATED_16_BIT_REFERENCE: + concatRef = new ConcatRef(); + concatRef.refNumber = (inStream.read() << 8) | inStream.read(); + concatRef.msgCount = inStream.read(); + concatRef.seqNumber = inStream.read(); + concatRef.isEightBits = false; + if (concatRef.msgCount != 0 && concatRef.seqNumber != 0 && + concatRef.seqNumber <= concatRef.msgCount) { + smsHeader.concatRef = concatRef; + } + break; + case ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT: + portAddrs = new PortAddrs(); + portAddrs.destPort = inStream.read(); + portAddrs.origPort = inStream.read(); + portAddrs.areEightBits = true; + smsHeader.portAddrs = portAddrs; + break; + case ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT: + portAddrs = new PortAddrs(); + portAddrs.destPort = (inStream.read() << 8) | inStream.read(); + portAddrs.origPort = (inStream.read() << 8) | inStream.read(); + portAddrs.areEightBits = false; + smsHeader.portAddrs = portAddrs; + break; + case ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT: + smsHeader.languageShiftTable = inStream.read(); + break; + case ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT: + smsHeader.languageTable = inStream.read(); + break; + default: + MiscElt miscElt = new MiscElt(); + miscElt.id = id; + miscElt.data = new byte[length]; + inStream.read(miscElt.data, 0, length); + smsHeader.miscEltList.add(miscElt); + } + } + return smsHeader; + } + + /** + * Create serialized byte array representation from structured SmsHeader object. + * (see TS 23.040 9.2.3.24) + * @return Byte array representing the SmsHeader + */ + public static byte[] toByteArray(SmsHeader smsHeader) { + if ((smsHeader.portAddrs == null) && + (smsHeader.concatRef == null) && + (smsHeader.miscEltList.isEmpty()) && + (smsHeader.languageShiftTable == 0) && + (smsHeader.languageTable == 0)) { + return null; + } + + ByteArrayOutputStream outStream = + new ByteArrayOutputStream(SmsConstants.MAX_USER_DATA_BYTES); + ConcatRef concatRef = smsHeader.concatRef; + if (concatRef != null) { + if (concatRef.isEightBits) { + outStream.write(ELT_ID_CONCATENATED_8_BIT_REFERENCE); + outStream.write(3); + outStream.write(concatRef.refNumber); + } else { + outStream.write(ELT_ID_CONCATENATED_16_BIT_REFERENCE); + outStream.write(4); + outStream.write(concatRef.refNumber >>> 8); + outStream.write(concatRef.refNumber & 0x00FF); + } + outStream.write(concatRef.msgCount); + outStream.write(concatRef.seqNumber); + } + PortAddrs portAddrs = smsHeader.portAddrs; + if (portAddrs != null) { + if (portAddrs.areEightBits) { + outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_8_BIT); + outStream.write(2); + outStream.write(portAddrs.destPort); + outStream.write(portAddrs.origPort); + } else { + outStream.write(ELT_ID_APPLICATION_PORT_ADDRESSING_16_BIT); + outStream.write(4); + outStream.write(portAddrs.destPort >>> 8); + outStream.write(portAddrs.destPort & 0x00FF); + outStream.write(portAddrs.origPort >>> 8); + outStream.write(portAddrs.origPort & 0x00FF); + } + } + if (smsHeader.languageShiftTable != 0) { + outStream.write(ELT_ID_NATIONAL_LANGUAGE_SINGLE_SHIFT); + outStream.write(1); + outStream.write(smsHeader.languageShiftTable); + } + if (smsHeader.languageTable != 0) { + outStream.write(ELT_ID_NATIONAL_LANGUAGE_LOCKING_SHIFT); + outStream.write(1); + outStream.write(smsHeader.languageTable); + } + for (MiscElt miscElt : smsHeader.miscEltList) { + outStream.write(miscElt.id); + outStream.write(miscElt.data.length); + outStream.write(miscElt.data, 0, miscElt.data.length); + } + return outStream.toByteArray(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("UserDataHeader "); + builder.append("{ ConcatRef "); + if (concatRef == null) { + builder.append("unset"); + } else { + builder.append("{ refNumber=" + concatRef.refNumber); + builder.append(", msgCount=" + concatRef.msgCount); + builder.append(", seqNumber=" + concatRef.seqNumber); + builder.append(", isEightBits=" + concatRef.isEightBits); + builder.append(" }"); + } + builder.append(", PortAddrs "); + if (portAddrs == null) { + builder.append("unset"); + } else { + builder.append("{ destPort=" + portAddrs.destPort); + builder.append(", origPort=" + portAddrs.origPort); + builder.append(", areEightBits=" + portAddrs.areEightBits); + builder.append(" }"); + } + if (languageShiftTable != 0) { + builder.append(", languageShiftTable=" + languageShiftTable); + } + if (languageTable != 0) { + builder.append(", languageTable=" + languageTable); + } + for (MiscElt miscElt : miscEltList) { + builder.append(", MiscElt "); + builder.append("{ id=" + miscElt.id); + builder.append(", length=" + miscElt.data.length); + builder.append(", data=" + HexDump.toHexString(miscElt.data)); + builder.append(" }"); + } + builder.append(" }"); + return builder.toString(); + } + +} diff --git a/src/java/com/android/internal/telephony/SmsMessageBase.java b/src/java/com/android/internal/telephony/SmsMessageBase.java new file mode 100644 index 0000000..22d8cd8 --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsMessageBase.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import java.util.Arrays; + +import android.provider.Telephony; + +/** + * Base class declaring the specific methods and members for SmsMessage. + * {@hide} + */ +public abstract class SmsMessageBase { + private static final String LOG_TAG = "SMS"; + + /** {@hide} The address of the SMSC. May be null */ + protected String scAddress; + + /** {@hide} The address of the sender */ + protected SmsAddress originatingAddress; + + /** {@hide} The message body as a string. May be null if the message isn't text */ + protected String messageBody; + + /** {@hide} */ + protected String pseudoSubject; + + /** {@hide} Non-null if this is an email gateway message */ + protected String emailFrom; + + /** {@hide} Non-null if this is an email gateway message */ + protected String emailBody; + + /** {@hide} */ + protected boolean isEmail; + + /** {@hide} */ + protected long scTimeMillis; + + /** {@hide} The raw PDU of the message */ + protected byte[] mPdu; + + /** {@hide} The raw bytes for the user data section of the message */ + protected byte[] userData; + + /** {@hide} */ + protected SmsHeader userDataHeader; + + // "Message Waiting Indication Group" + // 23.038 Section 4 + /** {@hide} */ + protected boolean isMwi; + + /** {@hide} */ + protected boolean mwiSense; + + /** {@hide} */ + protected boolean mwiDontStore; + + /** + * Indicates status for messages stored on the ICC. + */ + protected int statusOnIcc = -1; + + /** + * Record index of message in the EF. + */ + protected int indexOnIcc = -1; + + /** TP-Message-Reference - Message Reference of sent message. @hide */ + public int messageRef; + + // TODO(): This class is duplicated in SmsMessage.java. Refactor accordingly. + public static abstract class SubmitPduBase { + public byte[] encodedScAddress; // Null if not applicable. + public byte[] encodedMessage; + + public String toString() { + return "SubmitPdu: encodedScAddress = " + + Arrays.toString(encodedScAddress) + + ", encodedMessage = " + + Arrays.toString(encodedMessage); + } + } + + /** + * Returns the address of the SMS service center that relayed this message + * or null if there is none. + */ + public String getServiceCenterAddress() { + return scAddress; + } + + /** + * Returns the originating address (sender) of this SMS message in String + * form or null if unavailable + */ + public String getOriginatingAddress() { + if (originatingAddress == null) { + return null; + } + + return originatingAddress.getAddressString(); + } + + /** + * Returns the originating address, or email from address if this message + * was from an email gateway. Returns null if originating address + * unavailable. + */ + public String getDisplayOriginatingAddress() { + if (isEmail) { + return emailFrom; + } else { + return getOriginatingAddress(); + } + } + + /** + * Returns the message body as a String, if it exists and is text based. + * @return message body is there is one, otherwise null + */ + public String getMessageBody() { + return messageBody; + } + + /** + * Returns the class of this message. + */ + public abstract SmsConstants.MessageClass getMessageClass(); + + /** + * Returns the message body, or email message body if this message was from + * an email gateway. Returns null if message body unavailable. + */ + public String getDisplayMessageBody() { + if (isEmail) { + return emailBody; + } else { + return getMessageBody(); + } + } + + /** + * Unofficial convention of a subject line enclosed in parens empty string + * if not present + */ + public String getPseudoSubject() { + return pseudoSubject == null ? "" : pseudoSubject; + } + + /** + * Returns the service centre timestamp in currentTimeMillis() format + */ + public long getTimestampMillis() { + return scTimeMillis; + } + + /** + * Returns true if message is an email. + * + * @return true if this message came through an email gateway and email + * sender / subject / parsed body are available + */ + public boolean isEmail() { + return isEmail; + } + + /** + * @return if isEmail() is true, body of the email sent through the gateway. + * null otherwise + */ + public String getEmailBody() { + return emailBody; + } + + /** + * @return if isEmail() is true, email from address of email sent through + * the gateway. null otherwise + */ + public String getEmailFrom() { + return emailFrom; + } + + /** + * Get protocol identifier. + */ + public abstract int getProtocolIdentifier(); + + /** + * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" + * SMS + */ + public abstract boolean isReplace(); + + /** + * Returns true for CPHS MWI toggle message. + * + * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section + * B.4.2 + */ + public abstract boolean isCphsMwiMessage(); + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) clear message + */ + public abstract boolean isMWIClearMessage(); + + /** + * returns true if this message is a CPHS voicemail / message waiting + * indicator (MWI) set message + */ + public abstract boolean isMWISetMessage(); + + /** + * returns true if this message is a "Message Waiting Indication Group: + * Discard Message" notification and should not be stored. + */ + public abstract boolean isMwiDontStore(); + + /** + * returns the user data section minus the user data header if one was + * present. + */ + public byte[] getUserData() { + return userData; + } + + /** + * Returns an object representing the user data header + * + * {@hide} + */ + public SmsHeader getUserDataHeader() { + return userDataHeader; + } + + /** + * TODO(cleanup): The term PDU is used in a seemingly non-unique + * manner -- for example, what is the difference between this byte + * array and the contents of SubmitPdu objects. Maybe a more + * illustrative term would be appropriate. + */ + + /** + * Returns the raw PDU for the message. + */ + public byte[] getPdu() { + return mPdu; + } + + /** + * For an SMS-STATUS-REPORT message, this returns the status field from + * the status report. This field indicates the status of a previously + * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a + * description of values. + * + * @return 0 indicates the previously sent message was received. + * See TS 23.040, 9.9.2.3.15 for a description of other possible + * values. + */ + public abstract int getStatus(); + + /** + * Return true iff the message is a SMS-STATUS-REPORT message. + */ + public abstract boolean isStatusReportMessage(); + + /** + * Returns true iff the <code>TP-Reply-Path</code> bit is set in + * this message. + */ + public abstract boolean isReplyPathPresent(); + + /** + * Returns the status of the message on the ICC (read, unread, sent, unsent). + * + * @return the status of the message on the ICC. These are: + * SmsManager.STATUS_ON_ICC_FREE + * SmsManager.STATUS_ON_ICC_READ + * SmsManager.STATUS_ON_ICC_UNREAD + * SmsManager.STATUS_ON_ICC_SEND + * SmsManager.STATUS_ON_ICC_UNSENT + */ + public int getStatusOnIcc() { + return statusOnIcc; + } + + /** + * Returns the record index of the message on the ICC (1-based index). + * @return the record index of the message on the ICC, or -1 if this + * SmsMessage was not created from a ICC SMS EF record. + */ + public int getIndexOnIcc() { + return indexOnIcc; + } + + protected void parseMessageBody() { + // originatingAddress could be null if this message is from a status + // report. + if (originatingAddress != null && originatingAddress.couldBeEmailGateway()) { + extractEmailAddressFromMessageBody(); + } + } + + /** + * Try to parse this message as an email gateway message + * There are two ways specified in TS 23.040 Section 3.8 : + * - SMS message "may have its TP-PID set for Internet electronic mail - MT + * SMS format: [<from-address><space>]<message> - "Depending on the + * nature of the gateway, the destination/origination address is either + * derived from the content of the SMS TP-OA or TP-DA field, or the + * TP-OA/TP-DA field contains a generic gateway address and the to/from + * address is added at the beginning as shown above." (which is supported here) + * - Multiple addresses separated by commas, no spaces, Subject field delimited + * by '()' or '##' and '#' Section 9.2.3.24.11 (which are NOT supported here) + */ + protected void extractEmailAddressFromMessageBody() { + + /* Some carriers may use " /" delimiter as below + * + * 1. [x@y][ ]/[subject][ ]/[body] + * -or- + * 2. [x@y][ ]/[body] + */ + String[] parts = messageBody.split("( /)|( )", 2); + if (parts.length < 2) return; + emailFrom = parts[0]; + emailBody = parts[1]; + isEmail = Telephony.Mms.isEmailAddress(emailFrom); + } + +} diff --git a/src/java/com/android/internal/telephony/SmsRawData.java b/src/java/com/android/internal/telephony/SmsRawData.java new file mode 100644 index 0000000..891d942 --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsRawData.java @@ -0,0 +1,62 @@ +/* +** Copyright 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 com.android.internal.telephony; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A parcelable holder class of byte[] for ISms aidl implementation + */ +public class SmsRawData implements Parcelable { + byte[] data; + + //Static Methods + public static final Parcelable.Creator<SmsRawData> CREATOR + = new Parcelable.Creator<SmsRawData> (){ + public SmsRawData createFromParcel(Parcel source) { + int size; + size = source.readInt(); + byte[] data = new byte[size]; + source.readByteArray(data); + return new SmsRawData(data); + } + + public SmsRawData[] newArray(int size) { + return new SmsRawData[size]; + } + }; + + // Constructor + public SmsRawData(byte[] data) { + this.data = data; + } + + public byte[] getBytes() { + return data; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(data.length); + dest.writeByteArray(data); + } +} diff --git a/src/java/com/android/internal/telephony/SmsResponse.java b/src/java/com/android/internal/telephony/SmsResponse.java new file mode 100644 index 0000000..a7c2840 --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsResponse.java @@ -0,0 +1,48 @@ +/* + * 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 com.android.internal.telephony; + +/** + * Object returned by the RIL upon successful completion of sendSMS. + * Contains message reference and ackPdu. + * + */ +public class SmsResponse { + /** Message reference of the just-sent SMS. */ + int messageRef; + /** ackPdu for the just-sent SMS. */ + String ackPdu; + /** + * errorCode: See 3GPP 27.005, 3.2.5 for GSM/UMTS, + * 3GPP2 N.S0005 (IS-41C) Table 171 for CDMA, -1 if unknown or not applicable. + */ + int errorCode; + + public SmsResponse(int messageRef, String ackPdu, int errorCode) { + this.messageRef = messageRef; + this.ackPdu = ackPdu; + this.errorCode = errorCode; + } + + public String toString() { + String ret = "{ messageRef = " + messageRef + + ", errorCode = " + errorCode + + ", ackPdu = " + ackPdu + + "}"; + return ret; + } +} diff --git a/src/java/com/android/internal/telephony/SmsStorageMonitor.java b/src/java/com/android/internal/telephony/SmsStorageMonitor.java new file mode 100644 index 0000000..0c06ffc --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsStorageMonitor.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.provider.Telephony.Sms.Intents; +import android.util.Log; + +/** + * Monitors the device and ICC storage, and sends the appropriate events. + * + * This code was formerly part of {@link SMSDispatcher}, and has been moved + * into a separate class to support instantiation of multiple SMSDispatchers on + * dual-mode devices that require support for both 3GPP and 3GPP2 format messages. + */ +public final class SmsStorageMonitor extends Handler { + private static final String TAG = "SmsStorageMonitor"; + + /** SIM/RUIM storage is full */ + private static final int EVENT_ICC_FULL = 1; + + /** Memory status reporting is acknowledged by RIL */ + private static final int EVENT_REPORT_MEMORY_STATUS_DONE = 2; + + /** Radio is ON */ + private static final int EVENT_RADIO_ON = 3; + + /** Context from phone object passed to constructor. */ + private final Context mContext; + + /** Wake lock to ensure device stays awake while dispatching the SMS intent. */ + private PowerManager.WakeLock mWakeLock; + + private boolean mReportMemoryStatusPending; + + final CommandsInterface mCm; // accessed from inner class + boolean mStorageAvailable = true; // accessed from inner class + + /** + * Hold the wake lock for 5 seconds, which should be enough time for + * any receiver(s) to grab its own wake lock. + */ + private static final int WAKE_LOCK_TIMEOUT = 5000; + + /** + * Creates an SmsStorageMonitor and registers for events. + * @param phone the Phone to use + */ + public SmsStorageMonitor(PhoneBase phone) { + mContext = phone.getContext(); + mCm = phone.mCM; + + createWakelock(); + + mCm.setOnIccSmsFull(this, EVENT_ICC_FULL, null); + mCm.registerForOn(this, EVENT_RADIO_ON, null); + + // Register for device storage intents. Use these to notify the RIL + // that storage for SMS is or is not available. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_FULL); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_NOT_FULL); + mContext.registerReceiver(mResultReceiver, filter); + } + + public void dispose() { + mCm.unSetOnIccSmsFull(this); + mCm.unregisterForOn(this); + mContext.unregisterReceiver(mResultReceiver); + } + + /** + * Handles events coming from the phone stack. Overridden from handler. + * @param msg the message to handle + */ + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_ICC_FULL: + handleIccFull(); + break; + + case EVENT_REPORT_MEMORY_STATUS_DONE: + ar = (AsyncResult) msg.obj; + if (ar.exception != null) { + mReportMemoryStatusPending = true; + Log.v(TAG, "Memory status report to modem pending : mStorageAvailable = " + + mStorageAvailable); + } else { + mReportMemoryStatusPending = false; + } + break; + + case EVENT_RADIO_ON: + if (mReportMemoryStatusPending) { + Log.v(TAG, "Sending pending memory status report : mStorageAvailable = " + + mStorageAvailable); + mCm.reportSmsMemoryStatus(mStorageAvailable, + obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); + } + break; + } + } + + private void createWakelock() { + PowerManager pm = (PowerManager)mContext.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SmsStorageMonitor"); + mWakeLock.setReferenceCounted(true); + } + + /** + * Called when SIM_FULL message is received from the RIL. Notifies interested + * parties that SIM storage for SMS messages is full. + */ + private void handleIccFull() { + // broadcast SIM_FULL intent + Intent intent = new Intent(Intents.SIM_FULL_ACTION); + mWakeLock.acquire(WAKE_LOCK_TIMEOUT); + mContext.sendBroadcast(intent, SMSDispatcher.RECEIVE_SMS_PERMISSION); + } + + /** Returns whether or not there is storage available for an incoming SMS. */ + public boolean isStorageAvailable() { + return mStorageAvailable; + } + + private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_FULL)) { + mStorageAvailable = false; + mCm.reportSmsMemoryStatus(false, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); + } else if (intent.getAction().equals(Intent.ACTION_DEVICE_STORAGE_NOT_FULL)) { + mStorageAvailable = true; + mCm.reportSmsMemoryStatus(true, obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE)); + } + } + }; +} diff --git a/src/java/com/android/internal/telephony/SmsUsageMonitor.java b/src/java/com/android/internal/telephony/SmsUsageMonitor.java new file mode 100644 index 0000000..1804d97 --- /dev/null +++ b/src/java/com/android/internal/telephony/SmsUsageMonitor.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Message; +import android.provider.Settings; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Implement the per-application based SMS control, which limits the number of + * SMS/MMS messages an app can send in the checking period. + * + * This code was formerly part of {@link SMSDispatcher}, and has been moved + * into a separate class to support instantiation of multiple SMSDispatchers on + * dual-mode devices that require support for both 3GPP and 3GPP2 format messages. + */ +public class SmsUsageMonitor { + private static final String TAG = "SmsUsageMonitor"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + /** Default checking period for SMS sent without user permission. */ + private static final int DEFAULT_SMS_CHECK_PERIOD = 1800000; // 30 minutes + + /** Default number of SMS sent in checking period without user permission. */ + private static final int DEFAULT_SMS_MAX_COUNT = 30; + + /** Return value from {@link #checkDestination} for regular phone numbers. */ + static final int CATEGORY_NOT_SHORT_CODE = 0; + + /** Return value from {@link #checkDestination} for free (no cost) short codes. */ + static final int CATEGORY_FREE_SHORT_CODE = 1; + + /** Return value from {@link #checkDestination} for standard rate (non-premium) short codes. */ + static final int CATEGORY_STANDARD_SHORT_CODE = 2; + + /** Return value from {@link #checkDestination} for possible premium short codes. */ + static final int CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE = 3; + + /** Return value from {@link #checkDestination} for premium short codes. */ + static final int CATEGORY_PREMIUM_SHORT_CODE = 4; + + private final int mCheckPeriod; + private final int mMaxAllowed; + + private final HashMap<String, ArrayList<Long>> mSmsStamp = + new HashMap<String, ArrayList<Long>>(); + + /** Context for retrieving regexes from XML resource. */ + private final Context mContext; + + /** Country code for the cached short code pattern matcher. */ + private String mCurrentCountry; + + /** Cached short code pattern matcher for {@link #mCurrentCountry}. */ + private ShortCodePatternMatcher mCurrentPatternMatcher; + + /** Cached short code regex patterns from secure settings for {@link #mCurrentCountry}. */ + private String mSettingsShortCodePatterns; + + /** Handler for responding to content observer updates. */ + private final SettingsObserverHandler mSettingsObserverHandler; + + /** XML tag for root element. */ + private static final String TAG_SHORTCODES = "shortcodes"; + + /** XML tag for short code patterns for a specific country. */ + private static final String TAG_SHORTCODE = "shortcode"; + + /** XML attribute for the country code. */ + private static final String ATTR_COUNTRY = "country"; + + /** XML attribute for the short code regex pattern. */ + private static final String ATTR_PATTERN = "pattern"; + + /** XML attribute for the premium short code regex pattern. */ + private static final String ATTR_PREMIUM = "premium"; + + /** XML attribute for the free short code regex pattern. */ + private static final String ATTR_FREE = "free"; + + /** XML attribute for the standard rate short code regex pattern. */ + private static final String ATTR_STANDARD = "standard"; + + /** + * SMS short code regex pattern matcher for a specific country. + */ + private static final class ShortCodePatternMatcher { + private final Pattern mShortCodePattern; + private final Pattern mPremiumShortCodePattern; + private final Pattern mFreeShortCodePattern; + private final Pattern mStandardShortCodePattern; + + ShortCodePatternMatcher(String shortCodeRegex, String premiumShortCodeRegex, + String freeShortCodeRegex, String standardShortCodeRegex) { + mShortCodePattern = (shortCodeRegex != null ? Pattern.compile(shortCodeRegex) : null); + mPremiumShortCodePattern = (premiumShortCodeRegex != null ? + Pattern.compile(premiumShortCodeRegex) : null); + mFreeShortCodePattern = (freeShortCodeRegex != null ? + Pattern.compile(freeShortCodeRegex) : null); + mStandardShortCodePattern = (standardShortCodeRegex != null ? + Pattern.compile(standardShortCodeRegex) : null); + } + + int getNumberCategory(String phoneNumber) { + if (mFreeShortCodePattern != null && mFreeShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_FREE_SHORT_CODE; + } + if (mStandardShortCodePattern != null && mStandardShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_STANDARD_SHORT_CODE; + } + if (mPremiumShortCodePattern != null && mPremiumShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_PREMIUM_SHORT_CODE; + } + if (mShortCodePattern != null && mShortCodePattern.matcher(phoneNumber).matches()) { + return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; + } + return CATEGORY_NOT_SHORT_CODE; + } + } + + /** + * Observe the secure setting for updated regex patterns. + */ + private static class SettingsObserver extends ContentObserver { + private final int mWhat; + private final Handler mHandler; + + SettingsObserver(Handler handler, int what) { + super(handler); + mHandler = handler; + mWhat = what; + } + + @Override + public void onChange(boolean selfChange) { + mHandler.obtainMessage(mWhat).sendToTarget(); + } + } + + /** + * Handler to update regex patterns when secure setting for the current country is updated. + */ + private class SettingsObserverHandler extends Handler { + /** Current content observer, or null. */ + SettingsObserver mSettingsObserver; + + /** Current country code to watch for settings updates. */ + private String mCountryIso; + + /** Request to start observing a secure setting. */ + static final int OBSERVE_SETTING = 1; + + /** Handler event for updated secure settings. */ + static final int SECURE_SETTINGS_CHANGED = 2; + + /** Send a message to this handler requesting to observe the setting for a new country. */ + void observeSettingForCountry(String countryIso) { + obtainMessage(OBSERVE_SETTING, countryIso).sendToTarget(); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case OBSERVE_SETTING: + if (msg.obj != null && msg.obj instanceof String) { + mCountryIso = (String) msg.obj; + String settingName = getSettingNameForCountry(mCountryIso); + ContentResolver resolver = mContext.getContentResolver(); + + if (mSettingsObserver != null) { + if (VDBG) log("Unregistering old content observer"); + resolver.unregisterContentObserver(mSettingsObserver); + } + + mSettingsObserver = new SettingsObserver(this, SECURE_SETTINGS_CHANGED); + resolver.registerContentObserver( + Settings.Secure.getUriFor(settingName), false, mSettingsObserver); + if (VDBG) log("Registered content observer for " + settingName); + } + break; + + case SECURE_SETTINGS_CHANGED: + loadPatternsFromSettings(mCountryIso); + break; + } + } + } + + /** + * Create SMS usage monitor. + * @param context the context to use to load resources and get TelephonyManager service + */ + public SmsUsageMonitor(Context context) { + mContext = context; + ContentResolver resolver = context.getContentResolver(); + + mMaxAllowed = Settings.Secure.getInt(resolver, + Settings.Secure.SMS_OUTGOING_CHECK_MAX_COUNT, + DEFAULT_SMS_MAX_COUNT); + + mCheckPeriod = Settings.Secure.getInt(resolver, + Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS, + DEFAULT_SMS_CHECK_PERIOD); + + mSettingsObserverHandler = new SettingsObserverHandler(); + } + + /** + * Return a pattern matcher object for the specified country. + * @param country the country to search for + * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found + */ + private ShortCodePatternMatcher getPatternMatcher(String country) { + int id = com.android.internal.R.xml.sms_short_codes; + XmlResourceParser parser = mContext.getResources().getXml(id); + + try { + return getPatternMatcher(country, parser); + } catch (XmlPullParserException e) { + Log.e(TAG, "XML parser exception reading short code pattern resource", e); + } catch (IOException e) { + Log.e(TAG, "I/O exception reading short code pattern resource", e); + } finally { + parser.close(); + } + return null; // country not found + } + + /** + * Return a pattern matcher object for the specified country from a secure settings string. + * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found + */ + private static ShortCodePatternMatcher getPatternMatcher(String country, String settingsPattern) { + // embed pattern tag into an XML document. + String document = "<shortcodes>" + settingsPattern + "</shortcodes>"; + if (VDBG) log("loading updated patterns from: " + document); + + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParser parser = factory.newPullParser(); + parser.setInput(new StringReader(document)); + return getPatternMatcher(country, parser); + } catch (XmlPullParserException e) { + Log.e(TAG, "XML parser exception reading short code pattern from settings", e); + } catch (IOException e) { + Log.e(TAG, "I/O exception reading short code pattern from settings", e); + } + return null; // country not found + } + + /** + * Return a pattern matcher object for the specified country and pattern XML parser. + * @param country the country to search for + * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found + */ + private static ShortCodePatternMatcher getPatternMatcher(String country, XmlPullParser parser) + throws XmlPullParserException, IOException + { + XmlUtils.beginDocument(parser, TAG_SHORTCODES); + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null) break; + + if (element.equals(TAG_SHORTCODE)) { + String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY); + if (country.equals(currentCountry)) { + String pattern = parser.getAttributeValue(null, ATTR_PATTERN); + String premium = parser.getAttributeValue(null, ATTR_PREMIUM); + String free = parser.getAttributeValue(null, ATTR_FREE); + String standard = parser.getAttributeValue(null, ATTR_STANDARD); + return new ShortCodePatternMatcher(pattern, premium, free, standard); + } + } else { + Log.e(TAG, "Error: skipping unknown XML tag " + element); + } + } + return null; // country not found + } + + /** Clear the SMS application list for disposal. */ + void dispose() { + mSmsStamp.clear(); + } + + /** + * Check to see if an application is allowed to send new SMS messages, and confirm with + * user if the send limit was reached or if a non-system app is potentially sending to a + * premium SMS short code or number. + * + * @param appName the package name of the app requesting to send an SMS + * @param smsWaiting the number of new messages desired to send + * @return true if application is allowed to send the requested number + * of new sms messages + */ + public boolean check(String appName, int smsWaiting) { + synchronized (mSmsStamp) { + removeExpiredTimestamps(); + + ArrayList<Long> sentList = mSmsStamp.get(appName); + if (sentList == null) { + sentList = new ArrayList<Long>(); + mSmsStamp.put(appName, sentList); + } + + return isUnderLimit(sentList, smsWaiting); + } + } + + /** + * Check if the destination is a possible premium short code. + * NOTE: the caller is expected to strip non-digits from the destination number with + * {@link PhoneNumberUtils#extractNetworkPortion} before calling this method. + * This happens in {@link SMSDispatcher#sendRawPdu} so that we use the same phone number + * for testing and in the user confirmation dialog if the user needs to confirm the number. + * This makes it difficult for malware to fool the user or the short code pattern matcher + * by using non-ASCII characters to make the number appear to be different from the real + * destination phone number. + * + * @param destAddress the destination address to test for possible short code + * @return {@link #CATEGORY_NOT_SHORT_CODE}, {@link #CATEGORY_FREE_SHORT_CODE}, + * {@link #CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE}, or {@link #CATEGORY_PREMIUM_SHORT_CODE}. + */ + public int checkDestination(String destAddress, String countryIso) { + synchronized (mSettingsObserverHandler) { + // always allow emergency numbers + if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) { + return CATEGORY_NOT_SHORT_CODE; + } + + ShortCodePatternMatcher patternMatcher = null; + + if (countryIso != null) { + // query secure settings and initialize content observer for updated regex patterns + if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry)) { + loadPatternsFromSettings(countryIso); + mSettingsObserverHandler.observeSettingForCountry(countryIso); + } + + if (countryIso.equals(mCurrentCountry)) { + patternMatcher = mCurrentPatternMatcher; + } else { + patternMatcher = getPatternMatcher(countryIso); + mCurrentCountry = countryIso; + mCurrentPatternMatcher = patternMatcher; // may be null if not found + } + } + + if (patternMatcher != null) { + return patternMatcher.getNumberCategory(destAddress); + } else { + // Generic rule: numbers of 5 digits or less are considered potential short codes + Log.e(TAG, "No patterns for \"" + countryIso + "\": using generic short code rule"); + if (destAddress.length() <= 5) { + return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; + } else { + return CATEGORY_NOT_SHORT_CODE; + } + } + } + } + + private static String getSettingNameForCountry(String countryIso) { + return Settings.Secure.SMS_SHORT_CODES_PREFIX + countryIso; + } + + /** + * Load regex patterns from secure settings if present. + * @param countryIso the country to search for + */ + void loadPatternsFromSettings(String countryIso) { + synchronized (mSettingsObserverHandler) { + if (VDBG) log("loadPatternsFromSettings(" + countryIso + ") called"); + String settingsPatterns = Settings.Secure.getString( + mContext.getContentResolver(), getSettingNameForCountry(countryIso)); + if (settingsPatterns != null && !settingsPatterns.equals( + mSettingsShortCodePatterns)) { + // settings pattern string has changed: update the pattern matcher + mSettingsShortCodePatterns = settingsPatterns; + ShortCodePatternMatcher matcher = getPatternMatcher(countryIso, settingsPatterns); + if (matcher != null) { + mCurrentCountry = countryIso; + mCurrentPatternMatcher = matcher; + } + } else if (settingsPatterns == null && mSettingsShortCodePatterns != null) { + // pattern string was removed: caller will load default patterns from XML resource + mCurrentCountry = null; + mCurrentPatternMatcher = null; + mSettingsShortCodePatterns = null; + } + } + } + + /** + * Remove keys containing only old timestamps. This can happen if an SMS app is used + * to send messages and then uninstalled. + */ + private void removeExpiredTimestamps() { + long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod; + + synchronized (mSmsStamp) { + Iterator<Map.Entry<String, ArrayList<Long>>> iter = mSmsStamp.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry<String, ArrayList<Long>> entry = iter.next(); + ArrayList<Long> oldList = entry.getValue(); + if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) { + iter.remove(); + } + } + } + } + + private boolean isUnderLimit(ArrayList<Long> sent, int smsWaiting) { + Long ct = System.currentTimeMillis(); + long beginCheckPeriod = ct - mCheckPeriod; + + if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct); + + while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) { + sent.remove(0); + } + + if ((sent.size() + smsWaiting) <= mMaxAllowed) { + for (int i = 0; i < smsWaiting; i++ ) { + sent.add(ct); + } + return true; + } + return false; + } + + private static void log(String msg) { + Log.d(TAG, msg); + } +} diff --git a/src/java/com/android/internal/telephony/TelephonyCapabilities.java b/src/java/com/android/internal/telephony/TelephonyCapabilities.java new file mode 100644 index 0000000..a9e9376 --- /dev/null +++ b/src/java/com/android/internal/telephony/TelephonyCapabilities.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +import android.util.Log; + +import com.android.internal.telephony.Phone; + +/** + * Utilities that check if the phone supports specified capabilities. + */ +public class TelephonyCapabilities { + private static final String LOG_TAG = "TelephonyCapabilities"; + + /** This class is never instantiated. */ + private TelephonyCapabilities() { + } + + /** + * Return true if the current phone supports ECM ("Emergency Callback + * Mode"), which is a feature where the device goes into a special + * state for a short period of time after making an outgoing emergency + * call. + * + * (On current devices, that state lasts 5 minutes. It prevents data + * usage by other apps, to avoid conflicts with any possible incoming + * calls. It also puts up a notification in the status bar, showing a + * countdown while ECM is active, and allowing the user to exit ECM.) + * + * Currently this is assumed to be true for CDMA phones, and false + * otherwise. + */ + public static boolean supportsEcm(Phone phone) { + return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA); + } + + /** + * Return true if the current phone supports Over The Air Service + * Provisioning (OTASP) + * + * Currently this is assumed to be true for CDMA phones, and false + * otherwise. + * + * TODO: Watch out: this is also highly carrier-specific, since the + * OTASP procedure is different from one carrier to the next, *and* the + * different carriers may want very different onscreen UI as well. + * The procedure may even be different for different devices with the + * same carrier. + * + * So we eventually will need a much more flexible, pluggable design. + * This method here is just a placeholder to reduce hardcoded + * "if (CDMA)" checks sprinkled throughout the phone app. + */ + public static boolean supportsOtasp(Phone phone) { + return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA); + } + + /** + * Return true if the current phone can retrieve the voice message count. + * + * Currently this is assumed to be true on CDMA phones and false otherwise. + */ + public static boolean supportsVoiceMessageCount(Phone phone) { + return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA); + } + + /** + * Return true if this phone allows the user to select which + * network to use. + * + * Currently this is assumed to be true only on GSM phones. + * + * TODO: Should CDMA phones allow this as well? + */ + public static boolean supportsNetworkSelection(Phone phone) { + return (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM); + } + + /** + * Returns a resource ID for a label to use when displaying the + * "device id" of the current device. (This is currently used as the + * title of the "device id" dialog.) + * + * This is specific to the device's telephony technology: the device + * id is called "IMEI" on GSM phones and "MEID" on CDMA phones. + */ + public static int getDeviceIdLabel(Phone phone) { + if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) { + return com.android.internal.R.string.imei; + } else if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) { + return com.android.internal.R.string.meid; + } else { + Log.w(LOG_TAG, "getDeviceIdLabel: no known label for phone " + + phone.getPhoneName()); + return 0; + } + } + + /** + * Return true if the current phone supports the ability to explicitly + * manage the state of a conference call (i.e. view the participants, + * and hangup or separate individual callers.) + * + * The in-call screen's "Manage conference" UI is available only on + * devices that support this feature. + * + * Currently this is assumed to be true on GSM phones and false otherwise. + */ + public static boolean supportsConferenceCallManagement(Phone phone) { + return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) + || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)); + } + + /** + * Return true if the current phone supports explicit "Hold" and + * "Unhold" actions for an active call. (If so, the in-call UI will + * provide onscreen "Hold" / "Unhold" buttons.) + * + * Currently this is assumed to be true on GSM phones and false + * otherwise. (In particular, CDMA has no concept of "putting a call + * on hold.") + */ + public static boolean supportsHoldAndUnhold(Phone phone) { + return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) + || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)); + } + + /** + * Return true if the current phone supports distinct "Answer & Hold" + * and "Answer & End" behaviors in the call-waiting scenario. If so, + * the in-call UI may provide separate buttons or menu items for these + * two actions. + * + * Currently this is assumed to be true on GSM phones and false + * otherwise. (In particular, CDMA has no concept of explicitly + * managing the background call, or "putting a call on hold.") + * + * TODO: It might be better to expose this capability in a more + * generic form, like maybe "supportsExplicitMultipleLineManagement()" + * rather than focusing specifically on call-waiting behavior. + */ + public static boolean supportsAnswerAndHold(Phone phone) { + return ((phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM) + || (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)); + } + + /** + * Return true if phones with the given phone type support ADN + * (Abbreviated Dialing Numbers). + * + * Currently this returns true when the phone type is GSM + * ({@link Phone#PHONE_TYPE_GSM}). + * + * This is using int for an argument for letting apps outside + * Phone process access to it, while other methods in this class is + * using Phone object. + * + * TODO: Theoretically phones other than GSM may have the ADN capability. + * Consider having better check here, or have better capability as part + * of public API, with which the argument should be replaced with + * something more appropriate. + */ + public static boolean supportsAdn(int phoneType) { + return phoneType == PhoneConstants.PHONE_TYPE_GSM; + } + + /** + * Returns true if the device can distinguish the phone's dialing state + * (Call.State.DIALING/ALERTING) and connected state (Call.State.ACTIVE). + * + * Currently this returns true for GSM phones as we cannot know when a CDMA + * phone has transitioned from dialing/active to connected. + */ + public static boolean canDistinguishDialingAndConnected(int phoneType) { + return phoneType == PhoneConstants.PHONE_TYPE_GSM; + } +} diff --git a/src/java/com/android/internal/telephony/UUSInfo.java b/src/java/com/android/internal/telephony/UUSInfo.java new file mode 100644 index 0000000..801b845 --- /dev/null +++ b/src/java/com/android/internal/telephony/UUSInfo.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +public class UUSInfo { + + /* + * User-to-User signaling Info activation types derived from 3GPP 23.087 + * v8.0 + */ + + public static final int UUS_TYPE1_IMPLICIT = 0; + + public static final int UUS_TYPE1_REQUIRED = 1; + + public static final int UUS_TYPE1_NOT_REQUIRED = 2; + + public static final int UUS_TYPE2_REQUIRED = 3; + + public static final int UUS_TYPE2_NOT_REQUIRED = 4; + + public static final int UUS_TYPE3_REQUIRED = 5; + + public static final int UUS_TYPE3_NOT_REQUIRED = 6; + + /* + * User-to-User Signaling Information data coding schemes. Possible values + * for Octet 3 (Protocol Discriminator field) in the UUIE. The values have + * been specified in section 10.5.4.25 of 3GPP TS 24.008 + */ + + public static final int UUS_DCS_USP = 0; /* User specified protocol */ + + public static final int UUS_DCS_OSIHLP = 1; /* OSI higher layer protocol */ + + public static final int UUS_DCS_X244 = 2; /* X.244 */ + + public static final int UUS_DCS_RMCF = 3; /* + * Reserved for system management + * convergence function + */ + + public static final int UUS_DCS_IA5c = 4; /* IA5 characters */ + + private int uusType; + + private int uusDcs; + + private byte[] uusData; + + public UUSInfo() { + this.uusType = UUS_TYPE1_IMPLICIT; + this.uusDcs = UUS_DCS_IA5c; + this.uusData = null; + } + + public UUSInfo(int uusType, int uusDcs, byte[] uusData) { + this.uusType = uusType; + this.uusDcs = uusDcs; + this.uusData = uusData; + } + + public int getDcs() { + return uusDcs; + } + + public void setDcs(int uusDcs) { + this.uusDcs = uusDcs; + } + + public int getType() { + return uusType; + } + + public void setType(int uusType) { + this.uusType = uusType; + } + + public byte[] getUserData() { + return uusData; + } + + public void setUserData(byte[] uusData) { + this.uusData = uusData; + } +} diff --git a/src/java/com/android/internal/telephony/WapPushManagerParams.java b/src/java/com/android/internal/telephony/WapPushManagerParams.java new file mode 100644 index 0000000..11e5ff9 --- /dev/null +++ b/src/java/com/android/internal/telephony/WapPushManagerParams.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +/** + * WapPushManager constant value definitions + */ +public class WapPushManagerParams { + /** + * Application type activity + */ + public static final int APP_TYPE_ACTIVITY = 0; + + /** + * Application type service + */ + public static final int APP_TYPE_SERVICE = 1; + + /** + * Process Message return value + * Message is handled + */ + public static final int MESSAGE_HANDLED = 0x1; + + /** + * Process Message return value + * Application ID or content type was not found in the application ID table + */ + public static final int APP_QUERY_FAILED = 0x2; + + /** + * Process Message return value + * Receiver application signature check failed + */ + public static final int SIGNATURE_NO_MATCH = 0x4; + + /** + * Process Message return value + * Receiver application was not found + */ + public static final int INVALID_RECEIVER_NAME = 0x8; + + /** + * Process Message return value + * Unknown exception + */ + public static final int EXCEPTION_CAUGHT = 0x10; + + /** + * Process Message return value + * Need further processing after WapPushManager message processing + */ + public static final int FURTHER_PROCESSING = 0x8000; + +} + diff --git a/src/java/com/android/internal/telephony/WapPushOverSms.java b/src/java/com/android/internal/telephony/WapPushOverSms.java new file mode 100755 index 0000000..e2779dc --- /dev/null +++ b/src/java/com/android/internal/telephony/WapPushOverSms.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.app.Activity; +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.provider.Telephony; +import android.provider.Telephony.Sms.Intents; +import android.util.Log; +import android.os.IBinder; +import android.os.RemoteException; + +/** + * WAP push handler class. + * + * @hide + */ +public class WapPushOverSms { + private static final String LOG_TAG = "WAP PUSH"; + + private final Context mContext; + private WspTypeDecoder pduDecoder; + private SMSDispatcher mSmsDispatcher; + + /** + * Hold the wake lock for 5 seconds, which should be enough time for + * any receiver(s) to grab its own wake lock. + */ + private final int WAKE_LOCK_TIMEOUT = 5000; + + private final int BIND_RETRY_INTERVAL = 1000; + /** + * A handle to WapPushManager interface + */ + private WapPushConnection mWapConn = null; + private class WapPushConnection implements ServiceConnection { + private IWapPushManager mWapPushMan; + private Context mOwner; + + public WapPushConnection(Context ownerContext) { + mOwner = ownerContext; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + mWapPushMan = IWapPushManager.Stub.asInterface(service); + if (false) Log.v(LOG_TAG, "wappush manager connected to " + + mOwner.hashCode()); + } + + public void onServiceDisconnected(ComponentName name) { + mWapPushMan = null; + if (false) Log.v(LOG_TAG, "wappush manager disconnected."); + // WapPushManager must be always attached. + rebindWapPushManager(); + } + + /** + * bind WapPushManager + */ + public void bindWapPushManager() { + if (mWapPushMan != null) return; + + final ServiceConnection wapPushConnection = this; + + mOwner.bindService(new Intent(IWapPushManager.class.getName()), + wapPushConnection, Context.BIND_AUTO_CREATE); + } + + /** + * rebind WapPushManager + * This method is called when WapPushManager is disconnected unexpectedly. + */ + private void rebindWapPushManager() { + if (mWapPushMan != null) return; + + final ServiceConnection wapPushConnection = this; + new Thread() { + public void run() { + while (mWapPushMan == null) { + mOwner.bindService(new Intent(IWapPushManager.class.getName()), + wapPushConnection, Context.BIND_AUTO_CREATE); + try { + Thread.sleep(BIND_RETRY_INTERVAL); + } catch (InterruptedException e) { + if (false) Log.v(LOG_TAG, "sleep interrupted."); + } + } + } + }.start(); + } + + /** + * Returns interface to WapPushManager + */ + public IWapPushManager getWapPushManager() { + return mWapPushMan; + } + } + + public WapPushOverSms(Phone phone, SMSDispatcher smsDispatcher) { + mSmsDispatcher = smsDispatcher; + mContext = phone.getContext(); + mWapConn = new WapPushConnection(mContext); + mWapConn.bindWapPushManager(); + } + + + /** + * Dispatches inbound messages that are in the WAP PDU format. See + * wap-230-wsp-20010705-a section 8 for details on the WAP PDU format. + * + * @param pdu The WAP PDU, made up of one or more SMS PDUs + * @return a result code from {@link Telephony.Sms.Intents}, or + * {@link Activity#RESULT_OK} if the message has been broadcast + * to applications + */ + public int dispatchWapPdu(byte[] pdu) { + + if (false) Log.d(LOG_TAG, "Rx: " + IccUtils.bytesToHexString(pdu)); + + int index = 0; + int transactionId = pdu[index++] & 0xFF; + int pduType = pdu[index++] & 0xFF; + int headerLength = 0; + + if ((pduType != WspTypeDecoder.PDU_TYPE_PUSH) && + (pduType != WspTypeDecoder.PDU_TYPE_CONFIRMED_PUSH)) { + if (false) Log.w(LOG_TAG, "Received non-PUSH WAP PDU. Type = " + pduType); + return Intents.RESULT_SMS_HANDLED; + } + + pduDecoder = new WspTypeDecoder(pdu); + + /** + * Parse HeaderLen(unsigned integer). + * From wap-230-wsp-20010705-a section 8.1.2 + * The maximum size of a uintvar is 32 bits. + * So it will be encoded in no more than 5 octets. + */ + if (pduDecoder.decodeUintvarInteger(index) == false) { + if (false) Log.w(LOG_TAG, "Received PDU. Header Length error."); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + headerLength = (int)pduDecoder.getValue32(); + index += pduDecoder.getDecodedDataLength(); + + int headerStartIndex = index; + + /** + * Parse Content-Type. + * From wap-230-wsp-20010705-a section 8.4.2.24 + * + * Content-type-value = Constrained-media | Content-general-form + * Content-general-form = Value-length Media-type + * Media-type = (Well-known-media | Extension-Media) *(Parameter) + * Value-length = Short-length | (Length-quote Length) + * Short-length = <Any octet 0-30> (octet <= WAP_PDU_SHORT_LENGTH_MAX) + * Length-quote = <Octet 31> (WAP_PDU_LENGTH_QUOTE) + * Length = Uintvar-integer + */ + if (pduDecoder.decodeContentType(index) == false) { + if (false) Log.w(LOG_TAG, "Received PDU. Header Content-Type error."); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + + String mimeType = pduDecoder.getValueString(); + long binaryContentType = pduDecoder.getValue32(); + index += pduDecoder.getDecodedDataLength(); + + byte[] header = new byte[headerLength]; + System.arraycopy(pdu, headerStartIndex, header, 0, header.length); + + byte[] intentData; + + if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_CO)) { + intentData = pdu; + } else { + int dataIndex = headerStartIndex + headerLength; + intentData = new byte[pdu.length - dataIndex]; + System.arraycopy(pdu, dataIndex, intentData, 0, intentData.length); + } + + /** + * Seek for application ID field in WSP header. + * If application ID is found, WapPushManager substitute the message + * processing. Since WapPushManager is optional module, if WapPushManager + * is not found, legacy message processing will be continued. + */ + if (pduDecoder.seekXWapApplicationId(index, index + headerLength - 1)) { + index = (int) pduDecoder.getValue32(); + pduDecoder.decodeXWapApplicationId(index); + String wapAppId = pduDecoder.getValueString(); + if (wapAppId == null) { + wapAppId = Integer.toString((int) pduDecoder.getValue32()); + } + + String contentType = ((mimeType == null) ? + Long.toString(binaryContentType) : mimeType); + if (false) Log.v(LOG_TAG, "appid found: " + wapAppId + ":" + contentType); + + try { + boolean processFurther = true; + IWapPushManager wapPushMan = mWapConn.getWapPushManager(); + + if (wapPushMan == null) { + if (false) Log.w(LOG_TAG, "wap push manager not found!"); + } else { + Intent intent = new Intent(); + intent.putExtra("transactionId", transactionId); + intent.putExtra("pduType", pduType); + intent.putExtra("header", header); + intent.putExtra("data", intentData); + intent.putExtra("contentTypeParameters", + pduDecoder.getContentParameters()); + + int procRet = wapPushMan.processMessage(wapAppId, contentType, intent); + if (false) Log.v(LOG_TAG, "procRet:" + procRet); + if ((procRet & WapPushManagerParams.MESSAGE_HANDLED) > 0 + && (procRet & WapPushManagerParams.FURTHER_PROCESSING) == 0) { + processFurther = false; + } + } + if (!processFurther) { + return Intents.RESULT_SMS_HANDLED; + } + } catch (RemoteException e) { + if (false) Log.w(LOG_TAG, "remote func failed..."); + } + } + if (false) Log.v(LOG_TAG, "fall back to existing handler"); + + if (mimeType == null) { + if (false) Log.w(LOG_TAG, "Header Content-Type error."); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + + String permission; + + if (mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_MMS)) { + permission = "android.permission.RECEIVE_MMS"; + } else { + permission = "android.permission.RECEIVE_WAP_PUSH"; + } + + Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION); + intent.setType(mimeType); + intent.putExtra("transactionId", transactionId); + intent.putExtra("pduType", pduType); + intent.putExtra("header", header); + intent.putExtra("data", intentData); + intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters()); + + mSmsDispatcher.dispatch(intent, permission); + + return Activity.RESULT_OK; + } +} diff --git a/src/java/com/android/internal/telephony/WspTypeDecoder.java b/src/java/com/android/internal/telephony/WspTypeDecoder.java new file mode 100755 index 0000000..73260fb --- /dev/null +++ b/src/java/com/android/internal/telephony/WspTypeDecoder.java @@ -0,0 +1,731 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import java.util.HashMap; + +/** + * Implement the WSP data type decoder. + * + * @hide + */ +public class WspTypeDecoder { + + private static final int WAP_PDU_SHORT_LENGTH_MAX = 30; + private static final int WAP_PDU_LENGTH_QUOTE = 31; + + public static final int PDU_TYPE_PUSH = 0x06; + public static final int PDU_TYPE_CONFIRMED_PUSH = 0x07; + + private final static HashMap<Integer, String> WELL_KNOWN_MIME_TYPES = + new HashMap<Integer, String>(); + + private final static HashMap<Integer, String> WELL_KNOWN_PARAMETERS = + new HashMap<Integer, String>(); + + public static final int PARAMETER_ID_X_WAP_APPLICATION_ID = 0x2f; + private static final int Q_VALUE = 0x00; + + static { + WELL_KNOWN_MIME_TYPES.put(0x00, "*/*"); + WELL_KNOWN_MIME_TYPES.put(0x01, "text/*"); + WELL_KNOWN_MIME_TYPES.put(0x02, "text/html"); + WELL_KNOWN_MIME_TYPES.put(0x03, "text/plain"); + WELL_KNOWN_MIME_TYPES.put(0x04, "text/x-hdml"); + WELL_KNOWN_MIME_TYPES.put(0x05, "text/x-ttml"); + WELL_KNOWN_MIME_TYPES.put(0x06, "text/x-vCalendar"); + WELL_KNOWN_MIME_TYPES.put(0x07, "text/x-vCard"); + WELL_KNOWN_MIME_TYPES.put(0x08, "text/vnd.wap.wml"); + WELL_KNOWN_MIME_TYPES.put(0x09, "text/vnd.wap.wmlscript"); + WELL_KNOWN_MIME_TYPES.put(0x0A, "text/vnd.wap.wta-event"); + WELL_KNOWN_MIME_TYPES.put(0x0B, "multipart/*"); + WELL_KNOWN_MIME_TYPES.put(0x0C, "multipart/mixed"); + WELL_KNOWN_MIME_TYPES.put(0x0D, "multipart/form-data"); + WELL_KNOWN_MIME_TYPES.put(0x0E, "multipart/byterantes"); + WELL_KNOWN_MIME_TYPES.put(0x0F, "multipart/alternative"); + WELL_KNOWN_MIME_TYPES.put(0x10, "application/*"); + WELL_KNOWN_MIME_TYPES.put(0x11, "application/java-vm"); + WELL_KNOWN_MIME_TYPES.put(0x12, "application/x-www-form-urlencoded"); + WELL_KNOWN_MIME_TYPES.put(0x13, "application/x-hdmlc"); + WELL_KNOWN_MIME_TYPES.put(0x14, "application/vnd.wap.wmlc"); + WELL_KNOWN_MIME_TYPES.put(0x15, "application/vnd.wap.wmlscriptc"); + WELL_KNOWN_MIME_TYPES.put(0x16, "application/vnd.wap.wta-eventc"); + WELL_KNOWN_MIME_TYPES.put(0x17, "application/vnd.wap.uaprof"); + WELL_KNOWN_MIME_TYPES.put(0x18, "application/vnd.wap.wtls-ca-certificate"); + WELL_KNOWN_MIME_TYPES.put(0x19, "application/vnd.wap.wtls-user-certificate"); + WELL_KNOWN_MIME_TYPES.put(0x1A, "application/x-x509-ca-cert"); + WELL_KNOWN_MIME_TYPES.put(0x1B, "application/x-x509-user-cert"); + WELL_KNOWN_MIME_TYPES.put(0x1C, "image/*"); + WELL_KNOWN_MIME_TYPES.put(0x1D, "image/gif"); + WELL_KNOWN_MIME_TYPES.put(0x1E, "image/jpeg"); + WELL_KNOWN_MIME_TYPES.put(0x1F, "image/tiff"); + WELL_KNOWN_MIME_TYPES.put(0x20, "image/png"); + WELL_KNOWN_MIME_TYPES.put(0x21, "image/vnd.wap.wbmp"); + WELL_KNOWN_MIME_TYPES.put(0x22, "application/vnd.wap.multipart.*"); + WELL_KNOWN_MIME_TYPES.put(0x23, "application/vnd.wap.multipart.mixed"); + WELL_KNOWN_MIME_TYPES.put(0x24, "application/vnd.wap.multipart.form-data"); + WELL_KNOWN_MIME_TYPES.put(0x25, "application/vnd.wap.multipart.byteranges"); + WELL_KNOWN_MIME_TYPES.put(0x26, "application/vnd.wap.multipart.alternative"); + WELL_KNOWN_MIME_TYPES.put(0x27, "application/xml"); + WELL_KNOWN_MIME_TYPES.put(0x28, "text/xml"); + WELL_KNOWN_MIME_TYPES.put(0x29, "application/vnd.wap.wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x2A, "application/x-x968-cross-cert"); + WELL_KNOWN_MIME_TYPES.put(0x2B, "application/x-x968-ca-cert"); + WELL_KNOWN_MIME_TYPES.put(0x2C, "application/x-x968-user-cert"); + WELL_KNOWN_MIME_TYPES.put(0x2D, "text/vnd.wap.si"); + WELL_KNOWN_MIME_TYPES.put(0x2E, "application/vnd.wap.sic"); + WELL_KNOWN_MIME_TYPES.put(0x2F, "text/vnd.wap.sl"); + WELL_KNOWN_MIME_TYPES.put(0x30, "application/vnd.wap.slc"); + WELL_KNOWN_MIME_TYPES.put(0x31, "text/vnd.wap.co"); + WELL_KNOWN_MIME_TYPES.put(0x32, "application/vnd.wap.coc"); + WELL_KNOWN_MIME_TYPES.put(0x33, "application/vnd.wap.multipart.related"); + WELL_KNOWN_MIME_TYPES.put(0x34, "application/vnd.wap.sia"); + WELL_KNOWN_MIME_TYPES.put(0x35, "text/vnd.wap.connectivity-xml"); + WELL_KNOWN_MIME_TYPES.put(0x36, "application/vnd.wap.connectivity-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x37, "application/pkcs7-mime"); + WELL_KNOWN_MIME_TYPES.put(0x38, "application/vnd.wap.hashed-certificate"); + WELL_KNOWN_MIME_TYPES.put(0x39, "application/vnd.wap.signed-certificate"); + WELL_KNOWN_MIME_TYPES.put(0x3A, "application/vnd.wap.cert-response"); + WELL_KNOWN_MIME_TYPES.put(0x3B, "application/xhtml+xml"); + WELL_KNOWN_MIME_TYPES.put(0x3C, "application/wml+xml"); + WELL_KNOWN_MIME_TYPES.put(0x3D, "text/css"); + WELL_KNOWN_MIME_TYPES.put(0x3E, "application/vnd.wap.mms-message"); + WELL_KNOWN_MIME_TYPES.put(0x3F, "application/vnd.wap.rollover-certificate"); + WELL_KNOWN_MIME_TYPES.put(0x40, "application/vnd.wap.locc+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x41, "application/vnd.wap.loc+xml"); + WELL_KNOWN_MIME_TYPES.put(0x42, "application/vnd.syncml.dm+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x43, "application/vnd.syncml.dm+xml"); + WELL_KNOWN_MIME_TYPES.put(0x44, "application/vnd.syncml.notification"); + WELL_KNOWN_MIME_TYPES.put(0x45, "application/vnd.wap.xhtml+xml"); + WELL_KNOWN_MIME_TYPES.put(0x46, "application/vnd.wv.csp.cir"); + WELL_KNOWN_MIME_TYPES.put(0x47, "application/vnd.oma.dd+xml"); + WELL_KNOWN_MIME_TYPES.put(0x48, "application/vnd.oma.drm.message"); + WELL_KNOWN_MIME_TYPES.put(0x49, "application/vnd.oma.drm.content"); + WELL_KNOWN_MIME_TYPES.put(0x4A, "application/vnd.oma.drm.rights+xml"); + WELL_KNOWN_MIME_TYPES.put(0x4B, "application/vnd.oma.drm.rights+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x4C, "application/vnd.wv.csp+xml"); + WELL_KNOWN_MIME_TYPES.put(0x4D, "application/vnd.wv.csp+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x4E, "application/vnd.syncml.ds.notification"); + WELL_KNOWN_MIME_TYPES.put(0x4F, "audio/*"); + WELL_KNOWN_MIME_TYPES.put(0x50, "video/*"); + WELL_KNOWN_MIME_TYPES.put(0x51, "application/vnd.oma.dd2+xml"); + WELL_KNOWN_MIME_TYPES.put(0x52, "application/mikey"); + WELL_KNOWN_MIME_TYPES.put(0x53, "application/vnd.oma.dcd"); + WELL_KNOWN_MIME_TYPES.put(0x54, "application/vnd.oma.dcdc"); + + WELL_KNOWN_MIME_TYPES.put(0x0201, "application/vnd.uplanet.cacheop-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0202, "application/vnd.uplanet.signal"); + WELL_KNOWN_MIME_TYPES.put(0x0203, "application/vnd.uplanet.alert-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0204, "application/vnd.uplanet.list-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0205, "application/vnd.uplanet.listcmd-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0206, "application/vnd.uplanet.channel-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0207, "application/vnd.uplanet.provisioning-status-uri"); + WELL_KNOWN_MIME_TYPES.put(0x0208, "x-wap.multipart/vnd.uplanet.header-set"); + WELL_KNOWN_MIME_TYPES.put(0x0209, "application/vnd.uplanet.bearer-choice-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x020A, "application/vnd.phonecom.mmc-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x020B, "application/vnd.nokia.syncset+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x020C, "image/x-up-wpng"); + WELL_KNOWN_MIME_TYPES.put(0x0300, "application/iota.mmc-wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0301, "application/iota.mmc-xml"); + WELL_KNOWN_MIME_TYPES.put(0x0302, "application/vnd.syncml+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0303, "application/vnd.syncml+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0304, "text/vnd.wap.emn+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0305, "text/calendar"); + WELL_KNOWN_MIME_TYPES.put(0x0306, "application/vnd.omads-email+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0307, "application/vnd.omads-file+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0308, "application/vnd.omads-folder+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0309, "text/directory;profile=vCard"); + WELL_KNOWN_MIME_TYPES.put(0x030A, "application/vnd.wap.emn+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x030B, "application/vnd.nokia.ipdc-purchase-response"); + WELL_KNOWN_MIME_TYPES.put(0x030C, "application/vnd.motorola.screen3+xml"); + WELL_KNOWN_MIME_TYPES.put(0x030D, "application/vnd.motorola.screen3+gzip"); + WELL_KNOWN_MIME_TYPES.put(0x030E, "application/vnd.cmcc.setting+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x030F, "application/vnd.cmcc.bombing+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0310, "application/vnd.docomo.pf"); + WELL_KNOWN_MIME_TYPES.put(0x0311, "application/vnd.docomo.ub"); + WELL_KNOWN_MIME_TYPES.put(0x0312, "application/vnd.omaloc-supl-init"); + WELL_KNOWN_MIME_TYPES.put(0x0313, "application/vnd.oma.group-usage-list+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0314, "application/oma-directory+xml"); + WELL_KNOWN_MIME_TYPES.put(0x0315, "application/vnd.docomo.pf2"); + WELL_KNOWN_MIME_TYPES.put(0x0316, "application/vnd.oma.drm.roap-trigger+wbxml"); + WELL_KNOWN_MIME_TYPES.put(0x0317, "application/vnd.sbm.mid2"); + WELL_KNOWN_MIME_TYPES.put(0x0318, "application/vnd.wmf.bootstrap"); + WELL_KNOWN_MIME_TYPES.put(0x0319, "application/vnc.cmcc.dcd+xml"); + WELL_KNOWN_MIME_TYPES.put(0x031A, "application/vnd.sbm.cid"); + WELL_KNOWN_MIME_TYPES.put(0x031B, "application/vnd.oma.bcast.provisioningtrigger"); + + WELL_KNOWN_PARAMETERS.put(0x00, "Q"); + WELL_KNOWN_PARAMETERS.put(0x01, "Charset"); + WELL_KNOWN_PARAMETERS.put(0x02, "Level"); + WELL_KNOWN_PARAMETERS.put(0x03, "Type"); + WELL_KNOWN_PARAMETERS.put(0x07, "Differences"); + WELL_KNOWN_PARAMETERS.put(0x08, "Padding"); + WELL_KNOWN_PARAMETERS.put(0x09, "Type"); + WELL_KNOWN_PARAMETERS.put(0x0E, "Max-Age"); + WELL_KNOWN_PARAMETERS.put(0x10, "Secure"); + WELL_KNOWN_PARAMETERS.put(0x11, "SEC"); + WELL_KNOWN_PARAMETERS.put(0x12, "MAC"); + WELL_KNOWN_PARAMETERS.put(0x13, "Creation-date"); + WELL_KNOWN_PARAMETERS.put(0x14, "Modification-date"); + WELL_KNOWN_PARAMETERS.put(0x15, "Read-date"); + WELL_KNOWN_PARAMETERS.put(0x16, "Size"); + WELL_KNOWN_PARAMETERS.put(0x17, "Name"); + WELL_KNOWN_PARAMETERS.put(0x18, "Filename"); + WELL_KNOWN_PARAMETERS.put(0x19, "Start"); + WELL_KNOWN_PARAMETERS.put(0x1A, "Start-info"); + WELL_KNOWN_PARAMETERS.put(0x1B, "Comment"); + WELL_KNOWN_PARAMETERS.put(0x1C, "Domain"); + WELL_KNOWN_PARAMETERS.put(0x1D, "Path"); + } + + public static final String CONTENT_TYPE_B_PUSH_CO = "application/vnd.wap.coc"; + public static final String CONTENT_TYPE_B_MMS = "application/vnd.wap.mms-message"; + public static final String CONTENT_TYPE_B_PUSH_SYNCML_NOTI = "application/vnd.syncml.notification"; + + byte[] wspData; + int dataLength; + long unsigned32bit; + String stringValue; + + HashMap<String, String> contentParameters; + + public WspTypeDecoder(byte[] pdu) { + wspData = pdu; + } + + /** + * Decode the "Text-string" type for WSP pdu + * + * @param startIndex The starting position of the "Text-string" in this pdu + * + * @return false when error(not a Text-string) occur + * return value can be retrieved by getValueString() method length of data in pdu can be + * retrieved by getDecodedDataLength() method + */ + public boolean decodeTextString(int startIndex) { + int index = startIndex; + while (wspData[index] != 0) { + index++; + } + dataLength = index - startIndex + 1; + if (wspData[startIndex] == 127) { + stringValue = new String(wspData, startIndex + 1, dataLength - 2); + } else { + stringValue = new String(wspData, startIndex, dataLength - 1); + } + return true; + } + + /** + * Decode the "Token-text" type for WSP pdu + * + * @param startIndex The starting position of the "Token-text" in this pdu + * + * @return always true + * return value can be retrieved by getValueString() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeTokenText(int startIndex) { + int index = startIndex; + while (wspData[index] != 0) { + index++; + } + dataLength = index - startIndex + 1; + stringValue = new String(wspData, startIndex, dataLength - 1); + + return true; + } + + /** + * Decode the "Short-integer" type for WSP pdu + * + * @param startIndex The starting position of the "Short-integer" in this pdu + * + * @return false when error(not a Short-integer) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeShortInteger(int startIndex) { + if ((wspData[startIndex] & 0x80) == 0) { + return false; + } + unsigned32bit = wspData[startIndex] & 0x7f; + dataLength = 1; + return true; + } + + /** + * Decode the "Long-integer" type for WSP pdu + * + * @param startIndex The starting position of the "Long-integer" in this pdu + * + * @return false when error(not a Long-integer) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeLongInteger(int startIndex) { + int lengthMultiOctet = wspData[startIndex] & 0xff; + + if (lengthMultiOctet > WAP_PDU_SHORT_LENGTH_MAX) { + return false; + } + unsigned32bit = 0; + for (int i = 1; i <= lengthMultiOctet; i++) { + unsigned32bit = (unsigned32bit << 8) | (wspData[startIndex + i] & 0xff); + } + dataLength = 1 + lengthMultiOctet; + return true; + } + + /** + * Decode the "Integer-Value" type for WSP pdu + * + * @param startIndex The starting position of the "Integer-Value" in this pdu + * + * @return false when error(not a Integer-Value) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeIntegerValue(int startIndex) { + if (decodeShortInteger(startIndex) == true) { + return true; + } + return decodeLongInteger(startIndex); + } + + /** + * Decode the "Uintvar-integer" type for WSP pdu + * + * @param startIndex The starting position of the "Uintvar-integer" in this pdu + * + * @return false when error(not a Uintvar-integer) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeUintvarInteger(int startIndex) { + int index = startIndex; + + unsigned32bit = 0; + while ((wspData[index] & 0x80) != 0) { + if ((index - startIndex) >= 4) { + return false; + } + unsigned32bit = (unsigned32bit << 7) | (wspData[index] & 0x7f); + index++; + } + unsigned32bit = (unsigned32bit << 7) | (wspData[index] & 0x7f); + dataLength = index - startIndex + 1; + return true; + } + + /** + * Decode the "Value-length" type for WSP pdu + * + * @param startIndex The starting position of the "Value-length" in this pdu + * + * @return false when error(not a Value-length) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeValueLength(int startIndex) { + if ((wspData[startIndex] & 0xff) > WAP_PDU_LENGTH_QUOTE) { + return false; + } + if (wspData[startIndex] < WAP_PDU_LENGTH_QUOTE) { + unsigned32bit = wspData[startIndex]; + dataLength = 1; + } else { + decodeUintvarInteger(startIndex + 1); + dataLength++; + } + return true; + } + + /** + * Decode the "Extension-media" type for WSP PDU. + * + * @param startIndex The starting position of the "Extension-media" in this PDU. + * + * @return false on error, such as if there is no Extension-media at startIndex. + * Side-effects: updates stringValue (available with + * getValueString()), which will be null on error. The length of the + * data in the PDU is available with getValue32(), 0 on error. + */ + public boolean decodeExtensionMedia(int startIndex) { + int index = startIndex; + dataLength = 0; + stringValue = null; + int length = wspData.length; + boolean rtrn = index < length; + + while (index < length && wspData[index] != 0) { + index++; + } + + dataLength = index - startIndex + 1; + stringValue = new String(wspData, startIndex, dataLength - 1); + + return rtrn; + } + + /** + * Decode the "Constrained-encoding" type for WSP pdu + * + * @param startIndex The starting position of the "Constrained-encoding" in this pdu + * + * @return false when error(not a Constrained-encoding) occur + * return value can be retrieved first by getValueString() and second by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeConstrainedEncoding(int startIndex) { + if (decodeShortInteger(startIndex) == true) { + stringValue = null; + return true; + } + return decodeExtensionMedia(startIndex); + } + + /** + * Decode the "Content-type" type for WSP pdu + * + * @param startIndex The starting position of the "Content-type" in this pdu + * + * @return false when error(not a Content-type) occurs + * If a content type exists in the headers (either as inline string, or as well-known + * value), getValueString() will return it. If a 'well known value' is encountered that + * cannot be mapped to a string mime type, getValueString() will return null, and + * getValue32() will return the unknown content type value. + * length of data in pdu can be retrieved by getDecodedDataLength() method + * Any content type parameters will be accessible via getContentParameters() + */ + public boolean decodeContentType(int startIndex) { + int mediaPrefixLength; + contentParameters = new HashMap<String, String>(); + + try { + if (decodeValueLength(startIndex) == false) { + boolean found = decodeConstrainedEncoding(startIndex); + if (found) { + expandWellKnownMimeType(); + } + return found; + } + int headersLength = (int) unsigned32bit; + mediaPrefixLength = getDecodedDataLength(); + if (decodeIntegerValue(startIndex + mediaPrefixLength) == true) { + dataLength += mediaPrefixLength; + int readLength = dataLength; + stringValue = null; + expandWellKnownMimeType(); + long wellKnownValue = unsigned32bit; + String mimeType = stringValue; + if (readContentParameters(startIndex + dataLength, + (headersLength - (dataLength - mediaPrefixLength)), 0)) { + dataLength += readLength; + unsigned32bit = wellKnownValue; + stringValue = mimeType; + return true; + } + return false; + } + if (decodeExtensionMedia(startIndex + mediaPrefixLength) == true) { + dataLength += mediaPrefixLength; + int readLength = dataLength; + expandWellKnownMimeType(); + long wellKnownValue = unsigned32bit; + String mimeType = stringValue; + if (readContentParameters(startIndex + dataLength, + (headersLength - (dataLength - mediaPrefixLength)), 0)) { + dataLength += readLength; + unsigned32bit = wellKnownValue; + stringValue = mimeType; + return true; + } + } + } catch (ArrayIndexOutOfBoundsException e) { + //something doesn't add up + return false; + } + return false; + } + + private boolean readContentParameters(int startIndex, int leftToRead, int accumulator) { + + int totalRead = 0; + + if (leftToRead > 0) { + byte nextByte = wspData[startIndex]; + String value = null; + String param = null; + if ((nextByte & 0x80) == 0x00 && nextByte > 31) { // untyped + decodeTokenText(startIndex); + param = stringValue; + totalRead += dataLength; + } else { // typed + if (decodeIntegerValue(startIndex)) { + totalRead += dataLength; + int wellKnownParameterValue = (int) unsigned32bit; + param = WELL_KNOWN_PARAMETERS.get(wellKnownParameterValue); + if (param == null) { + param = "unassigned/0x" + Long.toHexString(wellKnownParameterValue); + } + // special case for the "Q" parameter, value is a uintvar + if (wellKnownParameterValue == Q_VALUE) { + if (decodeUintvarInteger(startIndex + totalRead)) { + totalRead += dataLength; + value = String.valueOf(unsigned32bit); + contentParameters.put(param, value); + return readContentParameters(startIndex + totalRead, leftToRead + - totalRead, accumulator + totalRead); + } else { + return false; + } + } + } else { + return false; + } + } + + if (decodeNoValue(startIndex + totalRead)) { + totalRead += dataLength; + value = null; + } else if (decodeIntegerValue(startIndex + totalRead)) { + totalRead += dataLength; + int intValue = (int) unsigned32bit; + if (intValue == 0) { + value = ""; + } else { + value = String.valueOf(intValue); + } + } else { + decodeTokenText(startIndex + totalRead); + totalRead += dataLength; + value = stringValue; + if (value.startsWith("\"")) { + // quoted string, so remove the quote + value = value.substring(1); + } + } + contentParameters.put(param, value); + return readContentParameters(startIndex + totalRead, leftToRead - totalRead, + accumulator + totalRead); + + } else { + dataLength = accumulator; + return true; + } + } + + /** + * Check if the next byte is No-Value + * + * @param startIndex The starting position of the "Content length" in this pdu + * + * @return true if and only if the next byte is 0x00 + */ + private boolean decodeNoValue(int startIndex) { + if (wspData[startIndex] == 0) { + dataLength = 1; + return true; + } else { + return false; + } + } + + /** + * Populate stringValue with the mime type corresponding to the value in unsigned32bit + * + * Sets unsigned32bit to -1 if stringValue is already populated + */ + private void expandWellKnownMimeType() { + if (stringValue == null) { + int binaryContentType = (int) unsigned32bit; + stringValue = WELL_KNOWN_MIME_TYPES.get(binaryContentType); + } else { + unsigned32bit = -1; + } + } + + /** + * Decode the "Content length" type for WSP pdu + * + * @param startIndex The starting position of the "Content length" in this pdu + * + * @return false when error(not a Content length) occur + * return value can be retrieved by getValue32() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeContentLength(int startIndex) { + return decodeIntegerValue(startIndex); + } + + /** + * Decode the "Content location" type for WSP pdu + * + * @param startIndex The starting position of the "Content location" in this pdu + * + * @return false when error(not a Content location) occur + * return value can be retrieved by getValueString() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeContentLocation(int startIndex) { + return decodeTextString(startIndex); + } + + /** + * Decode the "X-Wap-Application-Id" type for WSP pdu + * + * @param startIndex The starting position of the "X-Wap-Application-Id" in this pdu + * + * @return false when error(not a X-Wap-Application-Id) occur + * return value can be retrieved first by getValueString() and second by getValue32() + * method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeXWapApplicationId(int startIndex) { + if (decodeIntegerValue(startIndex) == true) { + stringValue = null; + return true; + } + return decodeTextString(startIndex); + } + + /** + * Seek for the "X-Wap-Application-Id" field for WSP pdu + * + * @param startIndex The starting position of seek pointer + * @param endIndex Valid seek area end point + * + * @return false when error(not a X-Wap-Application-Id) occur + * return value can be retrieved by getValue32() + */ + public boolean seekXWapApplicationId(int startIndex, int endIndex) { + int index = startIndex; + + try { + for (index = startIndex; index <= endIndex; ) { + /** + * 8.4.1.1 Field name + * Field name is integer or text. + */ + if (decodeIntegerValue(index)) { + int fieldValue = (int) getValue32(); + + if (fieldValue == PARAMETER_ID_X_WAP_APPLICATION_ID) { + unsigned32bit = index + 1; + return true; + } + } else { + if (!decodeTextString(index)) return false; + } + index += getDecodedDataLength(); + if (index > endIndex) return false; + + /** + * 8.4.1.2 Field values + * Value Interpretation of First Octet + * 0 - 30 This octet is followed by the indicated number (0 - 30) + of data octets + * 31 This octet is followed by a uintvar, which indicates the number + * of data octets after it + * 32 - 127 The value is a text string, terminated by a zero octet + (NUL character) + * 128 - 255 It is an encoded 7-bit value; this header has no more data + */ + byte val = wspData[index]; + if (0 <= val && val <= WAP_PDU_SHORT_LENGTH_MAX) { + index += wspData[index] + 1; + } else if (val == WAP_PDU_LENGTH_QUOTE) { + if (index + 1 >= endIndex) return false; + index++; + if (!decodeUintvarInteger(index)) return false; + index += getDecodedDataLength(); + } else if (WAP_PDU_LENGTH_QUOTE < val && val <= 127) { + if (!decodeTextString(index)) return false; + index += getDecodedDataLength(); + } else { + index++; + } + } + } catch (ArrayIndexOutOfBoundsException e) { + //seek application ID failed. WSP header might be corrupted + return false; + } + return false; + } + + /** + * Decode the "X-Wap-Content-URI" type for WSP pdu + * + * @param startIndex The starting position of the "X-Wap-Content-URI" in this pdu + * + * @return false when error(not a X-Wap-Content-URI) occur + * return value can be retrieved by getValueString() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeXWapContentURI(int startIndex) { + return decodeTextString(startIndex); + } + + /** + * Decode the "X-Wap-Initiator-URI" type for WSP pdu + * + * @param startIndex The starting position of the "X-Wap-Initiator-URI" in this pdu + * + * @return false when error(not a X-Wap-Initiator-URI) occur + * return value can be retrieved by getValueString() method + * length of data in pdu can be retrieved by getDecodedDataLength() method + */ + public boolean decodeXWapInitiatorURI(int startIndex) { + return decodeTextString(startIndex); + } + + /** + * The data length of latest operation. + */ + public int getDecodedDataLength() { + return dataLength; + } + + /** + * The 32-bits result of latest operation. + */ + public long getValue32() { + return unsigned32bit; + } + + /** + * The String result of latest operation. + */ + public String getValueString() { + return stringValue; + } + + /** + * Any parameters encountered as part of a decodeContentType() invocation. + * + * @return a map of content parameters keyed by their names, or null if + * decodeContentType() has not been called If any unassigned + * well-known parameters are encountered, the key of the map will be + * 'unassigned/0x...', where '...' is the hex value of the + * unassigned parameter. If a parameter has No-Value the value will be null. + * + */ + public HashMap<String, String> getContentParameters() { + return contentParameters; + } +} diff --git a/src/java/com/android/internal/telephony/cat/AppInterface.java b/src/java/com/android/internal/telephony/cat/AppInterface.java new file mode 100644 index 0000000..299e140 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/AppInterface.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +/** + * Interface for communication between STK App and CAT Telephony + * + * {@hide} + */ +public interface AppInterface { + + /* + * Intent's actions which are broadcasted by the Telephony once a new CAT + * proactive command, session end arrive. + */ + public static final String CAT_CMD_ACTION = + "android.intent.action.stk.command"; + public static final String CAT_SESSION_END_ACTION = + "android.intent.action.stk.session_end"; + + /* + * Callback function from app to telephony to pass a result code and user's + * input back to the ICC. + */ + void onCmdResponse(CatResponseMessage resMsg); + + /* + * Enumeration for representing "Type of Command" of proactive commands. + * Those are the only commands which are supported by the Telephony. Any app + * implementation should support those. + * Refer to ETSI TS 102.223 section 9.4 + */ + public static enum CommandType { + DISPLAY_TEXT(0x21), + GET_INKEY(0x22), + GET_INPUT(0x23), + LAUNCH_BROWSER(0x15), + PLAY_TONE(0x20), + REFRESH(0x01), + SELECT_ITEM(0x24), + SEND_SS(0x11), + SEND_USSD(0x12), + SEND_SMS(0x13), + SEND_DTMF(0x14), + SET_UP_EVENT_LIST(0x05), + SET_UP_IDLE_MODE_TEXT(0x28), + SET_UP_MENU(0x25), + SET_UP_CALL(0x10), + PROVIDE_LOCAL_INFORMATION(0x26), + OPEN_CHANNEL(0x40), + CLOSE_CHANNEL(0x41), + RECEIVE_DATA(0x42), + SEND_DATA(0x43); + + private int mValue; + + CommandType(int value) { + mValue = value; + } + + public int value() { + return mValue; + } + + /** + * Create a CommandType object. + * + * @param value Integer value to be converted to a CommandType object. + * @return CommandType object whose "Type of Command" value is {@code + * value}. If no CommandType object has that value, null is + * returned. + */ + public static CommandType fromInt(int value) { + for (CommandType e : CommandType.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } + } +} diff --git a/src/java/com/android/internal/telephony/cat/BerTlv.java b/src/java/com/android/internal/telephony/cat/BerTlv.java new file mode 100644 index 0000000..095e65b --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/BerTlv.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import java.util.List; + +/** + * Class for representing BER-TLV objects. + * + * @see "ETSI TS 102 223 Annex C" for more information. + * + * {@hide} + */ +class BerTlv { + private int mTag = BER_UNKNOWN_TAG; + private List<ComprehensionTlv> mCompTlvs = null; + + public static final int BER_UNKNOWN_TAG = 0x00; + public static final int BER_PROACTIVE_COMMAND_TAG = 0xd0; + public static final int BER_MENU_SELECTION_TAG = 0xd3; + public static final int BER_EVENT_DOWNLOAD_TAG = 0xd6; + + private BerTlv(int tag, List<ComprehensionTlv> ctlvs) { + mTag = tag; + mCompTlvs = ctlvs; + } + + /** + * Gets a list of ComprehensionTlv objects contained in this BER-TLV object. + * + * @return A list of COMPREHENSION-TLV object + */ + public List<ComprehensionTlv> getComprehensionTlvs() { + return mCompTlvs; + } + + /** + * Gets a tag id of the BER-TLV object. + * + * @return A tag integer. + */ + public int getTag() { + return mTag; + } + + /** + * Decodes a BER-TLV object from a byte array. + * + * @param data A byte array to decode from + * @return A BER-TLV object decoded + * @throws ResultException + */ + public static BerTlv decode(byte[] data) throws ResultException { + int curIndex = 0; + int endIndex = data.length; + int tag, length = 0; + + try { + /* tag */ + tag = data[curIndex++] & 0xff; + if (tag == BER_PROACTIVE_COMMAND_TAG) { + /* length */ + int temp = data[curIndex++] & 0xff; + if (temp < 0x80) { + length = temp; + } else if (temp == 0x81) { + temp = data[curIndex++] & 0xff; + if (temp < 0x80) { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "length < 0x80 length=" + Integer.toHexString(length) + + " curIndex=" + curIndex + " endIndex=" + endIndex); + + } + length = temp; + } else { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "Expected first byte to be length or a length tag and < 0x81" + + " byte= " + Integer.toHexString(temp) + " curIndex=" + curIndex + + " endIndex=" + endIndex); + } + } else { + if (ComprehensionTlvTag.COMMAND_DETAILS.value() == (tag & ~0x80)) { + tag = BER_UNKNOWN_TAG; + curIndex = 0; + } + } + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING, + "IndexOutOfBoundsException " + + " curIndex=" + curIndex + " endIndex=" + endIndex); + } catch (ResultException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD, e.explanation()); + } + + /* COMPREHENSION-TLVs */ + if (endIndex - curIndex < length) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "Command had extra data endIndex=" + endIndex + " curIndex=" + curIndex + + " length=" + length); + } + + List<ComprehensionTlv> ctlvs = ComprehensionTlv.decodeMany(data, + curIndex); + + return new BerTlv(tag, ctlvs); + } +} diff --git a/src/java/com/android/internal/telephony/cat/CatCmdMessage.java b/src/java/com/android/internal/telephony/cat/CatCmdMessage.java new file mode 100644 index 0000000..48c2e2b --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CatCmdMessage.java @@ -0,0 +1,182 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Class used to pass CAT messages from telephony to application. Application + * should call getXXX() to get commands's specific values. + * + */ +public class CatCmdMessage implements Parcelable { + // members + CommandDetails mCmdDet; + private TextMessage mTextMsg; + private Menu mMenu; + private Input mInput; + private BrowserSettings mBrowserSettings = null; + private ToneSettings mToneSettings = null; + private CallSettings mCallSettings = null; + + /* + * Container for Launch Browser command settings. + */ + public class BrowserSettings { + public String url; + public LaunchBrowserMode mode; + } + + /* + * Container for Call Setup command settings. + */ + public class CallSettings { + public TextMessage confirmMsg; + public TextMessage callMsg; + } + + CatCmdMessage(CommandParams cmdParams) { + mCmdDet = cmdParams.cmdDet; + switch(getCmdType()) { + case SET_UP_MENU: + case SELECT_ITEM: + mMenu = ((SelectItemParams) cmdParams).menu; + break; + case DISPLAY_TEXT: + case SET_UP_IDLE_MODE_TEXT: + case SEND_DTMF: + case SEND_SMS: + case SEND_SS: + case SEND_USSD: + mTextMsg = ((DisplayTextParams) cmdParams).textMsg; + break; + case GET_INPUT: + case GET_INKEY: + mInput = ((GetInputParams) cmdParams).input; + break; + case LAUNCH_BROWSER: + mTextMsg = ((LaunchBrowserParams) cmdParams).confirmMsg; + mBrowserSettings = new BrowserSettings(); + mBrowserSettings.url = ((LaunchBrowserParams) cmdParams).url; + mBrowserSettings.mode = ((LaunchBrowserParams) cmdParams).mode; + break; + case PLAY_TONE: + PlayToneParams params = (PlayToneParams) cmdParams; + mToneSettings = params.settings; + mTextMsg = params.textMsg; + break; + case SET_UP_CALL: + mCallSettings = new CallSettings(); + mCallSettings.confirmMsg = ((CallSetupParams) cmdParams).confirmMsg; + mCallSettings.callMsg = ((CallSetupParams) cmdParams).callMsg; + break; + case OPEN_CHANNEL: + case CLOSE_CHANNEL: + case RECEIVE_DATA: + case SEND_DATA: + BIPClientParams param = (BIPClientParams) cmdParams; + mTextMsg = param.textMsg; + break; + } + } + + public CatCmdMessage(Parcel in) { + mCmdDet = in.readParcelable(null); + mTextMsg = in.readParcelable(null); + mMenu = in.readParcelable(null); + mInput = in.readParcelable(null); + switch (getCmdType()) { + case LAUNCH_BROWSER: + mBrowserSettings = new BrowserSettings(); + mBrowserSettings.url = in.readString(); + mBrowserSettings.mode = LaunchBrowserMode.values()[in.readInt()]; + break; + case PLAY_TONE: + mToneSettings = in.readParcelable(null); + break; + case SET_UP_CALL: + mCallSettings = new CallSettings(); + mCallSettings.confirmMsg = in.readParcelable(null); + mCallSettings.callMsg = in.readParcelable(null); + break; + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mCmdDet, 0); + dest.writeParcelable(mTextMsg, 0); + dest.writeParcelable(mMenu, 0); + dest.writeParcelable(mInput, 0); + switch(getCmdType()) { + case LAUNCH_BROWSER: + dest.writeString(mBrowserSettings.url); + dest.writeInt(mBrowserSettings.mode.ordinal()); + break; + case PLAY_TONE: + dest.writeParcelable(mToneSettings, 0); + break; + case SET_UP_CALL: + dest.writeParcelable(mCallSettings.confirmMsg, 0); + dest.writeParcelable(mCallSettings.callMsg, 0); + break; + } + } + + public static final Parcelable.Creator<CatCmdMessage> CREATOR = new Parcelable.Creator<CatCmdMessage>() { + public CatCmdMessage createFromParcel(Parcel in) { + return new CatCmdMessage(in); + } + + public CatCmdMessage[] newArray(int size) { + return new CatCmdMessage[size]; + } + }; + + public int describeContents() { + return 0; + } + + /* external API to be used by application */ + public AppInterface.CommandType getCmdType() { + return AppInterface.CommandType.fromInt(mCmdDet.typeOfCommand); + } + + public Menu getMenu() { + return mMenu; + } + + public Input geInput() { + return mInput; + } + + public TextMessage geTextMessage() { + return mTextMsg; + } + + public BrowserSettings getBrowserSettings() { + return mBrowserSettings; + } + + public ToneSettings getToneSettings() { + return mToneSettings; + } + + public CallSettings getCallSettings() { + return mCallSettings; + } +} diff --git a/src/java/com/android/internal/telephony/cat/CatException.java b/src/java/com/android/internal/telephony/cat/CatException.java new file mode 100644 index 0000000..1bf1369 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CatException.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import android.util.AndroidException; + + +/** + * Base class for all the exceptions in CAT service. + * + * {@hide} + */ +class CatException extends AndroidException { + public CatException() { + super(); + } +} diff --git a/src/java/com/android/internal/telephony/cat/CatLog.java b/src/java/com/android/internal/telephony/cat/CatLog.java new file mode 100644 index 0000000..e19ff43 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CatLog.java @@ -0,0 +1,41 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.util.Log; + +public abstract class CatLog { + static final boolean DEBUG = true; + + public static void d(Object caller, String msg) { + if (!DEBUG) { + return; + } + + String className = caller.getClass().getName(); + Log.d("CAT", className.substring(className.lastIndexOf('.') + 1) + ": " + + msg); + } + + public static void d(String caller, String msg) { + if (!DEBUG) { + return; + } + + Log.d("CAT", caller + ": " + msg); + } +} diff --git a/src/java/com/android/internal/telephony/cat/CatResponseMessage.java b/src/java/com/android/internal/telephony/cat/CatResponseMessage.java new file mode 100644 index 0000000..cfcac36 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CatResponseMessage.java @@ -0,0 +1,54 @@ +/* + * 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 com.android.internal.telephony.cat; + +public class CatResponseMessage { + CommandDetails cmdDet = null; + ResultCode resCode = ResultCode.OK; + int usersMenuSelection = 0; + String usersInput = null; + boolean usersYesNoSelection = false; + boolean usersConfirm = false; + + public CatResponseMessage(CatCmdMessage cmdMsg) { + this.cmdDet = cmdMsg.mCmdDet; + } + + public void setResultCode(ResultCode resCode) { + this.resCode = resCode; + } + + public void setMenuSelection(int selection) { + this.usersMenuSelection = selection; + } + + public void setInput(String input) { + this.usersInput = input; + } + + public void setYesNo(boolean yesNo) { + usersYesNoSelection = yesNo; + } + + public void setConfirmation(boolean confirm) { + usersConfirm = confirm; + } + + CommandDetails getCmdDetails() { + return cmdDet; + } + }
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/cat/CatService.java b/src/java/com/android/internal/telephony/cat/CatService.java new file mode 100644 index 0000000..2b37072 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CatService.java @@ -0,0 +1,750 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.os.SystemProperties; + +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccRecords; + + +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.Locale; + +class RilMessage { + int mId; + Object mData; + ResultCode mResCode; + + RilMessage(int msgId, String rawData) { + mId = msgId; + mData = rawData; + } + + RilMessage(RilMessage other) { + this.mId = other.mId; + this.mData = other.mData; + this.mResCode = other.mResCode; + } +} + +/** + * Class that implements SIM Toolkit Telephony Service. Interacts with the RIL + * and application. + * + * {@hide} + */ +public class CatService extends Handler implements AppInterface { + + // Class members + private static IccRecords mIccRecords; + + // Service members. + // Protects singleton instance lazy initialization. + private static final Object sInstanceLock = new Object(); + private static CatService sInstance; + private CommandsInterface mCmdIf; + private Context mContext; + private CatCmdMessage mCurrntCmd = null; + private CatCmdMessage mMenuCmd = null; + + private RilMessageDecoder mMsgDecoder = null; + private boolean mStkAppInstalled = false; + + // Service constants. + static final int MSG_ID_SESSION_END = 1; + static final int MSG_ID_PROACTIVE_COMMAND = 2; + static final int MSG_ID_EVENT_NOTIFY = 3; + static final int MSG_ID_CALL_SETUP = 4; + static final int MSG_ID_REFRESH = 5; + static final int MSG_ID_RESPONSE = 6; + static final int MSG_ID_SIM_READY = 7; + + static final int MSG_ID_RIL_MSG_DECODED = 10; + + // Events to signal SIM presence or absent in the device. + private static final int MSG_ID_ICC_RECORDS_LOADED = 20; + + private static final int DEV_ID_KEYPAD = 0x01; + private static final int DEV_ID_DISPLAY = 0x02; + private static final int DEV_ID_EARPIECE = 0x03; + private static final int DEV_ID_UICC = 0x81; + private static final int DEV_ID_TERMINAL = 0x82; + private static final int DEV_ID_NETWORK = 0x83; + + static final String STK_DEFAULT = "Defualt Message"; + + /* Intentionally private for singleton */ + private CatService(CommandsInterface ci, IccRecords ir, Context context, + IccFileHandler fh, IccCard ic) { + if (ci == null || ir == null || context == null || fh == null + || ic == null) { + throw new NullPointerException( + "Service: Input parameters must not be null"); + } + mCmdIf = ci; + mContext = context; + + // Get the RilMessagesDecoder for decoding the messages. + mMsgDecoder = RilMessageDecoder.getInstance(this, fh); + + // Register ril events handling. + mCmdIf.setOnCatSessionEnd(this, MSG_ID_SESSION_END, null); + mCmdIf.setOnCatProactiveCmd(this, MSG_ID_PROACTIVE_COMMAND, null); + mCmdIf.setOnCatEvent(this, MSG_ID_EVENT_NOTIFY, null); + mCmdIf.setOnCatCallSetUp(this, MSG_ID_CALL_SETUP, null); + //mCmdIf.setOnSimRefresh(this, MSG_ID_REFRESH, null); + + mIccRecords = ir; + + // Register for SIM ready event. + ic.registerForReady(this, MSG_ID_SIM_READY, null); + mIccRecords.registerForRecordsLoaded(this, MSG_ID_ICC_RECORDS_LOADED, null); + + // Check if STK application is availalbe + mStkAppInstalled = isStkAppInstalled(); + + CatLog.d(this, "Running CAT service. STK app installed:" + mStkAppInstalled); + } + + public void dispose() { + mIccRecords.unregisterForRecordsLoaded(this); + mCmdIf.unSetOnCatSessionEnd(this); + mCmdIf.unSetOnCatProactiveCmd(this); + mCmdIf.unSetOnCatEvent(this); + mCmdIf.unSetOnCatCallSetUp(this); + + this.removeCallbacksAndMessages(null); + } + + protected void finalize() { + CatLog.d(this, "Service finalized"); + } + + private void handleRilMsg(RilMessage rilMsg) { + if (rilMsg == null) { + return; + } + + // dispatch messages + CommandParams cmdParams = null; + switch (rilMsg.mId) { + case MSG_ID_EVENT_NOTIFY: + if (rilMsg.mResCode == ResultCode.OK) { + cmdParams = (CommandParams) rilMsg.mData; + if (cmdParams != null) { + handleCommand(cmdParams, false); + } + } + break; + case MSG_ID_PROACTIVE_COMMAND: + try { + cmdParams = (CommandParams) rilMsg.mData; + } catch (ClassCastException e) { + // for error handling : cast exception + CatLog.d(this, "Fail to parse proactive command"); + sendTerminalResponse(mCurrntCmd.mCmdDet, ResultCode.CMD_DATA_NOT_UNDERSTOOD, + false, 0x00, null); + break; + } + if (cmdParams != null) { + if (rilMsg.mResCode == ResultCode.OK) { + handleCommand(cmdParams, true); + } else { + // for proactive commands that couldn't be decoded + // successfully respond with the code generated by the + // message decoder. + sendTerminalResponse(cmdParams.cmdDet, rilMsg.mResCode, + false, 0, null); + } + } + break; + case MSG_ID_REFRESH: + cmdParams = (CommandParams) rilMsg.mData; + if (cmdParams != null) { + handleCommand(cmdParams, false); + } + break; + case MSG_ID_SESSION_END: + handleSessionEnd(); + break; + case MSG_ID_CALL_SETUP: + // prior event notify command supplied all the information + // needed for set up call processing. + break; + } + } + + /** + * Handles RIL_UNSOL_STK_EVENT_NOTIFY or RIL_UNSOL_STK_PROACTIVE_COMMAND command + * from RIL. + * Sends valid proactive command data to the application using intents. + * RIL_REQUEST_STK_SEND_TERMINAL_RESPONSE will be send back if the command is + * from RIL_UNSOL_STK_PROACTIVE_COMMAND. + */ + private void handleCommand(CommandParams cmdParams, boolean isProactiveCmd) { + CatLog.d(this, cmdParams.getCommandType().name()); + + CharSequence message; + CatCmdMessage cmdMsg = new CatCmdMessage(cmdParams); + switch (cmdParams.getCommandType()) { + case SET_UP_MENU: + if (removeMenu(cmdMsg.getMenu())) { + mMenuCmd = null; + } else { + mMenuCmd = cmdMsg; + } + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + break; + case DISPLAY_TEXT: + // when application is not required to respond, send an immediate response. + if (!cmdMsg.geTextMessage().responseNeeded) { + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + } + break; + case REFRESH: + // ME side only handles refresh commands which meant to remove IDLE + // MODE TEXT. + cmdParams.cmdDet.typeOfCommand = CommandType.SET_UP_IDLE_MODE_TEXT.value(); + break; + case SET_UP_IDLE_MODE_TEXT: + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + break; + case PROVIDE_LOCAL_INFORMATION: + ResponseData resp; + switch (cmdParams.cmdDet.commandQualifier) { + case CommandParamsFactory.DTTZ_SETTING: + resp = new DTTZResponseData(null); + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, resp); + break; + case CommandParamsFactory.LANGUAGE_SETTING: + resp = new LanguageResponseData(Locale.getDefault().getLanguage()); + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, resp); + break; + default: + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + } + // No need to start STK app here. + return; + case LAUNCH_BROWSER: + if ((((LaunchBrowserParams) cmdParams).confirmMsg.text != null) + && (((LaunchBrowserParams) cmdParams).confirmMsg.text.equals(STK_DEFAULT))) { + message = mContext.getText(com.android.internal.R.string.launchBrowserDefault); + ((LaunchBrowserParams) cmdParams).confirmMsg.text = message.toString(); + } + break; + case SELECT_ITEM: + case GET_INPUT: + case GET_INKEY: + break; + case SEND_DTMF: + case SEND_SMS: + case SEND_SS: + case SEND_USSD: + if ((((DisplayTextParams)cmdParams).textMsg.text != null) + && (((DisplayTextParams)cmdParams).textMsg.text.equals(STK_DEFAULT))) { + message = mContext.getText(com.android.internal.R.string.sending); + ((DisplayTextParams)cmdParams).textMsg.text = message.toString(); + } + break; + case PLAY_TONE: + break; + case SET_UP_CALL: + if ((((CallSetupParams) cmdParams).confirmMsg.text != null) + && (((CallSetupParams) cmdParams).confirmMsg.text.equals(STK_DEFAULT))) { + message = mContext.getText(com.android.internal.R.string.SetupCallDefault); + ((CallSetupParams) cmdParams).confirmMsg.text = message.toString(); + } + break; + case OPEN_CHANNEL: + case CLOSE_CHANNEL: + case RECEIVE_DATA: + case SEND_DATA: + BIPClientParams cmd = (BIPClientParams) cmdParams; + if (cmd.bHasAlphaId && (cmd.textMsg.text == null)) { + CatLog.d(this, "cmd " + cmdParams.getCommandType() + " with null alpha id"); + // If alpha length is zero, we just respond with OK. + if (isProactiveCmd) { + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + } + return; + } + // Respond with permanent failure to avoid retry if STK app is not present. + if (!mStkAppInstalled) { + CatLog.d(this, "No STK application found."); + if (isProactiveCmd) { + sendTerminalResponse(cmdParams.cmdDet, + ResultCode.BEYOND_TERMINAL_CAPABILITY, + false, 0, null); + return; + } + } + /* + * CLOSE_CHANNEL, RECEIVE_DATA and SEND_DATA can be delivered by + * either PROACTIVE_COMMAND or EVENT_NOTIFY. + * If PROACTIVE_COMMAND is used for those commands, send terminal + * response here. + */ + if (isProactiveCmd && + ((cmdParams.getCommandType() == CommandType.CLOSE_CHANNEL) || + (cmdParams.getCommandType() == CommandType.RECEIVE_DATA) || + (cmdParams.getCommandType() == CommandType.SEND_DATA))) { + sendTerminalResponse(cmdParams.cmdDet, ResultCode.OK, false, 0, null); + } + break; + default: + CatLog.d(this, "Unsupported command"); + return; + } + mCurrntCmd = cmdMsg; + Intent intent = new Intent(AppInterface.CAT_CMD_ACTION); + intent.putExtra("STK CMD", cmdMsg); + mContext.sendBroadcast(intent); + } + + /** + * Handles RIL_UNSOL_STK_SESSION_END unsolicited command from RIL. + * + */ + private void handleSessionEnd() { + CatLog.d(this, "SESSION END"); + + mCurrntCmd = mMenuCmd; + Intent intent = new Intent(AppInterface.CAT_SESSION_END_ACTION); + mContext.sendBroadcast(intent); + } + + private void sendTerminalResponse(CommandDetails cmdDet, + ResultCode resultCode, boolean includeAdditionalInfo, + int additionalInfo, ResponseData resp) { + + if (cmdDet == null) { + return; + } + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + Input cmdInput = null; + if (mCurrntCmd != null) { + cmdInput = mCurrntCmd.geInput(); + } + + // command details + int tag = ComprehensionTlvTag.COMMAND_DETAILS.value(); + if (cmdDet.compRequired) { + tag |= 0x80; + } + buf.write(tag); + buf.write(0x03); // length + buf.write(cmdDet.commandNumber); + buf.write(cmdDet.typeOfCommand); + buf.write(cmdDet.commandQualifier); + + // device identities + // According to TS102.223/TS31.111 section 6.8 Structure of + // TERMINAL RESPONSE, "For all SIMPLE-TLV objects with Min=N, + // the ME should set the CR(comprehension required) flag to + // comprehension not required.(CR=0)" + // Since DEVICE_IDENTITIES and DURATION TLVs have Min=N, + // the CR flag is not set. + tag = ComprehensionTlvTag.DEVICE_IDENTITIES.value(); + buf.write(tag); + buf.write(0x02); // length + buf.write(DEV_ID_TERMINAL); // source device id + buf.write(DEV_ID_UICC); // destination device id + + // result + tag = 0x80 | ComprehensionTlvTag.RESULT.value(); + buf.write(tag); + int length = includeAdditionalInfo ? 2 : 1; + buf.write(length); + buf.write(resultCode.value()); + + // additional info + if (includeAdditionalInfo) { + buf.write(additionalInfo); + } + + // Fill optional data for each corresponding command + if (resp != null) { + resp.format(buf); + } else { + encodeOptionalTags(cmdDet, resultCode, cmdInput, buf); + } + + byte[] rawData = buf.toByteArray(); + String hexString = IccUtils.bytesToHexString(rawData); + if (false) { + CatLog.d(this, "TERMINAL RESPONSE: " + hexString); + } + + mCmdIf.sendTerminalResponse(hexString, null); + } + + private void encodeOptionalTags(CommandDetails cmdDet, + ResultCode resultCode, Input cmdInput, ByteArrayOutputStream buf) { + CommandType cmdType = AppInterface.CommandType.fromInt(cmdDet.typeOfCommand); + if (cmdType != null) { + switch (cmdType) { + case GET_INKEY: + // ETSI TS 102 384,27.22.4.2.8.4.2. + // If it is a response for GET_INKEY command and the response timeout + // occured, then add DURATION TLV for variable timeout case. + if ((resultCode.value() == ResultCode.NO_RESPONSE_FROM_USER.value()) && + (cmdInput != null) && (cmdInput.duration != null)) { + getInKeyResponse(buf, cmdInput); + } + break; + case PROVIDE_LOCAL_INFORMATION: + if ((cmdDet.commandQualifier == CommandParamsFactory.LANGUAGE_SETTING) && + (resultCode.value() == ResultCode.OK.value())) { + getPliResponse(buf); + } + break; + default: + CatLog.d(this, "encodeOptionalTags() Unsupported Cmd details=" + cmdDet); + break; + } + } else { + CatLog.d(this, "encodeOptionalTags() bad Cmd details=" + cmdDet); + } + } + + private void getInKeyResponse(ByteArrayOutputStream buf, Input cmdInput) { + int tag = ComprehensionTlvTag.DURATION.value(); + + buf.write(tag); + buf.write(0x02); // length + buf.write(cmdInput.duration.timeUnit.SECOND.value()); // Time (Unit,Seconds) + buf.write(cmdInput.duration.timeInterval); // Time Duration + } + + private void getPliResponse(ByteArrayOutputStream buf) { + + // Locale Language Setting + String lang = SystemProperties.get("persist.sys.language"); + + if (lang != null) { + // tag + int tag = ComprehensionTlvTag.LANGUAGE.value(); + buf.write(tag); + ResponseData.writeLength(buf, lang.length()); + buf.write(lang.getBytes(), 0, lang.length()); + } + } + + private void sendMenuSelection(int menuId, boolean helpRequired) { + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + // tag + int tag = BerTlv.BER_MENU_SELECTION_TAG; + buf.write(tag); + + // length + buf.write(0x00); // place holder + + // device identities + tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value(); + buf.write(tag); + buf.write(0x02); // length + buf.write(DEV_ID_KEYPAD); // source device id + buf.write(DEV_ID_UICC); // destination device id + + // item identifier + tag = 0x80 | ComprehensionTlvTag.ITEM_ID.value(); + buf.write(tag); + buf.write(0x01); // length + buf.write(menuId); // menu identifier chosen + + // help request + if (helpRequired) { + tag = ComprehensionTlvTag.HELP_REQUEST.value(); + buf.write(tag); + buf.write(0x00); // length + } + + byte[] rawData = buf.toByteArray(); + + // write real length + int len = rawData.length - 2; // minus (tag + length) + rawData[1] = (byte) len; + + String hexString = IccUtils.bytesToHexString(rawData); + + mCmdIf.sendEnvelope(hexString, null); + } + + private void eventDownload(int event, int sourceId, int destinationId, + byte[] additionalInfo, boolean oneShot) { + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + + // tag + int tag = BerTlv.BER_EVENT_DOWNLOAD_TAG; + buf.write(tag); + + // length + buf.write(0x00); // place holder, assume length < 128. + + // event list + tag = 0x80 | ComprehensionTlvTag.EVENT_LIST.value(); + buf.write(tag); + buf.write(0x01); // length + buf.write(event); // event value + + // device identities + tag = 0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value(); + buf.write(tag); + buf.write(0x02); // length + buf.write(sourceId); // source device id + buf.write(destinationId); // destination device id + + // additional information + if (additionalInfo != null) { + for (byte b : additionalInfo) { + buf.write(b); + } + } + + byte[] rawData = buf.toByteArray(); + + // write real length + int len = rawData.length - 2; // minus (tag + length) + rawData[1] = (byte) len; + + String hexString = IccUtils.bytesToHexString(rawData); + + mCmdIf.sendEnvelope(hexString, null); + } + + /** + * Used for instantiating/updating the Service from the GsmPhone or CdmaPhone constructor. + * + * @param ci CommandsInterface object + * @param ir IccRecords object + * @param context phone app context + * @param fh Icc file handler + * @param ic Icc card + * @return The only Service object in the system + */ + public static CatService getInstance(CommandsInterface ci, IccRecords ir, + Context context, IccFileHandler fh, IccCard ic) { + synchronized (sInstanceLock) { + if (sInstance == null) { + if (ci == null || ir == null || context == null || fh == null + || ic == null) { + return null; + } + HandlerThread thread = new HandlerThread("Cat Telephony service"); + thread.start(); + sInstance = new CatService(ci, ir, context, fh, ic); + CatLog.d(sInstance, "NEW sInstance"); + } else if ((ir != null) && (mIccRecords != ir)) { + CatLog.d(sInstance, "Reinitialize the Service with SIMRecords"); + mIccRecords = ir; + + // re-Register for SIM ready event. + mIccRecords.registerForRecordsLoaded(sInstance, MSG_ID_ICC_RECORDS_LOADED, null); + CatLog.d(sInstance, "sr changed reinitialize and return current sInstance"); + } else { + CatLog.d(sInstance, "Return current sInstance"); + } + return sInstance; + } + } + + /** + * Used by application to get an AppInterface object. + * + * @return The only Service object in the system + */ + public static AppInterface getInstance() { + return getInstance(null, null, null, null, null); + } + + @Override + public void handleMessage(Message msg) { + + switch (msg.what) { + case MSG_ID_SESSION_END: + case MSG_ID_PROACTIVE_COMMAND: + case MSG_ID_EVENT_NOTIFY: + case MSG_ID_REFRESH: + CatLog.d(this, "ril message arrived"); + String data = null; + if (msg.obj != null) { + AsyncResult ar = (AsyncResult) msg.obj; + if (ar != null && ar.result != null) { + try { + data = (String) ar.result; + } catch (ClassCastException e) { + break; + } + } + } + mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, data)); + break; + case MSG_ID_CALL_SETUP: + mMsgDecoder.sendStartDecodingMessageParams(new RilMessage(msg.what, null)); + break; + case MSG_ID_ICC_RECORDS_LOADED: + break; + case MSG_ID_RIL_MSG_DECODED: + handleRilMsg((RilMessage) msg.obj); + break; + case MSG_ID_RESPONSE: + handleCmdResponse((CatResponseMessage) msg.obj); + break; + case MSG_ID_SIM_READY: + CatLog.d(this, "SIM ready. Reporting STK service running now..."); + mCmdIf.reportStkServiceIsRunning(null); + break; + default: + throw new AssertionError("Unrecognized CAT command: " + msg.what); + } + } + + public synchronized void onCmdResponse(CatResponseMessage resMsg) { + if (resMsg == null) { + return; + } + // queue a response message. + Message msg = this.obtainMessage(MSG_ID_RESPONSE, resMsg); + msg.sendToTarget(); + } + + private boolean validateResponse(CatResponseMessage resMsg) { + if (mCurrntCmd != null) { + return (resMsg.cmdDet.compareTo(mCurrntCmd.mCmdDet)); + } + return false; + } + + private boolean removeMenu(Menu menu) { + try { + if (menu.items.size() == 1 && menu.items.get(0) == null) { + return true; + } + } catch (NullPointerException e) { + CatLog.d(this, "Unable to get Menu's items size"); + return true; + } + return false; + } + + private void handleCmdResponse(CatResponseMessage resMsg) { + // Make sure the response details match the last valid command. An invalid + // response is a one that doesn't have a corresponding proactive command + // and sending it can "confuse" the baseband/ril. + // One reason for out of order responses can be UI glitches. For example, + // if the application launch an activity, and that activity is stored + // by the framework inside the history stack. That activity will be + // available for relaunch using the latest application dialog + // (long press on the home button). Relaunching that activity can send + // the same command's result again to the CatService and can cause it to + // get out of sync with the SIM. + if (!validateResponse(resMsg)) { + return; + } + ResponseData resp = null; + boolean helpRequired = false; + CommandDetails cmdDet = resMsg.getCmdDetails(); + + switch (resMsg.resCode) { + case HELP_INFO_REQUIRED: + helpRequired = true; + // fall through + case OK: + case PRFRMD_WITH_PARTIAL_COMPREHENSION: + case PRFRMD_WITH_MISSING_INFO: + case PRFRMD_WITH_ADDITIONAL_EFS_READ: + case PRFRMD_ICON_NOT_DISPLAYED: + case PRFRMD_MODIFIED_BY_NAA: + case PRFRMD_LIMITED_SERVICE: + case PRFRMD_WITH_MODIFICATION: + case PRFRMD_NAA_NOT_ACTIVE: + case PRFRMD_TONE_NOT_PLAYED: + switch (AppInterface.CommandType.fromInt(cmdDet.typeOfCommand)) { + case SET_UP_MENU: + helpRequired = resMsg.resCode == ResultCode.HELP_INFO_REQUIRED; + sendMenuSelection(resMsg.usersMenuSelection, helpRequired); + return; + case SELECT_ITEM: + resp = new SelectItemResponseData(resMsg.usersMenuSelection); + break; + case GET_INPUT: + case GET_INKEY: + Input input = mCurrntCmd.geInput(); + if (!input.yesNo) { + // when help is requested there is no need to send the text + // string object. + if (!helpRequired) { + resp = new GetInkeyInputResponseData(resMsg.usersInput, + input.ucs2, input.packed); + } + } else { + resp = new GetInkeyInputResponseData( + resMsg.usersYesNoSelection); + } + break; + case DISPLAY_TEXT: + case LAUNCH_BROWSER: + break; + case SET_UP_CALL: + mCmdIf.handleCallSetupRequestFromSim(resMsg.usersConfirm, null); + // No need to send terminal response for SET UP CALL. The user's + // confirmation result is send back using a dedicated ril message + // invoked by the CommandInterface call above. + mCurrntCmd = null; + return; + } + break; + case NO_RESPONSE_FROM_USER: + case UICC_SESSION_TERM_BY_USER: + case BACKWARD_MOVE_BY_USER: + case USER_NOT_ACCEPT: + resp = null; + break; + default: + return; + } + sendTerminalResponse(cmdDet, resMsg.resCode, false, 0, resp); + mCurrntCmd = null; + } + + private boolean isStkAppInstalled() { + Intent intent = new Intent(AppInterface.CAT_CMD_ACTION); + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> broadcastReceivers = + pm.queryBroadcastReceivers(intent, PackageManager.GET_META_DATA); + int numReceiver = broadcastReceivers == null ? 0 : broadcastReceivers.size(); + + return (numReceiver > 0); + } +} diff --git a/src/java/com/android/internal/telephony/cat/CommandDetails.java b/src/java/com/android/internal/telephony/cat/CommandDetails.java new file mode 100644 index 0000000..3e7f722 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CommandDetails.java @@ -0,0 +1,115 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.os.Parcel; +import android.os.Parcelable; + +abstract class ValueObject { + abstract ComprehensionTlvTag getTag(); +} + +/** + * Class for Command Detailes object of proactive commands from SIM. + * {@hide} + */ +class CommandDetails extends ValueObject implements Parcelable { + public boolean compRequired; + public int commandNumber; + public int typeOfCommand; + public int commandQualifier; + + public ComprehensionTlvTag getTag() { + return ComprehensionTlvTag.COMMAND_DETAILS; + } + + CommandDetails() { + } + + public boolean compareTo(CommandDetails other) { + return (this.compRequired == other.compRequired && + this.commandNumber == other.commandNumber && + this.commandQualifier == other.commandQualifier && + this.typeOfCommand == other.typeOfCommand); + } + + public CommandDetails(Parcel in) { + compRequired = in.readInt() != 0; + commandNumber = in.readInt(); + typeOfCommand = in.readInt(); + commandQualifier = in.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(compRequired ? 1 : 0); + dest.writeInt(commandNumber); + dest.writeInt(typeOfCommand); + dest.writeInt(commandQualifier); + } + + public static final Parcelable.Creator<CommandDetails> CREATOR = + new Parcelable.Creator<CommandDetails>() { + public CommandDetails createFromParcel(Parcel in) { + return new CommandDetails(in); + } + + public CommandDetails[] newArray(int size) { + return new CommandDetails[size]; + } + }; + + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "CmdDetails: compRequired=" + compRequired + + " commandNumber=" + commandNumber + + " typeOfCommand=" + typeOfCommand + + " commandQualifier=" + commandQualifier; + } +} + +class DeviceIdentities extends ValueObject { + public int sourceId; + public int destinationId; + + ComprehensionTlvTag getTag() { + return ComprehensionTlvTag.DEVICE_IDENTITIES; + } +} + +// Container class to hold icon identifier value. +class IconId extends ValueObject { + int recordNumber; + boolean selfExplanatory; + + ComprehensionTlvTag getTag() { + return ComprehensionTlvTag.ICON_ID; + } +} + +// Container class to hold item icon identifier list value. +class ItemsIconId extends ValueObject { + int [] recordNumbers; + boolean selfExplanatory; + + ComprehensionTlvTag getTag() { + return ComprehensionTlvTag.ITEM_ICON_ID_LIST; + } +} diff --git a/src/java/com/android/internal/telephony/cat/CommandParams.java b/src/java/com/android/internal/telephony/cat/CommandParams.java new file mode 100644 index 0000000..79f6ad2 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CommandParams.java @@ -0,0 +1,199 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.graphics.Bitmap; + +/** + * Container class for proactive command parameters. + * + */ +class CommandParams { + CommandDetails cmdDet; + + CommandParams(CommandDetails cmdDet) { + this.cmdDet = cmdDet; + } + + AppInterface.CommandType getCommandType() { + return AppInterface.CommandType.fromInt(cmdDet.typeOfCommand); + } + + boolean setIcon(Bitmap icon) { return true; } + + @Override + public String toString() { + return cmdDet.toString(); + } +} + +class DisplayTextParams extends CommandParams { + TextMessage textMsg; + + DisplayTextParams(CommandDetails cmdDet, TextMessage textMsg) { + super(cmdDet); + this.textMsg = textMsg; + } + + boolean setIcon(Bitmap icon) { + if (icon != null && textMsg != null) { + textMsg.icon = icon; + return true; + } + return false; + } +} + +class LaunchBrowserParams extends CommandParams { + TextMessage confirmMsg; + LaunchBrowserMode mode; + String url; + + LaunchBrowserParams(CommandDetails cmdDet, TextMessage confirmMsg, + String url, LaunchBrowserMode mode) { + super(cmdDet); + this.confirmMsg = confirmMsg; + this.mode = mode; + this.url = url; + } + + boolean setIcon(Bitmap icon) { + if (icon != null && confirmMsg != null) { + confirmMsg.icon = icon; + return true; + } + return false; + } +} + +class PlayToneParams extends CommandParams { + TextMessage textMsg; + ToneSettings settings; + + PlayToneParams(CommandDetails cmdDet, TextMessage textMsg, + Tone tone, Duration duration, boolean vibrate) { + super(cmdDet); + this.textMsg = textMsg; + this.settings = new ToneSettings(duration, tone, vibrate); + } + + boolean setIcon(Bitmap icon) { + if (icon != null && textMsg != null) { + textMsg.icon = icon; + return true; + } + return false; + } +} + +class CallSetupParams extends CommandParams { + TextMessage confirmMsg; + TextMessage callMsg; + + CallSetupParams(CommandDetails cmdDet, TextMessage confirmMsg, + TextMessage callMsg) { + super(cmdDet); + this.confirmMsg = confirmMsg; + this.callMsg = callMsg; + } + + boolean setIcon(Bitmap icon) { + if (icon == null) { + return false; + } + if (confirmMsg != null && confirmMsg.icon == null) { + confirmMsg.icon = icon; + return true; + } else if (callMsg != null && callMsg.icon == null) { + callMsg.icon = icon; + return true; + } + return false; + } +} + +class SelectItemParams extends CommandParams { + Menu menu = null; + boolean loadTitleIcon = false; + + SelectItemParams(CommandDetails cmdDet, Menu menu, boolean loadTitleIcon) { + super(cmdDet); + this.menu = menu; + this.loadTitleIcon = loadTitleIcon; + } + + boolean setIcon(Bitmap icon) { + if (icon != null && menu != null) { + if (loadTitleIcon && menu.titleIcon == null) { + menu.titleIcon = icon; + } else { + for (Item item : menu.items) { + if (item.icon != null) { + continue; + } + item.icon = icon; + break; + } + } + return true; + } + return false; + } +} + +class GetInputParams extends CommandParams { + Input input = null; + + GetInputParams(CommandDetails cmdDet, Input input) { + super(cmdDet); + this.input = input; + } + + boolean setIcon(Bitmap icon) { + if (icon != null && input != null) { + input.icon = icon; + } + return true; + } +} + +/* + * BIP (Bearer Independent Protocol) is the mechanism for SIM card applications + * to access data connection through the mobile device. + * + * SIM utilizes proactive commands (OPEN CHANNEL, CLOSE CHANNEL, SEND DATA and + * RECEIVE DATA to control/read/write data for BIP. Refer to ETSI TS 102 223 for + * the details of proactive commands procedures and their structures. + */ +class BIPClientParams extends CommandParams { + TextMessage textMsg; + boolean bHasAlphaId; + + BIPClientParams(CommandDetails cmdDet, TextMessage textMsg, boolean has_alpha_id) { + super(cmdDet); + this.textMsg = textMsg; + this.bHasAlphaId = has_alpha_id; + } + + boolean setIcon(Bitmap icon) { + if (icon != null && textMsg != null) { + textMsg.icon = icon; + return true; + } + return false; + } +} diff --git a/src/java/com/android/internal/telephony/cat/CommandParamsFactory.java b/src/java/com/android/internal/telephony/cat/CommandParamsFactory.java new file mode 100644 index 0000000..a554012 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/CommandParamsFactory.java @@ -0,0 +1,943 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccFileHandler; + +import java.util.Iterator; +import java.util.List; + +/** + * Factory class, used for decoding raw byte arrays, received from baseband, + * into a CommandParams object. + * + */ +class CommandParamsFactory extends Handler { + private static CommandParamsFactory sInstance = null; + private IconLoader mIconLoader; + private CommandParams mCmdParams = null; + private int mIconLoadState = LOAD_NO_ICON; + private RilMessageDecoder mCaller = null; + + // constants + static final int MSG_ID_LOAD_ICON_DONE = 1; + + // loading icons state parameters. + static final int LOAD_NO_ICON = 0; + static final int LOAD_SINGLE_ICON = 1; + static final int LOAD_MULTI_ICONS = 2; + + // Command Qualifier values for refresh command + static final int REFRESH_NAA_INIT_AND_FULL_FILE_CHANGE = 0x00; + static final int REFRESH_NAA_INIT_AND_FILE_CHANGE = 0x02; + static final int REFRESH_NAA_INIT = 0x03; + static final int REFRESH_UICC_RESET = 0x04; + + // Command Qualifier values for PLI command + static final int DTTZ_SETTING = 0x03; + static final int LANGUAGE_SETTING = 0x04; + + static synchronized CommandParamsFactory getInstance(RilMessageDecoder caller, + IccFileHandler fh) { + if (sInstance != null) { + return sInstance; + } + if (fh != null) { + return new CommandParamsFactory(caller, fh); + } + return null; + } + + private CommandParamsFactory(RilMessageDecoder caller, IccFileHandler fh) { + mCaller = caller; + mIconLoader = IconLoader.getInstance(this, fh); + } + + private CommandDetails processCommandDetails(List<ComprehensionTlv> ctlvs) { + CommandDetails cmdDet = null; + + if (ctlvs != null) { + // Search for the Command Details object. + ComprehensionTlv ctlvCmdDet = searchForTag( + ComprehensionTlvTag.COMMAND_DETAILS, ctlvs); + if (ctlvCmdDet != null) { + try { + cmdDet = ValueParser.retrieveCommandDetails(ctlvCmdDet); + } catch (ResultException e) { + CatLog.d(this, + "processCommandDetails: Failed to procees command details e=" + e); + } + } + } + return cmdDet; + } + + void make(BerTlv berTlv) { + if (berTlv == null) { + return; + } + // reset global state parameters. + mCmdParams = null; + mIconLoadState = LOAD_NO_ICON; + // only proactive command messages are processed. + if (berTlv.getTag() != BerTlv.BER_PROACTIVE_COMMAND_TAG) { + sendCmdParams(ResultCode.CMD_TYPE_NOT_UNDERSTOOD); + return; + } + boolean cmdPending = false; + List<ComprehensionTlv> ctlvs = berTlv.getComprehensionTlvs(); + // process command dtails from the tlv list. + CommandDetails cmdDet = processCommandDetails(ctlvs); + if (cmdDet == null) { + sendCmdParams(ResultCode.CMD_TYPE_NOT_UNDERSTOOD); + return; + } + + // extract command type enumeration from the raw value stored inside + // the Command Details object. + AppInterface.CommandType cmdType = AppInterface.CommandType + .fromInt(cmdDet.typeOfCommand); + if (cmdType == null) { + // This PROACTIVE COMMAND is presently not handled. Hence set + // result code as BEYOND_TERMINAL_CAPABILITY in TR. + mCmdParams = new CommandParams(cmdDet); + sendCmdParams(ResultCode.BEYOND_TERMINAL_CAPABILITY); + return; + } + + try { + switch (cmdType) { + case SET_UP_MENU: + cmdPending = processSelectItem(cmdDet, ctlvs); + break; + case SELECT_ITEM: + cmdPending = processSelectItem(cmdDet, ctlvs); + break; + case DISPLAY_TEXT: + cmdPending = processDisplayText(cmdDet, ctlvs); + break; + case SET_UP_IDLE_MODE_TEXT: + cmdPending = processSetUpIdleModeText(cmdDet, ctlvs); + break; + case GET_INKEY: + cmdPending = processGetInkey(cmdDet, ctlvs); + break; + case GET_INPUT: + cmdPending = processGetInput(cmdDet, ctlvs); + break; + case SEND_DTMF: + case SEND_SMS: + case SEND_SS: + case SEND_USSD: + cmdPending = processEventNotify(cmdDet, ctlvs); + break; + case SET_UP_CALL: + cmdPending = processSetupCall(cmdDet, ctlvs); + break; + case REFRESH: + processRefresh(cmdDet, ctlvs); + cmdPending = false; + break; + case LAUNCH_BROWSER: + cmdPending = processLaunchBrowser(cmdDet, ctlvs); + break; + case PLAY_TONE: + cmdPending = processPlayTone(cmdDet, ctlvs); + break; + case PROVIDE_LOCAL_INFORMATION: + cmdPending = processProvideLocalInfo(cmdDet, ctlvs); + break; + case OPEN_CHANNEL: + case CLOSE_CHANNEL: + case RECEIVE_DATA: + case SEND_DATA: + cmdPending = processBIPClient(cmdDet, ctlvs); + break; + default: + // unsupported proactive commands + mCmdParams = new CommandParams(cmdDet); + sendCmdParams(ResultCode.BEYOND_TERMINAL_CAPABILITY); + return; + } + } catch (ResultException e) { + CatLog.d(this, "make: caught ResultException e=" + e); + mCmdParams = new CommandParams(cmdDet); + sendCmdParams(e.result()); + return; + } + if (!cmdPending) { + sendCmdParams(ResultCode.OK); + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_ID_LOAD_ICON_DONE: + sendCmdParams(setIcons(msg.obj)); + break; + } + } + + private ResultCode setIcons(Object data) { + Bitmap[] icons = null; + int iconIndex = 0; + + if (data == null) { + return ResultCode.PRFRMD_ICON_NOT_DISPLAYED; + } + switch(mIconLoadState) { + case LOAD_SINGLE_ICON: + mCmdParams.setIcon((Bitmap) data); + break; + case LOAD_MULTI_ICONS: + icons = (Bitmap[]) data; + // set each item icon. + for (Bitmap icon : icons) { + mCmdParams.setIcon(icon); + } + break; + } + return ResultCode.OK; + } + + private void sendCmdParams(ResultCode resCode) { + mCaller.sendMsgParamsDecoded(resCode, mCmdParams); + } + + /** + * Search for a COMPREHENSION-TLV object with the given tag from a list + * + * @param tag A tag to search for + * @param ctlvs List of ComprehensionTlv objects used to search in + * + * @return A ComprehensionTlv object that has the tag value of {@code tag}. + * If no object is found with the tag, null is returned. + */ + private ComprehensionTlv searchForTag(ComprehensionTlvTag tag, + List<ComprehensionTlv> ctlvs) { + Iterator<ComprehensionTlv> iter = ctlvs.iterator(); + return searchForNextTag(tag, iter); + } + + /** + * Search for the next COMPREHENSION-TLV object with the given tag from a + * list iterated by {@code iter}. {@code iter} points to the object next to + * the found object when this method returns. Used for searching the same + * list for similar tags, usually item id. + * + * @param tag A tag to search for + * @param iter Iterator for ComprehensionTlv objects used for search + * + * @return A ComprehensionTlv object that has the tag value of {@code tag}. + * If no object is found with the tag, null is returned. + */ + private ComprehensionTlv searchForNextTag(ComprehensionTlvTag tag, + Iterator<ComprehensionTlv> iter) { + int tagValue = tag.value(); + while (iter.hasNext()) { + ComprehensionTlv ctlv = iter.next(); + if (ctlv.getTag() == tagValue) { + return ctlv; + } + } + return null; + } + + /** + * Processes DISPLAY_TEXT proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processDisplayText(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) + throws ResultException { + + CatLog.d(this, "process DisplayText"); + + TextMessage textMsg = new TextMessage(); + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING, + ctlvs); + if (ctlv != null) { + textMsg.text = ValueParser.retrieveTextString(ctlv); + } + // If the tlv object doesn't exist or the it is a null object reply + // with command not understood. + if (textMsg.text == null) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + + ctlv = searchForTag(ComprehensionTlvTag.IMMEDIATE_RESPONSE, ctlvs); + if (ctlv != null) { + textMsg.responseNeeded = false; + } + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + textMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + // parse tone duration + ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs); + if (ctlv != null) { + textMsg.duration = ValueParser.retrieveDuration(ctlv); + } + + // Parse command qualifier parameters. + textMsg.isHighPriority = (cmdDet.commandQualifier & 0x01) != 0; + textMsg.userClear = (cmdDet.commandQualifier & 0x80) != 0; + + mCmdParams = new DisplayTextParams(cmdDet, textMsg); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes SET_UP_IDLE_MODE_TEXT proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processSetUpIdleModeText(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process SetUpIdleModeText"); + + TextMessage textMsg = new TextMessage(); + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING, + ctlvs); + if (ctlv != null) { + textMsg.text = ValueParser.retrieveTextString(ctlv); + } + // load icons only when text exist. + if (textMsg.text != null) { + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + textMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + } + + mCmdParams = new DisplayTextParams(cmdDet, textMsg); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes GET_INKEY proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processGetInkey(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process GetInkey"); + + Input input = new Input(); + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING, + ctlvs); + if (ctlv != null) { + input.text = ValueParser.retrieveTextString(ctlv); + } else { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING); + } + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + } + + // parse duration + ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs); + if (ctlv != null) { + input.duration = ValueParser.retrieveDuration(ctlv); + } + + input.minLen = 1; + input.maxLen = 1; + + input.digitOnly = (cmdDet.commandQualifier & 0x01) == 0; + input.ucs2 = (cmdDet.commandQualifier & 0x02) != 0; + input.yesNo = (cmdDet.commandQualifier & 0x04) != 0; + input.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0; + input.echo = true; + + mCmdParams = new GetInputParams(cmdDet, input); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes GET_INPUT proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processGetInput(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process GetInput"); + + Input input = new Input(); + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TEXT_STRING, + ctlvs); + if (ctlv != null) { + input.text = ValueParser.retrieveTextString(ctlv); + } else { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING); + } + + ctlv = searchForTag(ComprehensionTlvTag.RESPONSE_LENGTH, ctlvs); + if (ctlv != null) { + try { + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + input.minLen = rawValue[valueIndex] & 0xff; + input.maxLen = rawValue[valueIndex + 1] & 0xff; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } else { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING); + } + + ctlv = searchForTag(ComprehensionTlvTag.DEFAULT_TEXT, ctlvs); + if (ctlv != null) { + input.defaultText = ValueParser.retrieveTextString(ctlv); + } + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + } + + input.digitOnly = (cmdDet.commandQualifier & 0x01) == 0; + input.ucs2 = (cmdDet.commandQualifier & 0x02) != 0; + input.echo = (cmdDet.commandQualifier & 0x04) == 0; + input.packed = (cmdDet.commandQualifier & 0x08) != 0; + input.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0; + + mCmdParams = new GetInputParams(cmdDet, input); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes REFRESH proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + */ + private boolean processRefresh(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) { + + CatLog.d(this, "process Refresh"); + + // REFRESH proactive command is rerouted by the baseband and handled by + // the telephony layer. IDLE TEXT should be removed for a REFRESH command + // with "initialization" or "reset" + switch (cmdDet.commandQualifier) { + case REFRESH_NAA_INIT_AND_FULL_FILE_CHANGE: + case REFRESH_NAA_INIT_AND_FILE_CHANGE: + case REFRESH_NAA_INIT: + case REFRESH_UICC_RESET: + mCmdParams = new DisplayTextParams(cmdDet, null); + break; + } + return false; + } + + /** + * Processes SELECT_ITEM proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processSelectItem(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process SelectItem"); + + Menu menu = new Menu(); + IconId titleIconId = null; + ItemsIconId itemsIconId = null; + Iterator<ComprehensionTlv> iter = ctlvs.iterator(); + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, + ctlvs); + if (ctlv != null) { + menu.title = ValueParser.retrieveAlphaId(ctlv); + } + + while (true) { + ctlv = searchForNextTag(ComprehensionTlvTag.ITEM, iter); + if (ctlv != null) { + menu.items.add(ValueParser.retrieveItem(ctlv)); + } else { + break; + } + } + + // We must have at least one menu item. + if (menu.items.size() == 0) { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING); + } + + ctlv = searchForTag(ComprehensionTlvTag.ITEM_ID, ctlvs); + if (ctlv != null) { + // CAT items are listed 1...n while list start at 0, need to + // subtract one. + menu.defaultItem = ValueParser.retrieveItemId(ctlv) - 1; + } + + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + mIconLoadState = LOAD_SINGLE_ICON; + titleIconId = ValueParser.retrieveIconId(ctlv); + menu.titleIconSelfExplanatory = titleIconId.selfExplanatory; + } + + ctlv = searchForTag(ComprehensionTlvTag.ITEM_ICON_ID_LIST, ctlvs); + if (ctlv != null) { + mIconLoadState = LOAD_MULTI_ICONS; + itemsIconId = ValueParser.retrieveItemsIconId(ctlv); + menu.itemsIconSelfExplanatory = itemsIconId.selfExplanatory; + } + + boolean presentTypeSpecified = (cmdDet.commandQualifier & 0x01) != 0; + if (presentTypeSpecified) { + if ((cmdDet.commandQualifier & 0x02) == 0) { + menu.presentationType = PresentationType.DATA_VALUES; + } else { + menu.presentationType = PresentationType.NAVIGATION_OPTIONS; + } + } + menu.softKeyPreferred = (cmdDet.commandQualifier & 0x04) != 0; + menu.helpAvailable = (cmdDet.commandQualifier & 0x80) != 0; + + mCmdParams = new SelectItemParams(cmdDet, menu, titleIconId != null); + + // Load icons data if needed. + switch(mIconLoadState) { + case LOAD_NO_ICON: + return false; + case LOAD_SINGLE_ICON: + mIconLoader.loadIcon(titleIconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + break; + case LOAD_MULTI_ICONS: + int[] recordNumbers = itemsIconId.recordNumbers; + if (titleIconId != null) { + // Create a new array for all the icons (title and items). + recordNumbers = new int[itemsIconId.recordNumbers.length + 1]; + recordNumbers[0] = titleIconId.recordNumber; + System.arraycopy(itemsIconId.recordNumbers, 0, recordNumbers, + 1, itemsIconId.recordNumbers.length); + } + mIconLoader.loadIcons(recordNumbers, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + break; + } + return true; + } + + /** + * Processes EVENT_NOTIFY message from baseband. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + */ + private boolean processEventNotify(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process EventNotify"); + + TextMessage textMsg = new TextMessage(); + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, + ctlvs); + textMsg.text = ValueParser.retrieveAlphaId(ctlv); + + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + textMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + + textMsg.responseNeeded = false; + mCmdParams = new DisplayTextParams(cmdDet, textMsg); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes SET_UP_EVENT_LIST proactive command from the SIM card. + * + * @param cmdDet Command Details object retrieved. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + */ + private boolean processSetUpEventList(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) { + + CatLog.d(this, "process SetUpEventList"); + // + // ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.EVENT_LIST, + // ctlvs); + // if (ctlv != null) { + // try { + // byte[] rawValue = ctlv.getRawValue(); + // int valueIndex = ctlv.getValueIndex(); + // int valueLen = ctlv.getLength(); + // + // } catch (IndexOutOfBoundsException e) {} + // } + return true; + } + + /** + * Processes LAUNCH_BROWSER proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + * @throws ResultException + */ + private boolean processLaunchBrowser(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process LaunchBrowser"); + + TextMessage confirmMsg = new TextMessage(); + IconId iconId = null; + String url = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.URL, ctlvs); + if (ctlv != null) { + try { + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int valueLen = ctlv.getLength(); + if (valueLen > 0) { + url = GsmAlphabet.gsm8BitUnpackedToString(rawValue, + valueIndex, valueLen); + } else { + url = null; + } + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } + + // parse alpha identifier. + ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs); + confirmMsg.text = ValueParser.retrieveAlphaId(ctlv); + + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + confirmMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + + // parse command qualifier value. + LaunchBrowserMode mode; + switch (cmdDet.commandQualifier) { + case 0x00: + default: + mode = LaunchBrowserMode.LAUNCH_IF_NOT_ALREADY_LAUNCHED; + break; + case 0x02: + mode = LaunchBrowserMode.USE_EXISTING_BROWSER; + break; + case 0x03: + mode = LaunchBrowserMode.LAUNCH_NEW_BROWSER; + break; + } + + mCmdParams = new LaunchBrowserParams(cmdDet, confirmMsg, url, mode); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes PLAY_TONE proactive command from the SIM card. + * + * @param cmdDet Command Details container object. + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required.t + * @throws ResultException + */ + private boolean processPlayTone(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + + CatLog.d(this, "process PlayTone"); + + Tone tone = null; + TextMessage textMsg = new TextMessage(); + Duration duration = null; + IconId iconId = null; + + ComprehensionTlv ctlv = searchForTag(ComprehensionTlvTag.TONE, ctlvs); + if (ctlv != null) { + // Nothing to do for null objects. + if (ctlv.getLength() > 0) { + try { + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int toneVal = rawValue[valueIndex]; + tone = Tone.fromInt(toneVal); + } catch (IndexOutOfBoundsException e) { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } + } + // parse alpha identifier + ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs); + if (ctlv != null) { + textMsg.text = ValueParser.retrieveAlphaId(ctlv); + } + // parse tone duration + ctlv = searchForTag(ComprehensionTlvTag.DURATION, ctlvs); + if (ctlv != null) { + duration = ValueParser.retrieveDuration(ctlv); + } + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + textMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + + boolean vibrate = (cmdDet.commandQualifier & 0x01) != 0x00; + + textMsg.responseNeeded = false; + mCmdParams = new PlayToneParams(cmdDet, textMsg, tone, duration, vibrate); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + /** + * Processes SETUP_CALL proactive command from the SIM card. + * + * @param cmdDet Command Details object retrieved from the proactive command + * object + * @param ctlvs List of ComprehensionTlv objects following Command Details + * object and Device Identities object within the proactive command + * @return true if the command is processing is pending and additional + * asynchronous processing is required. + */ + private boolean processSetupCall(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + CatLog.d(this, "process SetupCall"); + + Iterator<ComprehensionTlv> iter = ctlvs.iterator(); + ComprehensionTlv ctlv = null; + // User confirmation phase message. + TextMessage confirmMsg = new TextMessage(); + // Call set up phase message. + TextMessage callMsg = new TextMessage(); + IconId confirmIconId = null; + IconId callIconId = null; + + // get confirmation message string. + ctlv = searchForNextTag(ComprehensionTlvTag.ALPHA_ID, iter); + confirmMsg.text = ValueParser.retrieveAlphaId(ctlv); + + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + confirmIconId = ValueParser.retrieveIconId(ctlv); + confirmMsg.iconSelfExplanatory = confirmIconId.selfExplanatory; + } + + // get call set up message string. + ctlv = searchForNextTag(ComprehensionTlvTag.ALPHA_ID, iter); + if (ctlv != null) { + callMsg.text = ValueParser.retrieveAlphaId(ctlv); + } + + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + callIconId = ValueParser.retrieveIconId(ctlv); + callMsg.iconSelfExplanatory = callIconId.selfExplanatory; + } + + mCmdParams = new CallSetupParams(cmdDet, confirmMsg, callMsg); + + if (confirmIconId != null || callIconId != null) { + mIconLoadState = LOAD_MULTI_ICONS; + int[] recordNumbers = new int[2]; + recordNumbers[0] = confirmIconId != null + ? confirmIconId.recordNumber : -1; + recordNumbers[1] = callIconId != null ? callIconId.recordNumber + : -1; + + mIconLoader.loadIcons(recordNumbers, this + .obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } + + private boolean processProvideLocalInfo(CommandDetails cmdDet, List<ComprehensionTlv> ctlvs) + throws ResultException { + CatLog.d(this, "process ProvideLocalInfo"); + switch (cmdDet.commandQualifier) { + case DTTZ_SETTING: + CatLog.d(this, "PLI [DTTZ_SETTING]"); + mCmdParams = new CommandParams(cmdDet); + break; + case LANGUAGE_SETTING: + CatLog.d(this, "PLI [LANGUAGE_SETTING]"); + mCmdParams = new CommandParams(cmdDet); + break; + default: + CatLog.d(this, "PLI[" + cmdDet.commandQualifier + "] Command Not Supported"); + mCmdParams = new CommandParams(cmdDet); + throw new ResultException(ResultCode.BEYOND_TERMINAL_CAPABILITY); + } + return false; + } + + private boolean processBIPClient(CommandDetails cmdDet, + List<ComprehensionTlv> ctlvs) throws ResultException { + AppInterface.CommandType commandType = + AppInterface.CommandType.fromInt(cmdDet.typeOfCommand); + if (commandType != null) { + CatLog.d(this, "process "+ commandType.name()); + } + + TextMessage textMsg = new TextMessage(); + IconId iconId = null; + ComprehensionTlv ctlv = null; + boolean has_alpha_id = false; + + // parse alpha identifier + ctlv = searchForTag(ComprehensionTlvTag.ALPHA_ID, ctlvs); + if (ctlv != null) { + textMsg.text = ValueParser.retrieveAlphaId(ctlv); + CatLog.d(this, "alpha TLV text=" + textMsg.text); + has_alpha_id = true; + } + + // parse icon identifier + ctlv = searchForTag(ComprehensionTlvTag.ICON_ID, ctlvs); + if (ctlv != null) { + iconId = ValueParser.retrieveIconId(ctlv); + textMsg.iconSelfExplanatory = iconId.selfExplanatory; + } + + textMsg.responseNeeded = false; + mCmdParams = new BIPClientParams(cmdDet, textMsg, has_alpha_id); + + if (iconId != null) { + mIconLoadState = LOAD_SINGLE_ICON; + mIconLoader.loadIcon(iconId.recordNumber, this.obtainMessage(MSG_ID_LOAD_ICON_DONE)); + return true; + } + return false; + } +} diff --git a/src/java/com/android/internal/telephony/cat/ComprehensionTlv.java b/src/java/com/android/internal/telephony/cat/ComprehensionTlv.java new file mode 100644 index 0000000..22cd5a4 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ComprehensionTlv.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2011 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.internal.telephony.cat; + +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + + +/** + * Class for representing COMPREHENSION-TLV objects. + * + * @see "ETSI TS 101 220 subsection 7.1.1" + * + * {@hide} + */ +class ComprehensionTlv { + private static final String LOG_TAG = "ComprehensionTlv"; + private int mTag; + private boolean mCr; + private int mLength; + private int mValueIndex; + private byte[] mRawValue; + + /** + * Constructor. Private on purpose. Use + * {@link #decodeMany(byte[], int) decodeMany} or + * {@link #decode(byte[], int) decode} method. + * + * @param tag The tag for this object + * @param cr Comprehension Required flag + * @param length Length of the value + * @param data Byte array containing the value + * @param valueIndex Index in data at which the value starts + */ + protected ComprehensionTlv(int tag, boolean cr, int length, byte[] data, + int valueIndex) { + mTag = tag; + mCr = cr; + mLength = length; + mValueIndex = valueIndex; + mRawValue = data; + } + + public int getTag() { + return mTag; + } + + public boolean isComprehensionRequired() { + return mCr; + } + + public int getLength() { + return mLength; + } + + public int getValueIndex() { + return mValueIndex; + } + + public byte[] getRawValue() { + return mRawValue; + } + + /** + * Parses a list of COMPREHENSION-TLV objects from a byte array. + * + * @param data A byte array containing data to be parsed + * @param startIndex Index in data at which to start parsing + * @return A list of COMPREHENSION-TLV objects parsed + * @throws ResultException + */ + public static List<ComprehensionTlv> decodeMany(byte[] data, int startIndex) + throws ResultException { + ArrayList<ComprehensionTlv> items = new ArrayList<ComprehensionTlv>(); + int endIndex = data.length; + while (startIndex < endIndex) { + ComprehensionTlv ctlv = ComprehensionTlv.decode(data, startIndex); + if (ctlv != null) { + items.add(ctlv); + startIndex = ctlv.mValueIndex + ctlv.mLength; + } else { + CatLog.d(LOG_TAG, "decodeMany: ctlv is null, stop decoding"); + break; + } + } + + return items; + } + + /** + * Parses an COMPREHENSION-TLV object from a byte array. + * + * @param data A byte array containing data to be parsed + * @param startIndex Index in data at which to start parsing + * @return A COMPREHENSION-TLV object parsed + * @throws ResultException + */ + public static ComprehensionTlv decode(byte[] data, int startIndex) + throws ResultException { + int curIndex = startIndex; + int endIndex = data.length; + + try { + /* tag */ + int tag; + boolean cr; // Comprehension required flag + int temp = data[curIndex++] & 0xff; + switch (temp) { + case 0: + case 0xff: + case 0x80: + Log.d("CAT ", "decode: unexpected first tag byte=" + Integer.toHexString(temp) + + ", startIndex=" + startIndex + " curIndex=" + curIndex + + " endIndex=" + endIndex); + // Return null which will stop decoding, this has occurred + // with Ghana MTN simcard and JDI simcard. + return null; + + case 0x7f: // tag is in three-byte format + tag = ((data[curIndex] & 0xff) << 8) + | (data[curIndex + 1] & 0xff); + cr = (tag & 0x8000) != 0; + tag &= ~0x8000; + curIndex += 2; + break; + + default: // tag is in single-byte format + tag = temp; + cr = (tag & 0x80) != 0; + tag &= ~0x80; + break; + } + + /* length */ + int length; + temp = data[curIndex++] & 0xff; + if (temp < 0x80) { + length = temp; + } else if (temp == 0x81) { + length = data[curIndex++] & 0xff; + if (length < 0x80) { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "length < 0x80 length=" + Integer.toHexString(length) + + " startIndex=" + startIndex + " curIndex=" + curIndex + + " endIndex=" + endIndex); + } + } else if (temp == 0x82) { + length = ((data[curIndex] & 0xff) << 8) + | (data[curIndex + 1] & 0xff); + curIndex += 2; + if (length < 0x100) { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "two byte length < 0x100 length=" + Integer.toHexString(length) + + " startIndex=" + startIndex + " curIndex=" + curIndex + + " endIndex=" + endIndex); + } + } else if (temp == 0x83) { + length = ((data[curIndex] & 0xff) << 16) + | ((data[curIndex + 1] & 0xff) << 8) + | (data[curIndex + 2] & 0xff); + curIndex += 3; + if (length < 0x10000) { + throw new ResultException( + ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "three byte length < 0x10000 length=0x" + Integer.toHexString(length) + + " startIndex=" + startIndex + " curIndex=" + curIndex + + " endIndex=" + endIndex); + } + } else { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "Bad length modifer=" + temp + + " startIndex=" + startIndex + " curIndex=" + curIndex + + " endIndex=" + endIndex); + + } + + return new ComprehensionTlv(tag, cr, length, data, curIndex); + + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD, + "IndexOutOfBoundsException" + " startIndex=" + startIndex + + " curIndex=" + curIndex + " endIndex=" + endIndex); + } + } +} diff --git a/src/java/com/android/internal/telephony/cat/ComprehensionTlvTag.java b/src/java/com/android/internal/telephony/cat/ComprehensionTlvTag.java new file mode 100644 index 0000000..973dbc8 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ComprehensionTlvTag.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2011 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.internal.telephony.cat; + +/** + * Enumeration for representing the tag value of COMPREHENSION-TLV objects. If + * you want to get the actual value, call {@link #value() value} method. + * + * {@hide} + */ +public enum ComprehensionTlvTag { + COMMAND_DETAILS(0x01), + DEVICE_IDENTITIES(0x02), + RESULT(0x03), + DURATION(0x04), + ALPHA_ID(0x05), + ADDRESS(0x06), + USSD_STRING(0x0a), + SMS_TPDU(0x0b), + TEXT_STRING(0x0d), + TONE(0x0e), + ITEM(0x0f), + ITEM_ID(0x10), + RESPONSE_LENGTH(0x11), + FILE_LIST(0x12), + HELP_REQUEST(0x15), + DEFAULT_TEXT(0x17), + EVENT_LIST(0x19), + ICON_ID(0x1e), + ITEM_ICON_ID_LIST(0x1f), + IMMEDIATE_RESPONSE(0x2b), + LANGUAGE(0x2d), + URL(0x31), + BROWSER_TERMINATION_CAUSE(0x34), + TEXT_ATTRIBUTE(0x50); + + private int mValue; + + ComprehensionTlvTag(int value) { + mValue = value; + } + + /** + * Returns the actual value of this COMPREHENSION-TLV object. + * + * @return Actual tag value of this object + */ + public int value() { + return mValue; + } + + public static ComprehensionTlvTag fromInt(int value) { + for (ComprehensionTlvTag e : ComprehensionTlvTag.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cat/Duration.java b/src/java/com/android/internal/telephony/cat/Duration.java new file mode 100644 index 0000000..e8cd404 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/Duration.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import android.os.Parcel; +import android.os.Parcelable; + + +/** + * Class for representing "Duration" object for CAT. + * + * {@hide} + */ +public class Duration implements Parcelable { + public int timeInterval; + public TimeUnit timeUnit; + + public enum TimeUnit { + MINUTE(0x00), + SECOND(0x01), + TENTH_SECOND(0x02); + + private int mValue; + + TimeUnit(int value) { + mValue = value; + } + + public int value() { + return mValue; + } + } + + /** + * @param timeInterval Between 1 and 255 inclusive. + */ + public Duration(int timeInterval, TimeUnit timeUnit) { + this.timeInterval = timeInterval; + this.timeUnit = timeUnit; + } + + private Duration(Parcel in) { + timeInterval = in.readInt(); + timeUnit = TimeUnit.values()[in.readInt()]; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(timeInterval); + dest.writeInt(timeUnit.ordinal()); + } + + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Duration> CREATOR = new Parcelable.Creator<Duration>() { + public Duration createFromParcel(Parcel in) { + return new Duration(in); + } + + public Duration[] newArray(int size) { + return new Duration[size]; + } + }; +} diff --git a/src/java/com/android/internal/telephony/cat/FontSize.java b/src/java/com/android/internal/telephony/cat/FontSize.java new file mode 100644 index 0000000..02c7ea0 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/FontSize.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Enumeration for representing text font size. + * + * {@hide} + */ +public enum FontSize { + NORMAL(0x0), + LARGE(0x1), + SMALL(0x2); + + private int mValue; + + FontSize(int value) { + mValue = value; + } + + /** + * Create a FontSize object. + * @param value Integer value to be converted to a FontSize object. + * @return FontSize object whose value is {@code value}. If no + * FontSize object has that value, null is returned. + */ + public static FontSize fromInt(int value) { + for (FontSize e : FontSize.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cat/IconLoader.java b/src/java/com/android/internal/telephony/cat/IconLoader.java new file mode 100644 index 0000000..2fa1811 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/IconLoader.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import com.android.internal.telephony.IccFileHandler; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.util.HashMap; + +/** + * Class for loading icons from the SIM card. Has two states: single, for loading + * one icon. Multi, for loading icons list. + * + */ +class IconLoader extends Handler { + // members + private int mState = STATE_SINGLE_ICON; + private ImageDescriptor mId = null; + private Bitmap mCurrentIcon = null; + private int mRecordNumber; + private IccFileHandler mSimFH = null; + private Message mEndMsg = null; + private byte[] mIconData = null; + // multi icons state members + private int[] mRecordNumbers = null; + private int mCurrentRecordIndex = 0; + private Bitmap[] mIcons = null; + private HashMap<Integer, Bitmap> mIconsCache = null; + + private static IconLoader sLoader = null; + + // Loader state values. + private static final int STATE_SINGLE_ICON = 1; + private static final int STATE_MULTI_ICONS = 2; + + // Finished loading single record from a linear-fixed EF-IMG. + private static final int EVENT_READ_EF_IMG_RECOED_DONE = 1; + // Finished loading single icon from a Transparent DF-Graphics. + private static final int EVENT_READ_ICON_DONE = 2; + // Finished loading single colour icon lookup table. + private static final int EVENT_READ_CLUT_DONE = 3; + + // Color lookup table offset inside the EF. + private static final int CLUT_LOCATION_OFFSET = 4; + // CLUT entry size, {Red, Green, Black} + private static final int CLUT_ENTRY_SIZE = 3; + + + private IconLoader(Looper looper , IccFileHandler fh) { + super(looper); + mSimFH = fh; + + mIconsCache = new HashMap<Integer, Bitmap>(50); + } + + static IconLoader getInstance(Handler caller, IccFileHandler fh) { + if (sLoader != null) { + return sLoader; + } + if (fh != null) { + HandlerThread thread = new HandlerThread("Cat Icon Loader"); + thread.start(); + return new IconLoader(thread.getLooper(), fh); + } + return null; + } + + void loadIcons(int[] recordNumbers, Message msg) { + if (recordNumbers == null || recordNumbers.length == 0 || msg == null) { + return; + } + mEndMsg = msg; + // initialize multi icons load variables. + mIcons = new Bitmap[recordNumbers.length]; + mRecordNumbers = recordNumbers; + mCurrentRecordIndex = 0; + mState = STATE_MULTI_ICONS; + startLoadingIcon(recordNumbers[0]); + } + + void loadIcon(int recordNumber, Message msg) { + if (msg == null) { + return; + } + mEndMsg = msg; + mState = STATE_SINGLE_ICON; + startLoadingIcon(recordNumber); + } + + private void startLoadingIcon(int recordNumber) { + // Reset the load variables. + mId = null; + mIconData = null; + mCurrentIcon = null; + mRecordNumber = recordNumber; + + // make sure the icon was not already loaded and saved in the local cache. + if (mIconsCache.containsKey(recordNumber)) { + mCurrentIcon = mIconsCache.get(recordNumber); + postIcon(); + return; + } + + // start the first phase ==> loading Image Descriptor. + readId(); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + try { + switch (msg.what) { + case EVENT_READ_EF_IMG_RECOED_DONE: + ar = (AsyncResult) msg.obj; + if (handleImageDescriptor((byte[]) ar.result)) { + readIconData(); + } else { + throw new Exception("Unable to parse image descriptor"); + } + break; + case EVENT_READ_ICON_DONE: + ar = (AsyncResult) msg.obj; + byte[] rawData = ((byte[]) ar.result); + if (mId.codingScheme == ImageDescriptor.CODING_SCHEME_BASIC) { + mCurrentIcon = parseToBnW(rawData, rawData.length); + mIconsCache.put(mRecordNumber, mCurrentIcon); + postIcon(); + } else if (mId.codingScheme == ImageDescriptor.CODING_SCHEME_COLOUR) { + mIconData = rawData; + readClut(); + } + break; + case EVENT_READ_CLUT_DONE: + ar = (AsyncResult) msg.obj; + byte [] clut = ((byte[]) ar.result); + mCurrentIcon = parseToRGB(mIconData, mIconData.length, + false, clut); + mIconsCache.put(mRecordNumber, mCurrentIcon); + postIcon(); + break; + } + } catch (Exception e) { + CatLog.d(this, "Icon load failed"); + // post null icon back to the caller. + postIcon(); + } + } + + /** + * Handles Image descriptor parsing and required processing. This is the + * first step required to handle retrieving icons from the SIM. + * + * @param data byte [] containing Image Instance descriptor as defined in + * TS 51.011. + */ + private boolean handleImageDescriptor(byte[] rawData) { + mId = ImageDescriptor.parse(rawData, 1); + if (mId == null) { + return false; + } + return true; + } + + // Start reading colour lookup table from SIM card. + private void readClut() { + int length = mIconData[3] * CLUT_ENTRY_SIZE; + Message msg = this.obtainMessage(EVENT_READ_CLUT_DONE); + mSimFH.loadEFImgTransparent(mId.imageId, + mIconData[CLUT_LOCATION_OFFSET], + mIconData[CLUT_LOCATION_OFFSET + 1], length, msg); + } + + // Start reading Image Descriptor from SIM card. + private void readId() { + if (mRecordNumber < 0) { + mCurrentIcon = null; + postIcon(); + return; + } + Message msg = this.obtainMessage(EVENT_READ_EF_IMG_RECOED_DONE); + mSimFH.loadEFImgLinearFixed(mRecordNumber, msg); + } + + // Start reading icon bytes array from SIM card. + private void readIconData() { + Message msg = this.obtainMessage(EVENT_READ_ICON_DONE); + mSimFH.loadEFImgTransparent(mId.imageId, 0, 0, mId.length ,msg); + } + + // When all is done pass icon back to caller. + private void postIcon() { + if (mState == STATE_SINGLE_ICON) { + mEndMsg.obj = mCurrentIcon; + mEndMsg.sendToTarget(); + } else if (mState == STATE_MULTI_ICONS) { + mIcons[mCurrentRecordIndex++] = mCurrentIcon; + // If not all icons were loaded, start loading the next one. + if (mCurrentRecordIndex < mRecordNumbers.length) { + startLoadingIcon(mRecordNumbers[mCurrentRecordIndex]); + } else { + mEndMsg.obj = mIcons; + mEndMsg.sendToTarget(); + } + } + } + + /** + * Convert a TS 131.102 image instance of code scheme '11' into Bitmap + * @param data The raw data + * @param length The length of image body + * @return The bitmap + */ + public static Bitmap parseToBnW(byte[] data, int length){ + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int numOfPixels = width*height; + + int[] pixels = new int[numOfPixels]; + + int pixelIndex = 0; + int bitIndex = 7; + byte currentByte = 0x00; + while (pixelIndex < numOfPixels) { + // reassign data and index for every byte (8 bits). + if (pixelIndex % 8 == 0) { + currentByte = data[valueIndex++]; + bitIndex = 7; + } + pixels[pixelIndex++] = bitToBnW((currentByte >> bitIndex-- ) & 0x01); + } + + if (pixelIndex != numOfPixels) { + CatLog.d("IconLoader", "parseToBnW; size error"); + } + return Bitmap.createBitmap(pixels, width, height, Bitmap.Config.ARGB_8888); + } + + /** + * Decode one bit to a black and white color: + * 0 is black + * 1 is white + * @param bit to decode + * @return RGB color + */ + private static int bitToBnW(int bit){ + if(bit == 1){ + return Color.WHITE; + } else { + return Color.BLACK; + } + } + + /** + * a TS 131.102 image instance of code scheme '11' into color Bitmap + * + * @param data The raw data + * @param length the length of image body + * @param transparency with or without transparency + * @param clut coulor lookup table + * @return The color bitmap + */ + public static Bitmap parseToRGB(byte[] data, int length, + boolean transparency, byte[] clut) { + int valueIndex = 0; + int width = data[valueIndex++] & 0xFF; + int height = data[valueIndex++] & 0xFF; + int bitsPerImg = data[valueIndex++] & 0xFF; + int numOfClutEntries = data[valueIndex++] & 0xFF; + + if (true == transparency) { + clut[numOfClutEntries - 1] = Color.TRANSPARENT; + } + + int numOfPixels = width * height; + int[] pixels = new int[numOfPixels]; + + valueIndex = 6; + int pixelIndex = 0; + int bitsStartOffset = 8 - bitsPerImg; + int bitIndex = bitsStartOffset; + byte currentByte = data[valueIndex++]; + int mask = getMask(bitsPerImg); + boolean bitsOverlaps = (8 % bitsPerImg == 0); + while (pixelIndex < numOfPixels) { + // reassign data and index for every byte (8 bits). + if (bitIndex < 0) { + currentByte = data[valueIndex++]; + bitIndex = bitsOverlaps ? (bitsStartOffset) : (bitIndex * -1); + } + int clutEntry = ((currentByte >> bitIndex) & mask); + int clutIndex = clutEntry * CLUT_ENTRY_SIZE; + pixels[pixelIndex++] = Color.rgb(clut[clutIndex], + clut[clutIndex + 1], clut[clutIndex + 2]); + bitIndex -= bitsPerImg; + } + + return Bitmap.createBitmap(pixels, width, height, + Bitmap.Config.ARGB_8888); + } + + /** + * Calculate bit mask for a given number of bits. The mask should enable to + * make a bitwise and to the given number of bits. + * @param numOfBits number of bits to calculate mask for. + * @return bit mask + */ + private static int getMask(int numOfBits) { + int mask = 0x00; + + switch (numOfBits) { + case 1: + mask = 0x01; + break; + case 2: + mask = 0x03; + break; + case 3: + mask = 0x07; + break; + case 4: + mask = 0x0F; + break; + case 5: + mask = 0x1F; + break; + case 6: + mask = 0x3F; + break; + case 7: + mask = 0x7F; + break; + case 8: + mask = 0xFF; + break; + } + return mask; + } +} diff --git a/src/java/com/android/internal/telephony/cat/ImageDescriptor.java b/src/java/com/android/internal/telephony/cat/ImageDescriptor.java new file mode 100644 index 0000000..711d977 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ImageDescriptor.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +/** + * {@hide} + */ +public class ImageDescriptor { + // members + int width; + int height; + int codingScheme; + int imageId; + int highOffset; + int lowOffset; + int length; + + // constants + static final int CODING_SCHEME_BASIC = 0x11; + static final int CODING_SCHEME_COLOUR = 0x21; + + // public static final int ID_LENGTH = 9; + // ID_LENGTH substituted by IccFileHandlerBase.GET_RESPONSE_EF_IMG_SIZE_BYTES + + ImageDescriptor() { + width = 0; + height = 0; + codingScheme = 0; + imageId = 0; + highOffset = 0; + lowOffset = 0; + length = 0; + } + + /** + * Extract descriptor information about image instance. + * + * @param rawData + * @param valueIndex + * @return ImageDescriptor + */ + static ImageDescriptor parse(byte[] rawData, int valueIndex) { + ImageDescriptor d = new ImageDescriptor(); + try { + d.width = rawData[valueIndex++] & 0xff; + d.height = rawData[valueIndex++] & 0xff; + d.codingScheme = rawData[valueIndex++] & 0xff; + + // parse image id + d.imageId = (rawData[valueIndex++] & 0xff) << 8; + d.imageId |= rawData[valueIndex++] & 0xff; + // parse offset + d.highOffset = (rawData[valueIndex++] & 0xff); // high byte offset + d.lowOffset = rawData[valueIndex++] & 0xff; // low byte offset + + d.length = ((rawData[valueIndex++] & 0xff) << 8 | (rawData[valueIndex++] & 0xff)); + } catch (IndexOutOfBoundsException e) { + CatLog.d("ImageDescripter", "parse; failed parsing image descriptor"); + d = null; + } + return d; + } +} diff --git a/src/java/com/android/internal/telephony/cat/Input.java b/src/java/com/android/internal/telephony/cat/Input.java new file mode 100644 index 0000000..13a5ad4 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/Input.java @@ -0,0 +1,101 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Container class for CAT GET INPUT, GET IN KEY commands parameters. + * + */ +public class Input implements Parcelable { + public String text; + public String defaultText; + public Bitmap icon; + public int minLen; + public int maxLen; + public boolean ucs2; + public boolean packed; + public boolean digitOnly; + public boolean echo; + public boolean yesNo; + public boolean helpAvailable; + public Duration duration; + + Input() { + text = ""; + defaultText = null; + icon = null; + minLen = 0; + maxLen = 1; + ucs2 = false; + packed = false; + digitOnly = false; + echo = false; + yesNo = false; + helpAvailable = false; + duration = null; + } + + private Input(Parcel in) { + text = in.readString(); + defaultText = in.readString(); + icon = in.readParcelable(null); + minLen = in.readInt(); + maxLen = in.readInt(); + ucs2 = in.readInt() == 1 ? true : false; + packed = in.readInt() == 1 ? true : false; + digitOnly = in.readInt() == 1 ? true : false; + echo = in.readInt() == 1 ? true : false; + yesNo = in.readInt() == 1 ? true : false; + helpAvailable = in.readInt() == 1 ? true : false; + duration = in.readParcelable(null); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(text); + dest.writeString(defaultText); + dest.writeParcelable(icon, 0); + dest.writeInt(minLen); + dest.writeInt(maxLen); + dest.writeInt(ucs2 ? 1 : 0); + dest.writeInt(packed ? 1 : 0); + dest.writeInt(digitOnly ? 1 : 0); + dest.writeInt(echo ? 1 : 0); + dest.writeInt(yesNo ? 1 : 0); + dest.writeInt(helpAvailable ? 1 : 0); + dest.writeParcelable(duration, 0); + } + + public static final Parcelable.Creator<Input> CREATOR = new Parcelable.Creator<Input>() { + public Input createFromParcel(Parcel in) { + return new Input(in); + } + + public Input[] newArray(int size) { + return new Input[size]; + } + }; + + boolean setIcon(Bitmap Icon) { return true; } +}
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/cat/Item.java b/src/java/com/android/internal/telephony/cat/Item.java new file mode 100644 index 0000000..d4702bb --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/Item.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Represents an Item COMPREHENSION-TLV object. + * + * {@hide} + */ +public class Item implements Parcelable { + /** Identifier of the item. */ + public int id; + /** Text string of the item. */ + public String text; + /** Icon of the item */ + public Bitmap icon; + + public Item(int id, String text) { + this.id = id; + this.text = text; + this.icon = null; + } + + public Item(Parcel in) { + id = in.readInt(); + text = in.readString(); + icon = in.readParcelable(null); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeString(text); + dest.writeParcelable(icon, flags); + } + + public static final Parcelable.Creator<Item> CREATOR = new Parcelable.Creator<Item>() { + public Item createFromParcel(Parcel in) { + return new Item(in); + } + + public Item[] newArray(int size) { + return new Item[size]; + } + }; + + public String toString() { + return text; + } +} diff --git a/src/java/com/android/internal/telephony/cat/LaunchBrowserMode.java b/src/java/com/android/internal/telephony/cat/LaunchBrowserMode.java new file mode 100644 index 0000000..af043d1 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/LaunchBrowserMode.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Browser launch mode for LAUNCH BROWSER proactive command. + * + * {@hide} + */ +public enum LaunchBrowserMode { + /** Launch browser if not already launched. */ + LAUNCH_IF_NOT_ALREADY_LAUNCHED, + /** + * Use the existing browser (the browser shall not use the active existing + * secured session). + */ + USE_EXISTING_BROWSER, + /** Close the existing browser session and launch new browser session. */ + LAUNCH_NEW_BROWSER; +} diff --git a/src/java/com/android/internal/telephony/cat/Menu.java b/src/java/com/android/internal/telephony/cat/Menu.java new file mode 100644 index 0000000..7bbae01 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/Menu.java @@ -0,0 +1,105 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Container class for CAT menu (SET UP MENU, SELECT ITEM) parameters. + * + */ +public class Menu implements Parcelable { + public List<Item> items; + public List<TextAttribute> titleAttrs; + public PresentationType presentationType; + public String title; + public Bitmap titleIcon; + public int defaultItem; + public boolean softKeyPreferred; + public boolean helpAvailable; + public boolean titleIconSelfExplanatory; + public boolean itemsIconSelfExplanatory; + + public Menu() { + // Create an empty list. + items = new ArrayList<Item>(); + title = null; + titleAttrs = null; + defaultItem = 0; + softKeyPreferred = false; + helpAvailable = false; + titleIconSelfExplanatory = false; + itemsIconSelfExplanatory = false; + titleIcon = null; + // set default style to be navigation menu. + presentationType = PresentationType.NAVIGATION_OPTIONS; + } + + private Menu(Parcel in) { + title = in.readString(); + titleIcon = in.readParcelable(null); + // rebuild items list. + items = new ArrayList<Item>(); + int size = in.readInt(); + for (int i=0; i<size; i++) { + Item item = in.readParcelable(null); + items.add(item); + } + defaultItem = in.readInt(); + softKeyPreferred = in.readInt() == 1 ? true : false; + helpAvailable = in.readInt() == 1 ? true : false; + titleIconSelfExplanatory = in.readInt() == 1 ? true : false; + itemsIconSelfExplanatory = in.readInt() == 1 ? true : false; + presentationType = PresentationType.values()[in.readInt()]; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(title); + dest.writeParcelable(titleIcon, flags); + // write items list to the parcel. + int size = items.size(); + dest.writeInt(size); + for (int i=0; i<size; i++) { + dest.writeParcelable(items.get(i), flags); + } + dest.writeInt(defaultItem); + dest.writeInt(softKeyPreferred ? 1 : 0); + dest.writeInt(helpAvailable ? 1 : 0); + dest.writeInt(titleIconSelfExplanatory ? 1 : 0); + dest.writeInt(itemsIconSelfExplanatory ? 1 : 0); + dest.writeInt(presentationType.ordinal()); + } + + public static final Parcelable.Creator<Menu> CREATOR = new Parcelable.Creator<Menu>() { + public Menu createFromParcel(Parcel in) { + return new Menu(in); + } + + public Menu[] newArray(int size) { + return new Menu[size]; + } + }; +} diff --git a/src/java/com/android/internal/telephony/cat/PresentationType.java b/src/java/com/android/internal/telephony/cat/PresentationType.java new file mode 100644 index 0000000..7c8cd8c --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/PresentationType.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Presentation types for SELECT TYPE proactive command. + * + * {@hide} + */ +public enum PresentationType { + /** Presentation type is not specified */ + NOT_SPECIFIED, + /** Presentation as a choice of data values */ + DATA_VALUES, + /** Presentation as a choice of navigation options */ + NAVIGATION_OPTIONS; +} diff --git a/src/java/com/android/internal/telephony/cat/ResponseData.java b/src/java/com/android/internal/telephony/cat/ResponseData.java new file mode 100644 index 0000000..1157c1a --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ResponseData.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2006-2007 Google Inc. + * + * 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.internal.telephony.cat; + +import com.android.internal.telephony.EncodeException; +import com.android.internal.telephony.GsmAlphabet; +import java.util.Calendar; +import java.util.TimeZone; +import android.os.SystemProperties; +import android.text.TextUtils; + +import com.android.internal.telephony.cat.AppInterface.CommandType; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; + +abstract class ResponseData { + /** + * Format the data appropriate for TERMINAL RESPONSE and write it into + * the ByteArrayOutputStream object. + */ + public abstract void format(ByteArrayOutputStream buf); + + public static void writeLength(ByteArrayOutputStream buf, int length) { + // As per ETSI 102.220 Sec7.1.2, if the total length is greater + // than 0x7F, it should be coded in two bytes and the first byte + // should be 0x81. + if (length > 0x7F) { + buf.write(0x81); + } + buf.write(length); + } +} + +class SelectItemResponseData extends ResponseData { + // members + private int id; + + public SelectItemResponseData(int id) { + super(); + this.id = id; + } + + @Override + public void format(ByteArrayOutputStream buf) { + // Item identifier object + int tag = 0x80 | ComprehensionTlvTag.ITEM_ID.value(); + buf.write(tag); // tag + buf.write(1); // length + buf.write(id); // identifier of item chosen + } +} + +class GetInkeyInputResponseData extends ResponseData { + // members + private boolean mIsUcs2; + private boolean mIsPacked; + private boolean mIsYesNo; + private boolean mYesNoResponse; + public String mInData; + + // GetInKey Yes/No response characters constants. + protected static final byte GET_INKEY_YES = 0x01; + protected static final byte GET_INKEY_NO = 0x00; + + public GetInkeyInputResponseData(String inData, boolean ucs2, boolean packed) { + super(); + this.mIsUcs2 = ucs2; + this.mIsPacked = packed; + this.mInData = inData; + this.mIsYesNo = false; + } + + public GetInkeyInputResponseData(boolean yesNoResponse) { + super(); + this.mIsUcs2 = false; + this.mIsPacked = false; + this.mInData = ""; + this.mIsYesNo = true; + this.mYesNoResponse = yesNoResponse; + } + + @Override + public void format(ByteArrayOutputStream buf) { + if (buf == null) { + return; + } + + // Text string object + int tag = 0x80 | ComprehensionTlvTag.TEXT_STRING.value(); + buf.write(tag); // tag + + byte[] data; + + if (mIsYesNo) { + data = new byte[1]; + data[0] = mYesNoResponse ? GET_INKEY_YES : GET_INKEY_NO; + } else if (mInData != null && mInData.length() > 0) { + try { + if (mIsUcs2) { + data = mInData.getBytes("UTF-16"); + } else if (mIsPacked) { + int size = mInData.length(); + + byte[] tempData = GsmAlphabet + .stringToGsm7BitPacked(mInData, 0, 0); + data = new byte[size]; + // Since stringToGsm7BitPacked() set byte 0 in the + // returned byte array to the count of septets used... + // copy to a new array without byte 0. + System.arraycopy(tempData, 1, data, 0, size); + } else { + data = GsmAlphabet.stringToGsm8BitPacked(mInData); + } + } catch (UnsupportedEncodingException e) { + data = new byte[0]; + } catch (EncodeException e) { + data = new byte[0]; + } + } else { + data = new byte[0]; + } + + // length - one more for data coding scheme. + writeLength(buf, data.length + 1); + + // data coding scheme + if (mIsUcs2) { + buf.write(0x08); // UCS2 + } else if (mIsPacked) { + buf.write(0x00); // 7 bit packed + } else { + buf.write(0x04); // 8 bit unpacked + } + + for (byte b : data) { + buf.write(b); + } + } +} + +// For "PROVIDE LOCAL INFORMATION" command. +// See TS 31.111 section 6.4.15/ETSI TS 102 223 +// TS 31.124 section 27.22.4.15 for test spec +class LanguageResponseData extends ResponseData { + private String lang; + + public LanguageResponseData(String lang) { + super(); + this.lang = lang; + } + + @Override + public void format(ByteArrayOutputStream buf) { + if (buf == null) { + return; + } + + // Text string object + int tag = 0x80 | ComprehensionTlvTag.LANGUAGE.value(); + buf.write(tag); // tag + + byte[] data; + + if (lang != null && lang.length() > 0) { + data = GsmAlphabet.stringToGsm8BitPacked(lang); + } + else { + data = new byte[0]; + } + + buf.write(data.length); + + for (byte b : data) { + buf.write(b); + } + } +} + +// For "PROVIDE LOCAL INFORMATION" command. +// See TS 31.111 section 6.4.15/ETSI TS 102 223 +// TS 31.124 section 27.22.4.15 for test spec +class DTTZResponseData extends ResponseData { + private Calendar calendar; + + public DTTZResponseData(Calendar cal) { + super(); + calendar = cal; + } + + @Override + public void format(ByteArrayOutputStream buf) { + if (buf == null) { + return; + } + + // DTTZ object + int tag = 0x80 | CommandType.PROVIDE_LOCAL_INFORMATION.value(); + buf.write(tag); // tag + + byte[] data = new byte[8]; + + data[0] = 0x07; // Write length of DTTZ data + + if (calendar == null) { + calendar = Calendar.getInstance(); + } + // Fill year byte + data[1] = byteToBCD(calendar.get(java.util.Calendar.YEAR) % 100); + + // Fill month byte + data[2] = byteToBCD(calendar.get(java.util.Calendar.MONTH) + 1); + + // Fill day byte + data[3] = byteToBCD(calendar.get(java.util.Calendar.DATE)); + + // Fill hour byte + data[4] = byteToBCD(calendar.get(java.util.Calendar.HOUR_OF_DAY)); + + // Fill minute byte + data[5] = byteToBCD(calendar.get(java.util.Calendar.MINUTE)); + + // Fill second byte + data[6] = byteToBCD(calendar.get(java.util.Calendar.SECOND)); + + String tz = SystemProperties.get("persist.sys.timezone", ""); + if (TextUtils.isEmpty(tz)) { + data[7] = (byte) 0xFF; // set FF in terminal response + } else { + TimeZone zone = TimeZone.getTimeZone(tz); + int zoneOffset = zone.getRawOffset() + zone.getDSTSavings(); + data[7] = getTZOffSetByte(zoneOffset); + } + + for (byte b : data) { + buf.write(b); + } + } + + private byte byteToBCD(int value) { + if (value < 0 && value > 99) { + CatLog.d(this, "Err: byteToBCD conversion Value is " + value + + " Value has to be between 0 and 99"); + return 0; + } + + return (byte) ((value / 10) | ((value % 10) << 4)); + } + + private byte getTZOffSetByte(long offSetVal) { + boolean isNegative = (offSetVal < 0); + + /* + * The 'offSetVal' is in milliseconds. Convert it to hours and compute + * offset While sending T.R to UICC, offset is expressed is 'quarters of + * hours' + */ + + long tzOffset = offSetVal / (15 * 60 * 1000); + tzOffset = (isNegative ? -1 : 1) * tzOffset; + byte bcdVal = byteToBCD((int) tzOffset); + // For negative offsets, put '1' in the msb + return isNegative ? (bcdVal |= 0x08) : bcdVal; + } + +} + diff --git a/src/java/com/android/internal/telephony/cat/ResultCode.java b/src/java/com/android/internal/telephony/cat/ResultCode.java new file mode 100644 index 0000000..8544175 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ResultCode.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Enumeration for the return code in TERMINAL RESPONSE. + * To get the actual return code for each enum value, call {@link #code() code} + * method. + * + * {@hide} + */ +public enum ResultCode { + + /* + * Results '0X' and '1X' indicate that the command has been performed. + */ + + /** Command performed successfully */ + OK(0x00), + + /** Command performed with partial comprehension */ + PRFRMD_WITH_PARTIAL_COMPREHENSION(0x01), + + /** Command performed, with missing information */ + PRFRMD_WITH_MISSING_INFO(0x02), + + /** REFRESH performed with additional EFs read */ + PRFRMD_WITH_ADDITIONAL_EFS_READ(0x03), + + /** + * Command performed successfully, but requested icon could not be + * displayed + */ + PRFRMD_ICON_NOT_DISPLAYED(0x04), + + /** Command performed, but modified by call control by NAA */ + PRFRMD_MODIFIED_BY_NAA(0x05), + + /** Command performed successfully, limited service */ + PRFRMD_LIMITED_SERVICE(0x06), + + /** Command performed with modification */ + PRFRMD_WITH_MODIFICATION(0x07), + + /** REFRESH performed but indicated NAA was not active */ + PRFRMD_NAA_NOT_ACTIVE(0x08), + + /** Command performed successfully, tone not played */ + PRFRMD_TONE_NOT_PLAYED(0x09), + + /** Proactive UICC session terminated by the user */ + UICC_SESSION_TERM_BY_USER(0x10), + + /** Backward move in the proactive UICC session requested by the user */ + BACKWARD_MOVE_BY_USER(0x11), + + /** No response from user */ + NO_RESPONSE_FROM_USER(0x12), + + /** Help information required by the user */ + HELP_INFO_REQUIRED(0x13), + + /** USSD or SS transaction terminated by the user */ + USSD_SS_SESSION_TERM_BY_USER(0x14), + + + /* + * Results '2X' indicate to the UICC that it may be worth re-trying the + * command at a later opportunity. + */ + + /** Terminal currently unable to process command */ + TERMINAL_CRNTLY_UNABLE_TO_PROCESS(0x20), + + /** Network currently unable to process command */ + NETWORK_CRNTLY_UNABLE_TO_PROCESS(0x21), + + /** User did not accept the proactive command */ + USER_NOT_ACCEPT(0x22), + + /** User cleared down call before connection or network release */ + USER_CLEAR_DOWN_CALL(0x23), + + /** Action in contradiction with the current timer state */ + CONTRADICTION_WITH_TIMER(0x24), + + /** Interaction with call control by NAA, temporary problem */ + NAA_CALL_CONTROL_TEMPORARY(0x25), + + /** Launch browser generic error code */ + LAUNCH_BROWSER_ERROR(0x26), + + /** MMS temporary problem. */ + MMS_TEMPORARY(0x27), + + + /* + * Results '3X' indicate that it is not worth the UICC re-trying with an + * identical command, as it will only get the same response. However, the + * decision to retry lies with the application. + */ + + /** Command beyond terminal's capabilities */ + BEYOND_TERMINAL_CAPABILITY(0x30), + + /** Command type not understood by terminal */ + CMD_TYPE_NOT_UNDERSTOOD(0x31), + + /** Command data not understood by terminal */ + CMD_DATA_NOT_UNDERSTOOD(0x32), + + /** Command number not known by terminal */ + CMD_NUM_NOT_KNOWN(0x33), + + /** SS Return Error */ + SS_RETURN_ERROR(0x34), + + /** SMS RP-ERROR */ + SMS_RP_ERROR(0x35), + + /** Error, required values are missing */ + REQUIRED_VALUES_MISSING(0x36), + + /** USSD Return Error */ + USSD_RETURN_ERROR(0x37), + + /** MultipleCard commands error */ + MULTI_CARDS_CMD_ERROR(0x38), + + /** + * Interaction with call control by USIM or MO short message control by + * USIM, permanent problem + */ + USIM_CALL_CONTROL_PERMANENT(0x39), + + /** Bearer Independent Protocol error */ + BIP_ERROR(0x3a), + + /** Access Technology unable to process command */ + ACCESS_TECH_UNABLE_TO_PROCESS(0x3b), + + /** Frames error */ + FRAMES_ERROR(0x3c), + + /** MMS Error */ + MMS_ERROR(0x3d); + + + private int mCode; + + ResultCode(int code) { + mCode = code; + } + + /** + * Retrieves the actual result code that this object represents. + * @return Actual result code + */ + public int value() { + return mCode; + } + + public static ResultCode fromInt(int value) { + for (ResultCode r : ResultCode.values()) { + if (r.mCode == value) { + return r; + } + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cat/ResultException.java b/src/java/com/android/internal/telephony/cat/ResultException.java new file mode 100644 index 0000000..84879c2 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ResultException.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Class for errors in the Result object. + * + * {@hide} + */ +public class ResultException extends CatException { + private ResultCode mResult; + private int mAdditionalInfo; + private String mExplanation; + + public ResultException(ResultCode result) { + super(); + + // ETSI TS 102 223, 8.12 -- For the general results '20', '21', '26', + // '38', '39', '3A', '3C', and '3D', it is mandatory for the terminal + // to provide a specific cause value as additional information. + switch (result) { + case TERMINAL_CRNTLY_UNABLE_TO_PROCESS: // 0x20 + case NETWORK_CRNTLY_UNABLE_TO_PROCESS: // 0x21 + case LAUNCH_BROWSER_ERROR: // 0x26 + case MULTI_CARDS_CMD_ERROR: // 0x38 + case USIM_CALL_CONTROL_PERMANENT: // 0x39 + case BIP_ERROR: // 0x3a + case FRAMES_ERROR: // 0x3c + case MMS_ERROR: // 0x3d + throw new AssertionError( + "For result code, " + result + + ", additional information must be given!"); + } + + mResult = result; + mAdditionalInfo = -1; + mExplanation = ""; + } + + public ResultException(ResultCode result, String explanation) { + this(result); + mExplanation = explanation; + } + + public ResultException(ResultCode result, int additionalInfo) { + this(result); + + if (additionalInfo < 0) { + throw new AssertionError( + "Additional info must be greater than zero!"); + } + + mAdditionalInfo = additionalInfo; + } + + public ResultException(ResultCode result, int additionalInfo, String explanation) { + this(result, additionalInfo); + mExplanation = explanation; + } + + public ResultCode result() { + return mResult; + } + + public boolean hasAdditionalInfo() { + return mAdditionalInfo >= 0; + } + + public int additionalInfo() { + return mAdditionalInfo; + } + + public String explanation() { + return mExplanation; + } + + @Override + public String toString() { + return "result=" + mResult + " additionalInfo=" + mAdditionalInfo + + " explantion=" + mExplanation; + } +} diff --git a/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java b/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java new file mode 100644 index 0000000..fb33a8e --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/RilMessageDecoder.java @@ -0,0 +1,177 @@ +/* + * 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 com.android.internal.telephony.cat; + +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccUtils; + +import android.os.Handler; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import android.os.Message; + +/** + * Class used for queuing raw ril messages, decoding them into CommanParams + * objects and sending the result back to the CAT Service. + */ +class RilMessageDecoder extends StateMachine { + + // constants + private static final int CMD_START = 1; + private static final int CMD_PARAMS_READY = 2; + + // members + private static RilMessageDecoder sInstance = null; + private CommandParamsFactory mCmdParamsFactory = null; + private RilMessage mCurrentRilMessage = null; + private Handler mCaller = null; + + // States + private StateStart mStateStart = new StateStart(); + private StateCmdParamsReady mStateCmdParamsReady = new StateCmdParamsReady(); + + /** + * Get the singleton instance, constructing if necessary. + * + * @param caller + * @param fh + * @return RilMesssageDecoder + */ + public static synchronized RilMessageDecoder getInstance(Handler caller, IccFileHandler fh) { + if (sInstance == null) { + sInstance = new RilMessageDecoder(caller, fh); + sInstance.start(); + } + return sInstance; + } + + /** + * Start decoding the message parameters, + * when complete MSG_ID_RIL_MSG_DECODED will be returned to caller. + * + * @param rilMsg + */ + public void sendStartDecodingMessageParams(RilMessage rilMsg) { + Message msg = obtainMessage(CMD_START); + msg.obj = rilMsg; + sendMessage(msg); + } + + /** + * The command parameters have been decoded. + * + * @param resCode + * @param cmdParams + */ + public void sendMsgParamsDecoded(ResultCode resCode, CommandParams cmdParams) { + Message msg = obtainMessage(RilMessageDecoder.CMD_PARAMS_READY); + msg.arg1 = resCode.value(); + msg.obj = cmdParams; + sendMessage(msg); + } + + private void sendCmdForExecution(RilMessage rilMsg) { + Message msg = mCaller.obtainMessage(CatService.MSG_ID_RIL_MSG_DECODED, + new RilMessage(rilMsg)); + msg.sendToTarget(); + } + + private RilMessageDecoder(Handler caller, IccFileHandler fh) { + super("RilMessageDecoder"); + + addState(mStateStart); + addState(mStateCmdParamsReady); + setInitialState(mStateStart); + + mCaller = caller; + mCmdParamsFactory = CommandParamsFactory.getInstance(this, fh); + } + + private class StateStart extends State { + @Override + public boolean processMessage(Message msg) { + if (msg.what == CMD_START) { + if (decodeMessageParams((RilMessage)msg.obj)) { + transitionTo(mStateCmdParamsReady); + } + } else { + CatLog.d(this, "StateStart unexpected expecting START=" + + CMD_START + " got " + msg.what); + } + return true; + } + } + + private class StateCmdParamsReady extends State { + @Override + public boolean processMessage(Message msg) { + if (msg.what == CMD_PARAMS_READY) { + mCurrentRilMessage.mResCode = ResultCode.fromInt(msg.arg1); + mCurrentRilMessage.mData = msg.obj; + sendCmdForExecution(mCurrentRilMessage); + transitionTo(mStateStart); + } else { + CatLog.d(this, "StateCmdParamsReady expecting CMD_PARAMS_READY=" + + CMD_PARAMS_READY + " got " + msg.what); + deferMessage(msg); + } + return true; + } + } + + private boolean decodeMessageParams(RilMessage rilMsg) { + boolean decodingStarted; + + mCurrentRilMessage = rilMsg; + switch(rilMsg.mId) { + case CatService.MSG_ID_SESSION_END: + case CatService.MSG_ID_CALL_SETUP: + mCurrentRilMessage.mResCode = ResultCode.OK; + sendCmdForExecution(mCurrentRilMessage); + decodingStarted = false; + break; + case CatService.MSG_ID_PROACTIVE_COMMAND: + case CatService.MSG_ID_EVENT_NOTIFY: + case CatService.MSG_ID_REFRESH: + byte[] rawData = null; + try { + rawData = IccUtils.hexStringToBytes((String) rilMsg.mData); + } catch (Exception e) { + // zombie messages are dropped + CatLog.d(this, "decodeMessageParams dropping zombie messages"); + decodingStarted = false; + break; + } + try { + // Start asynch parsing of the command parameters. + mCmdParamsFactory.make(BerTlv.decode(rawData)); + decodingStarted = true; + } catch (ResultException e) { + // send to Service for proper RIL communication. + CatLog.d(this, "decodeMessageParams: caught ResultException e=" + e); + mCurrentRilMessage.mResCode = e.result(); + sendCmdForExecution(mCurrentRilMessage); + decodingStarted = false; + } + break; + default: + decodingStarted = false; + break; + } + return decodingStarted; + } +} diff --git a/src/java/com/android/internal/telephony/cat/TextAlignment.java b/src/java/com/android/internal/telephony/cat/TextAlignment.java new file mode 100644 index 0000000..7fb58a5 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/TextAlignment.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Enumeration for representing text alignment. + * + * {@hide} + */ +public enum TextAlignment { + LEFT(0x0), + CENTER(0x1), + RIGHT(0x2), + /** Language dependent (default) */ + DEFAULT(0x3); + + private int mValue; + + TextAlignment(int value) { + mValue = value; + } + + /** + * Create a TextAlignment object. + * @param value Integer value to be converted to a TextAlignment object. + * @return TextAlignment object whose value is {@code value}. If no + * TextAlignment object has that value, null is returned. + */ + public static TextAlignment fromInt(int value) { + for (TextAlignment e : TextAlignment.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cat/TextAttribute.java b/src/java/com/android/internal/telephony/cat/TextAttribute.java new file mode 100644 index 0000000..0dea640 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/TextAttribute.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Class for representing text attributes for SIM Toolkit. + * + * {@hide} + */ +public class TextAttribute { + public int start; + public int length; + public TextAlignment align; + public FontSize size; + public boolean bold; + public boolean italic; + public boolean underlined; + public boolean strikeThrough; + public TextColor color; + + public TextAttribute(int start, int length, TextAlignment align, + FontSize size, boolean bold, boolean italic, boolean underlined, + boolean strikeThrough, TextColor color) { + this.start = start; + this.length = length; + this.align = align; + this.size = size; + this.bold = bold; + this.italic = italic; + this.underlined = underlined; + this.strikeThrough = strikeThrough; + this.color = color; + } +} diff --git a/src/java/com/android/internal/telephony/cat/TextColor.java b/src/java/com/android/internal/telephony/cat/TextColor.java new file mode 100644 index 0000000..6447e74 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/TextColor.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + + +/** + * Enumeration for representing text color. + * + * {@hide} + */ +public enum TextColor { + BLACK(0x0), + DARK_GRAY(0x1), + DARK_RED(0x2), + DARK_YELLOW(0x3), + DARK_GREEN(0x4), + DARK_CYAN(0x5), + DARK_BLUE(0x6), + DARK_MAGENTA(0x7), + GRAY(0x8), + WHITE(0x9), + BRIGHT_RED(0xa), + BRIGHT_YELLOW(0xb), + BRIGHT_GREEN(0xc), + BRIGHT_CYAN(0xd), + BRIGHT_BLUE(0xe), + BRIGHT_MAGENTA(0xf); + + private int mValue; + + TextColor(int value) { + mValue = value; + } + + /** + * Create a TextColor object. + * @param value Integer value to be converted to a TextColor object. + * @return TextColor object whose value is {@code value}. If no TextColor + * object has that value, null is returned. + */ + public static TextColor fromInt(int value) { + for (TextColor e : TextColor.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cat/TextMessage.java b/src/java/com/android/internal/telephony/cat/TextMessage.java new file mode 100644 index 0000000..5ffd076 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/TextMessage.java @@ -0,0 +1,71 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +public class TextMessage implements Parcelable { + public String title = ""; + public String text = null; + public Bitmap icon = null; + public boolean iconSelfExplanatory = false; + public boolean isHighPriority = false; + public boolean responseNeeded = true; + public boolean userClear = false; + public Duration duration = null; + + TextMessage() { + } + + private TextMessage(Parcel in) { + title = in.readString(); + text = in.readString(); + icon = in.readParcelable(null); + iconSelfExplanatory = in.readInt() == 1 ? true : false; + isHighPriority = in.readInt() == 1 ? true : false; + responseNeeded = in.readInt() == 1 ? true : false; + userClear = in.readInt() == 1 ? true : false; + duration = in.readParcelable(null); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(title); + dest.writeString(text); + dest.writeParcelable(icon, 0); + dest.writeInt(iconSelfExplanatory ? 1 : 0); + dest.writeInt(isHighPriority ? 1 : 0); + dest.writeInt(responseNeeded ? 1 : 0); + dest.writeInt(userClear ? 1 : 0); + dest.writeParcelable(duration, 0); + } + + public static final Parcelable.Creator<TextMessage> CREATOR = new Parcelable.Creator<TextMessage>() { + public TextMessage createFromParcel(Parcel in) { + return new TextMessage(in); + } + + public TextMessage[] newArray(int size) { + return new TextMessage[size]; + } + }; +}
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/cat/Tone.java b/src/java/com/android/internal/telephony/cat/Tone.java new file mode 100644 index 0000000..27b4489 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/Tone.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cat; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Enumeration for representing the tone values for use with PLAY TONE + * proactive commands. + * + * {@hide} + */ +public enum Tone implements Parcelable { + // Standard supervisory tones + + /** + * Dial tone. + */ + DIAL(0x01), + + /** + * Called subscriber busy. + */ + BUSY(0x02), + + /** + * Congestion. + */ + CONGESTION(0x03), + + /** + * Radio path acknowledge. + */ + RADIO_PATH_ACK(0x04), + + /** + * Radio path not available / Call dropped. + */ + RADIO_PATH_NOT_AVAILABLE(0x05), + + /** + * Error/Special information. + */ + ERROR_SPECIAL_INFO(0x06), + + /** + * Call waiting tone. + */ + CALL_WAITING(0x07), + + /** + * Ringing tone. + */ + RINGING(0x08), + + // Terminal proprietary tones + + /** + * General beep. + */ + GENERAL_BEEP(0x10), + + /** + * Positive acknowledgement tone. + */ + POSITIVE_ACK(0x11), + + /** + * Negative acknowledgement tone. + */ + NEGATIVE_ACK(0x12), + + /** + * Ringing tone as selected by the user for incoming speech call. + */ + INCOMING_SPEECH_CALL(0x13), + + /** + * Alert tone as selected by the user for incoming SMS. + */ + INCOMING_SMS(0x14), + + /** + * Critical alert. + * This tone is to be used in critical situations. The terminal shall make + * every effort to alert the user when this tone is indicated independent + * from the volume setting in the terminal. + */ + CRITICAL_ALERT(0x15), + + /** + * Vibrate only, if available. + */ + VIBRATE_ONLY(0x20), + + // Themed tones + + /** + * Happy tone. + */ + HAPPY(0x30), + + /** + * Sad tone. + */ + SAD(0x31), + + /** + * Urgent action tone. + */ + URGENT(0x32), + + /** + * Question tone. + */ + QUESTION(0x33), + + /** + * Message received tone. + */ + MESSAGE_RECEIVED(0x34), + + // Melody tones + MELODY_1(0x40), + MELODY_2(0x41), + MELODY_3(0x42), + MELODY_4(0x43), + MELODY_5(0x44), + MELODY_6(0x45), + MELODY_7(0x46), + MELODY_8(0x47); + + private int mValue; + + Tone(int value) { + mValue = value; + } + + /** + * Create a Tone object. + * @param value Integer value to be converted to a Tone object. + * @return Tone object whose value is {@code value}. If no Tone object has + * that value, null is returned. + */ + public static Tone fromInt(int value) { + for (Tone e : Tone.values()) { + if (e.mValue == value) { + return e; + } + } + return null; + } + + Tone(Parcel in) { + mValue = in.readInt(); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(ordinal()); + } + + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<Tone> CREATOR = new Parcelable.Creator<Tone>() { + public Tone createFromParcel(Parcel in) { + return Tone.values()[in.readInt()]; + } + + public Tone[] newArray(int size) { + return new Tone[size]; + } + }; +} diff --git a/src/java/com/android/internal/telephony/cat/ToneSettings.java b/src/java/com/android/internal/telephony/cat/ToneSettings.java new file mode 100644 index 0000000..6375afb --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ToneSettings.java @@ -0,0 +1,62 @@ +/* + * 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 com.android.internal.telephony.cat; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Container class for PlayTone commands parameters. + * + */ +public class ToneSettings implements Parcelable { + public Duration duration; + public Tone tone; + public boolean vibrate; + + public ToneSettings(Duration duration, Tone tone, boolean vibrate) { + this.duration = duration; + this.tone = tone; + this.vibrate = vibrate; + } + + private ToneSettings(Parcel in) { + duration = in.readParcelable(null); + tone = in.readParcelable(null); + vibrate = in.readInt() == 1; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(duration, 0); + dest.writeParcelable(tone, 0); + dest.writeInt(vibrate ? 1 : 0); + } + + public static final Parcelable.Creator<ToneSettings> CREATOR = new Parcelable.Creator<ToneSettings>() { + public ToneSettings createFromParcel(Parcel in) { + return new ToneSettings(in); + } + + public ToneSettings[] newArray(int size) { + return new ToneSettings[size]; + } + }; +}
\ No newline at end of file diff --git a/src/java/com/android/internal/telephony/cat/ValueParser.java b/src/java/com/android/internal/telephony/cat/ValueParser.java new file mode 100644 index 0000000..584d96c --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/ValueParser.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2006-2007 Google Inc. + * + * 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.internal.telephony.cat; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.cat.Duration.TimeUnit; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +abstract class ValueParser { + + /** + * Search for a Command Details object from a list. + * + * @param ctlvs List of ComprehensionTlv objects used for search + * @return An CtlvCommandDetails object found from the objects. If no + * Command Details object is found, ResultException is thrown. + * @throws ResultException + */ + static CommandDetails retrieveCommandDetails(ComprehensionTlv ctlv) + throws ResultException { + + CommandDetails cmdDet = new CommandDetails(); + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + try { + cmdDet.compRequired = ctlv.isComprehensionRequired(); + cmdDet.commandNumber = rawValue[valueIndex] & 0xff; + cmdDet.typeOfCommand = rawValue[valueIndex + 1] & 0xff; + cmdDet.commandQualifier = rawValue[valueIndex + 2] & 0xff; + return cmdDet; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } + + /** + * Search for a Device Identities object from a list. + * + * @param ctlvs List of ComprehensionTlv objects used for search + * @return An CtlvDeviceIdentities object found from the objects. If no + * Command Details object is found, ResultException is thrown. + * @throws ResultException + */ + static DeviceIdentities retrieveDeviceIdentities(ComprehensionTlv ctlv) + throws ResultException { + + DeviceIdentities devIds = new DeviceIdentities(); + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + try { + devIds.sourceId = rawValue[valueIndex] & 0xff; + devIds.destinationId = rawValue[valueIndex + 1] & 0xff; + return devIds; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.REQUIRED_VALUES_MISSING); + } + } + + /** + * Retrieves Duration information from the Duration COMPREHENSION-TLV + * object. + * + * @param ctlv A Text Attribute COMPREHENSION-TLV object + * @return A Duration object + * @throws ResultException + */ + static Duration retrieveDuration(ComprehensionTlv ctlv) throws ResultException { + int timeInterval = 0; + TimeUnit timeUnit = TimeUnit.SECOND; + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + + try { + timeUnit = TimeUnit.values()[(rawValue[valueIndex] & 0xff)]; + timeInterval = rawValue[valueIndex + 1] & 0xff; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + return new Duration(timeInterval, timeUnit); + } + + /** + * Retrieves Item information from the COMPREHENSION-TLV object. + * + * @param ctlv A Text Attribute COMPREHENSION-TLV object + * @return An Item + * @throws ResultException + */ + static Item retrieveItem(ComprehensionTlv ctlv) throws ResultException { + Item item = null; + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int length = ctlv.getLength(); + + if (length != 0) { + int textLen = length - 1; + + try { + int id = rawValue[valueIndex] & 0xff; + String text = IccUtils.adnStringFieldToString(rawValue, + valueIndex + 1, textLen); + item = new Item(id, text); + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } + + return item; + } + + /** + * Retrieves Item id information from the COMPREHENSION-TLV object. + * + * @param ctlv A Text Attribute COMPREHENSION-TLV object + * @return An Item id + * @throws ResultException + */ + static int retrieveItemId(ComprehensionTlv ctlv) throws ResultException { + int id = 0; + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + + try { + id = rawValue[valueIndex] & 0xff; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + + return id; + } + + /** + * Retrieves icon id from an Icon Identifier COMPREHENSION-TLV object + * + * @param ctlv An Icon Identifier COMPREHENSION-TLV object + * @return IconId instance + * @throws ResultException + */ + static IconId retrieveIconId(ComprehensionTlv ctlv) throws ResultException { + IconId id = new IconId(); + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + try { + id.selfExplanatory = (rawValue[valueIndex++] & 0xff) == 0x00; + id.recordNumber = rawValue[valueIndex] & 0xff; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + + return id; + } + + /** + * Retrieves item icons id from an Icon Identifier List COMPREHENSION-TLV + * object + * + * @param ctlv An Item Icon List Identifier COMPREHENSION-TLV object + * @return ItemsIconId instance + * @throws ResultException + */ + static ItemsIconId retrieveItemsIconId(ComprehensionTlv ctlv) + throws ResultException { + CatLog.d("ValueParser", "retrieveItemsIconId:"); + ItemsIconId id = new ItemsIconId(); + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int numOfItems = ctlv.getLength() - 1; + id.recordNumbers = new int[numOfItems]; + + try { + // get icon self-explanatory + id.selfExplanatory = (rawValue[valueIndex++] & 0xff) == 0x00; + + for (int index = 0; index < numOfItems;) { + id.recordNumbers[index++] = rawValue[valueIndex++]; + } + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + return id; + } + + /** + * Retrieves text attribute information from the Text Attribute + * COMPREHENSION-TLV object. + * + * @param ctlv A Text Attribute COMPREHENSION-TLV object + * @return A list of TextAttribute objects + * @throws ResultException + */ + static List<TextAttribute> retrieveTextAttribute(ComprehensionTlv ctlv) + throws ResultException { + ArrayList<TextAttribute> lst = new ArrayList<TextAttribute>(); + + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int length = ctlv.getLength(); + + if (length != 0) { + // Each attribute is consisted of four bytes + int itemCount = length / 4; + + try { + for (int i = 0; i < itemCount; i++, valueIndex += 4) { + int start = rawValue[valueIndex] & 0xff; + int textLength = rawValue[valueIndex + 1] & 0xff; + int format = rawValue[valueIndex + 2] & 0xff; + int colorValue = rawValue[valueIndex + 3] & 0xff; + + int alignValue = format & 0x03; + TextAlignment align = TextAlignment.fromInt(alignValue); + + int sizeValue = (format >> 2) & 0x03; + FontSize size = FontSize.fromInt(sizeValue); + if (size == null) { + // Font size value is not defined. Use default. + size = FontSize.NORMAL; + } + + boolean bold = (format & 0x10) != 0; + boolean italic = (format & 0x20) != 0; + boolean underlined = (format & 0x40) != 0; + boolean strikeThrough = (format & 0x80) != 0; + + TextColor color = TextColor.fromInt(colorValue); + + TextAttribute attr = new TextAttribute(start, textLength, + align, size, bold, italic, underlined, + strikeThrough, color); + lst.add(attr); + } + + return lst; + + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } + return null; + } + + + /** + * Retrieves alpha identifier from an Alpha Identifier COMPREHENSION-TLV + * object. + * + * @param ctlv An Alpha Identifier COMPREHENSION-TLV object + * @return String corresponding to the alpha identifier + * @throws ResultException + */ + static String retrieveAlphaId(ComprehensionTlv ctlv) throws ResultException { + + if (ctlv != null) { + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + int length = ctlv.getLength(); + if (length != 0) { + try { + return IccUtils.adnStringFieldToString(rawValue, valueIndex, + length); + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } else { + return CatService.STK_DEFAULT; + } + } else { + return CatService.STK_DEFAULT; + } + } + + /** + * Retrieves text from the Text COMPREHENSION-TLV object, and decodes it + * into a Java String. + * + * @param ctlv A Text COMPREHENSION-TLV object + * @return A Java String object decoded from the Text object + * @throws ResultException + */ + static String retrieveTextString(ComprehensionTlv ctlv) throws ResultException { + byte[] rawValue = ctlv.getRawValue(); + int valueIndex = ctlv.getValueIndex(); + byte codingScheme = 0x00; + String text = null; + int textLen = ctlv.getLength(); + + // In case the text length is 0, return a null string. + if (textLen == 0) { + return text; + } else { + // one byte is coding scheme + textLen -= 1; + } + + try { + codingScheme = (byte) (rawValue[valueIndex] & 0x0c); + + if (codingScheme == 0x00) { // GSM 7-bit packed + text = GsmAlphabet.gsm7BitPackedToString(rawValue, + valueIndex + 1, (textLen * 8) / 7); + } else if (codingScheme == 0x04) { // GSM 8-bit unpacked + text = GsmAlphabet.gsm8BitUnpackedToString(rawValue, + valueIndex + 1, textLen); + } else if (codingScheme == 0x08) { // UCS2 + text = new String(rawValue, valueIndex + 1, textLen, "UTF-16"); + } else { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + + return text; + } catch (IndexOutOfBoundsException e) { + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } catch (UnsupportedEncodingException e) { + // This should never happen. + throw new ResultException(ResultCode.CMD_DATA_NOT_UNDERSTOOD); + } + } +} diff --git a/src/java/com/android/internal/telephony/cat/package.html b/src/java/com/android/internal/telephony/cat/package.html new file mode 100644 index 0000000..5b6bfc6 --- /dev/null +++ b/src/java/com/android/internal/telephony/cat/package.html @@ -0,0 +1,5 @@ +<HTML> +<BODY> +Provides classes for ICC Toolkit Service (CAT). +</BODY> +</HTML> diff --git a/src/java/com/android/internal/telephony/cdma/CDMALTEPhone.java b/src/java/com/android/internal/telephony/cdma/CDMALTEPhone.java new file mode 100644 index 0000000..df42515 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CDMALTEPhone.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2011 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.internal.telephony.cdma; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Message; +import android.preference.PreferenceManager; +import android.provider.Telephony; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.OperatorInfo; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneNotifier; +import com.android.internal.telephony.PhoneProxy; +import com.android.internal.telephony.SMSDispatcher; +import com.android.internal.telephony.gsm.GsmSMSDispatcher; +import com.android.internal.telephony.gsm.SmsMessage; +import com.android.internal.telephony.ims.IsimRecords; +import com.android.internal.telephony.uicc.UiccController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +public class CDMALTEPhone extends CDMAPhone { + static final String LOG_TAG = "CDMA"; + + private static final boolean DBG = true; + + /** Secondary SMSDispatcher for 3GPP format messages. */ + SMSDispatcher m3gppSMS; + + /** + * Small container class used to hold information relevant to + * the carrier selection process. operatorNumeric can be "" + * if we are looking for automatic selection. operatorAlphaLong is the + * corresponding operator name. + */ + private static class NetworkSelectMessage { + public Message message; + public String operatorNumeric; + public String operatorAlphaLong; + } + + // Constructors + public CDMALTEPhone(Context context, CommandsInterface ci, PhoneNotifier notifier) { + super(context, ci, notifier, false); + m3gppSMS = new GsmSMSDispatcher(this, mSmsStorageMonitor, mSmsUsageMonitor); + mIccRecords.registerForNewSms(this, EVENT_NEW_ICC_SMS, null); + } + + @Override + public void handleMessage (Message msg) { + AsyncResult ar; + switch (msg.what) { + // handle the select network completion callbacks. + case EVENT_SET_NETWORK_MANUAL_COMPLETE: + handleSetSelectNetwork((AsyncResult) msg.obj); + break; + case EVENT_NEW_ICC_SMS: + ar = (AsyncResult)msg.obj; + m3gppSMS.dispatchMessage((SmsMessage)ar.result); + break; + default: + super.handleMessage(msg); + } + } + + @Override + protected void initSstIcc() { + mIccCard.set(UiccController.getInstance(this).getIccCard()); + mIccRecords = mIccCard.get().getIccRecords(); + // CdmaLteServiceStateTracker registers with IccCard to know + // when the card is ready. So create mIccCard before the ServiceStateTracker + mSST = new CdmaLteServiceStateTracker(this); + } + + @Override + public void dispose() { + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + super.dispose(); + m3gppSMS.dispose(); + mIccRecords.unregisterForNewSms(this); + } + } + + @Override + public void removeReferences() { + super.removeReferences(); + m3gppSMS = null; + } + + @Override + public PhoneConstants.DataState getDataConnectionState(String apnType) { + PhoneConstants.DataState ret = PhoneConstants.DataState.DISCONNECTED; + + if (mSST == null) { + // Radio Technology Change is ongoing, dispose() and + // removeReferences() have already been called + + ret = PhoneConstants.DataState.DISCONNECTED; + } else if (mDataConnectionTracker.isApnTypeEnabled(apnType) == false) { + ret = PhoneConstants.DataState.DISCONNECTED; + } else { + switch (mDataConnectionTracker.getState(apnType)) { + case FAILED: + case IDLE: + ret = PhoneConstants.DataState.DISCONNECTED; + break; + + case CONNECTED: + case DISCONNECTING: + if (mCT.state != PhoneConstants.State.IDLE && + !mSST.isConcurrentVoiceAndDataAllowed()) { + ret = PhoneConstants.DataState.SUSPENDED; + } else { + ret = PhoneConstants.DataState.CONNECTED; + } + break; + + case INITING: + case CONNECTING: + case SCANNING: + ret = PhoneConstants.DataState.CONNECTING; + break; + } + } + + log("getDataConnectionState apnType=" + apnType + " ret=" + ret); + return ret; + } + + @Override + public void + selectNetworkManually(OperatorInfo network, + Message response) { + // wrap the response message in our own message along with + // the operator's id. + NetworkSelectMessage nsm = new NetworkSelectMessage(); + nsm.message = response; + nsm.operatorNumeric = network.getOperatorNumeric(); + nsm.operatorAlphaLong = network.getOperatorAlphaLong(); + + // get the message + Message msg = obtainMessage(EVENT_SET_NETWORK_MANUAL_COMPLETE, nsm); + + mCM.setNetworkSelectionModeManual(network.getOperatorNumeric(), msg); + } + + /** + * Used to track the settings upon completion of the network change. + */ + private void handleSetSelectNetwork(AsyncResult ar) { + // look for our wrapper within the asyncresult, skip the rest if it + // is null. + if (!(ar.userObj instanceof NetworkSelectMessage)) { + Log.e(LOG_TAG, "unexpected result from user object."); + return; + } + + NetworkSelectMessage nsm = (NetworkSelectMessage) ar.userObj; + + // found the object, now we send off the message we had originally + // attached to the request. + if (nsm.message != null) { + if (DBG) log("sending original message to recipient"); + AsyncResult.forMessage(nsm.message, ar.result, ar.exception); + nsm.message.sendToTarget(); + } + + // open the shared preferences editor, and write the value. + // nsm.operatorNumeric is "" if we're in automatic.selection. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(NETWORK_SELECTION_KEY, nsm.operatorNumeric); + editor.putString(NETWORK_SELECTION_NAME_KEY, nsm.operatorAlphaLong); + + // commit and log the result. + if (! editor.commit()) { + Log.e(LOG_TAG, "failed to commit network selection preference"); + } + + } + + @Override + public boolean updateCurrentCarrierInProvider() { + if (mIccRecords != null) { + try { + Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current"); + ContentValues map = new ContentValues(); + String operatorNumeric = mIccRecords.getOperatorNumeric(); + map.put(Telephony.Carriers.NUMERIC, operatorNumeric); + if (DBG) log("updateCurrentCarrierInProvider from UICC: numeric=" + + operatorNumeric); + mContext.getContentResolver().insert(uri, map); + return true; + } catch (SQLException e) { + Log.e(LOG_TAG, "[CDMALTEPhone] Can't store current operator ret false", e); + } + } else { + if (DBG) log("updateCurrentCarrierInProvider mIccRecords == null ret false"); + } + return false; + } + + // return IMSI from USIM as subscriber ID. + @Override + public String getSubscriberId() { + return mIccRecords.getIMSI(); + } + + @Override + public String getImei() { + return mImei; + } + + @Override + public String getDeviceSvn() { + return mImeiSv; + } + + @Override + public IsimRecords getIsimRecords() { + return mIccRecords.getIsimRecords(); + } + + @Override + public String getMsisdn() { + return mIccRecords.getMsisdnNumber(); + } + + @Override + public void getAvailableNetworks(Message response) { + mCM.getAvailableNetworks(response); + } + + @Override + public void requestIsimAuthentication(String nonce, Message result) { + mCM.requestIsimAuthentication(nonce, result); + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[CDMALTEPhone] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CDMALTEPhone extends:"); + super.dump(fd, pw, args); + pw.println(" m3gppSMS=" + m3gppSMS); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CDMAPhone.java b/src/java/com/android/internal/telephony/cdma/CDMAPhone.java new file mode 100755 index 0000000..26ef9dc --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CDMAPhone.java @@ -0,0 +1,1508 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import android.app.ActivityManagerNative; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.provider.Telephony; +import android.telephony.CellLocation; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.CallTracker; +import com.android.internal.telephony.CommandException; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccException; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccPhoneBookInterfaceManager; +import com.android.internal.telephony.IccSmsInterfaceManager; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.MmiCode; +import com.android.internal.telephony.OperatorInfo; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneNotifier; +import com.android.internal.telephony.PhoneProxy; +import com.android.internal.telephony.PhoneSubInfo; +import com.android.internal.telephony.ServiceStateTracker; +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.UUSInfo; +import com.android.internal.telephony.cat.CatService; +import com.android.internal.telephony.uicc.UiccController; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ISO_COUNTRY; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC; + +/** + * {@hide} + */ +public class CDMAPhone extends PhoneBase { + static final String LOG_TAG = "CDMA"; + private static final boolean DBG = true; + private static final boolean VDBG = false; /* STOP SHIP if true */ + + // Default Emergency Callback Mode exit timer + private static final int DEFAULT_ECM_EXIT_TIMER_VALUE = 300000; + + static final String VM_COUNT_CDMA = "vm_count_key_cdma"; + private static final String VM_NUMBER_CDMA = "vm_number_key_cdma"; + private String mVmNumber = null; + + static final int RESTART_ECM_TIMER = 0; // restart Ecm timer + static final int CANCEL_ECM_TIMER = 1; // cancel Ecm timer + + // Instance Variables + CdmaCallTracker mCT; + CdmaServiceStateTracker mSST; + CdmaSubscriptionSourceManager mCdmaSSM; + ArrayList <CdmaMmiCode> mPendingMmis = new ArrayList<CdmaMmiCode>(); + RuimPhoneBookInterfaceManager mRuimPhoneBookInterfaceManager; + RuimSmsInterfaceManager mRuimSmsInterfaceManager; + int mCdmaSubscriptionSource = CdmaSubscriptionSourceManager.SUBSCRIPTION_SOURCE_UNKNOWN; + PhoneSubInfo mSubInfo; + EriManager mEriManager; + WakeLock mWakeLock; + + // mEriFileLoadedRegistrants are informed after the ERI text has been loaded + private final RegistrantList mEriFileLoadedRegistrants = new RegistrantList(); + + // mEcmTimerResetRegistrants are informed after Ecm timer is canceled or re-started + private final RegistrantList mEcmTimerResetRegistrants = new RegistrantList(); + + // mEcmExitRespRegistrant is informed after the phone has been exited + //the emergency callback mode + //keep track of if phone is in emergency callback mode + private boolean mIsPhoneInEcmState; + private Registrant mEcmExitRespRegistrant; + protected String mImei; + protected String mImeiSv; + private String mEsn; + private String mMeid; + // string to define how the carrier specifies its own ota sp number + private String mCarrierOtaSpNumSchema; + + // A runnable which is used to automatically exit from Ecm after a period of time. + private Runnable mExitEcmRunnable = new Runnable() { + @Override + public void run() { + exitEmergencyCallbackMode(); + } + }; + + Registrant mPostDialHandler; + + static String PROPERTY_CDMA_HOME_OPERATOR_NUMERIC = "ro.cdma.home.operator.numeric"; + + // Constructors + public CDMAPhone(Context context, CommandsInterface ci, PhoneNotifier notifier) { + super(notifier, context, ci, false); + initSstIcc(); + init(context, notifier); + } + + public CDMAPhone(Context context, CommandsInterface ci, PhoneNotifier notifier, + boolean unitTestMode) { + super(notifier, context, ci, unitTestMode); + initSstIcc(); + init(context, notifier); + } + + protected void initSstIcc() { + mIccCard.set(UiccController.getInstance(this).getIccCard()); + mIccRecords = mIccCard.get().getIccRecords(); + // CdmaServiceStateTracker registers with IccCard to know + // when the Ruim card is ready. So create mIccCard before the ServiceStateTracker + mSST = new CdmaServiceStateTracker(this); + } + + protected void init(Context context, PhoneNotifier notifier) { + mCM.setPhoneType(PhoneConstants.PHONE_TYPE_CDMA); + mCT = new CdmaCallTracker(this); + mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(context, mCM, this, + EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null); + mSMS = new CdmaSMSDispatcher(this, mSmsStorageMonitor, mSmsUsageMonitor); + mDataConnectionTracker = new CdmaDataConnectionTracker (this); + mRuimPhoneBookInterfaceManager = new RuimPhoneBookInterfaceManager(this); + mRuimSmsInterfaceManager = new RuimSmsInterfaceManager(this, mSMS); + mSubInfo = new PhoneSubInfo(this); + mEriManager = new EriManager(this, context, EriManager.ERI_FROM_XML); + + mCM.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null); + registerForRuimRecordEvents(); + mCM.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + mCM.registerForOn(this, EVENT_RADIO_ON, null); + mCM.setOnSuppServiceNotification(this, EVENT_SSN, null); + mSST.registerForNetworkAttached(this, EVENT_REGISTERED_TO_NETWORK, null); + mCM.setEmergencyCallbackMode(this, EVENT_EMERGENCY_CALLBACK_MODE_ENTER, null); + + PowerManager pm + = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,LOG_TAG); + + //Change the system setting + SystemProperties.set(TelephonyProperties.CURRENT_ACTIVE_PHONE, + Integer.toString(PhoneConstants.PHONE_TYPE_CDMA)); + + // This is needed to handle phone process crashes + String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + mIsPhoneInEcmState = inEcm.equals("true"); + if (mIsPhoneInEcmState) { + // Send a message which will invoke handleExitEmergencyCallbackMode + mCM.exitEmergencyCallbackMode(obtainMessage(EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE)); + } + + // get the string that specifies the carrier OTA Sp number + mCarrierOtaSpNumSchema = SystemProperties.get( + TelephonyProperties.PROPERTY_OTASP_NUM_SCHEMA,""); + + // Sets operator alpha property by retrieving from build-time system property + String operatorAlpha = SystemProperties.get("ro.cdma.home.operator.alpha"); + setSystemProperty(PROPERTY_ICC_OPERATOR_ALPHA, operatorAlpha); + + // Sets operator numeric property by retrieving from build-time system property + String operatorNumeric = SystemProperties.get(PROPERTY_CDMA_HOME_OPERATOR_NUMERIC); + log("CDMAPhone: init set 'gsm.sim.operator.numeric' to operator='" + + operatorNumeric + "'"); + setSystemProperty(PROPERTY_ICC_OPERATOR_NUMERIC, operatorNumeric); + + // Sets iso country property by retrieving from build-time system property + setIsoCountryProperty(operatorNumeric); + + // Sets current entry in the telephony carrier table + updateCurrentCarrierInProvider(operatorNumeric); + + // Notify voicemails. + notifier.notifyMessageWaitingChanged(this); + } + + @Override + public void dispose() { + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + super.dispose(); + log("dispose"); + + //Unregister from all former registered events + unregisterForRuimRecordEvents(); + mCM.unregisterForAvailable(this); //EVENT_RADIO_AVAILABLE + mCM.unregisterForOffOrNotAvailable(this); //EVENT_RADIO_OFF_OR_NOT_AVAILABLE + mCM.unregisterForOn(this); //EVENT_RADIO_ON + mSST.unregisterForNetworkAttached(this); //EVENT_REGISTERED_TO_NETWORK + mCM.unSetOnSuppServiceNotification(this); + removeCallbacks(mExitEcmRunnable); + + mPendingMmis.clear(); + + //Force all referenced classes to unregister their former registered events + mCT.dispose(); + mDataConnectionTracker.dispose(); + mSST.dispose(); + mCdmaSSM.dispose(this); + mSMS.dispose(); + mRuimPhoneBookInterfaceManager.dispose(); + mRuimSmsInterfaceManager.dispose(); + mSubInfo.dispose(); + mEriManager.dispose(); + } + } + + @Override + public void removeReferences() { + log("removeReferences"); + mRuimPhoneBookInterfaceManager = null; + mRuimSmsInterfaceManager = null; + mSubInfo = null; + mCT = null; + mSST = null; + mEriManager = null; + mExitEcmRunnable = null; + super.removeReferences(); + } + + @Override + protected void finalize() { + if(DBG) Log.d(LOG_TAG, "CDMAPhone finalized"); + if (mWakeLock.isHeld()) { + Log.e(LOG_TAG, "UNEXPECTED; mWakeLock is held when finalizing."); + mWakeLock.release(); + } + } + + public ServiceState getServiceState() { + return mSST.ss; + } + + public CallTracker getCallTracker() { + return mCT; + } + + public PhoneConstants.State getState() { + return mCT.state; + } + + public ServiceStateTracker getServiceStateTracker() { + return mSST; + } + + public String getPhoneName() { + return "CDMA"; + } + + public int getPhoneType() { + return PhoneConstants.PHONE_TYPE_CDMA; + } + + public boolean canTransfer() { + Log.e(LOG_TAG, "canTransfer: not possible in CDMA"); + return false; + } + + public CdmaCall getRingingCall() { + return mCT.ringingCall; + } + + public void setMute(boolean muted) { + mCT.setMute(muted); + } + + public boolean getMute() { + return mCT.getMute(); + } + + public void conference() throws CallStateException { + // three way calls in CDMA will be handled by feature codes + Log.e(LOG_TAG, "conference: not possible in CDMA"); + } + + public void enableEnhancedVoicePrivacy(boolean enable, Message onComplete) { + this.mCM.setPreferredVoicePrivacy(enable, onComplete); + } + + public void getEnhancedVoicePrivacy(Message onComplete) { + this.mCM.getPreferredVoicePrivacy(onComplete); + } + + public void clearDisconnected() { + mCT.clearDisconnected(); + } + + public DataActivityState getDataActivityState() { + DataActivityState ret = DataActivityState.NONE; + + if (mSST.getCurrentDataConnectionState() == ServiceState.STATE_IN_SERVICE) { + + switch (mDataConnectionTracker.getActivity()) { + case DATAIN: + ret = DataActivityState.DATAIN; + break; + + case DATAOUT: + ret = DataActivityState.DATAOUT; + break; + + case DATAINANDOUT: + ret = DataActivityState.DATAINANDOUT; + break; + + case DORMANT: + ret = DataActivityState.DORMANT; + break; + } + } + return ret; + } + + /*package*/ void + notifySignalStrength() { + mNotifier.notifySignalStrength(this); + } + + public Connection + dial (String dialString) throws CallStateException { + // Need to make sure dialString gets parsed properly + String newDialString = PhoneNumberUtils.stripSeparators(dialString); + return mCT.dial(newDialString); + } + + public Connection dial(String dialString, UUSInfo uusInfo) throws CallStateException { + throw new CallStateException("Sending UUS information NOT supported in CDMA!"); + } + + public SignalStrength getSignalStrength() { + return mSST.mSignalStrength; + } + + public boolean + getMessageWaitingIndicator() { + return (getVoiceMessageCount() > 0); + } + + public List<? extends MmiCode> + getPendingMmiCodes() { + return mPendingMmis; + } + + public void registerForSuppServiceNotification( + Handler h, int what, Object obj) { + Log.e(LOG_TAG, "method registerForSuppServiceNotification is NOT supported in CDMA!"); + } + + public CdmaCall getBackgroundCall() { + return mCT.backgroundCall; + } + + public boolean handleInCallMmiCommands(String dialString) { + Log.e(LOG_TAG, "method handleInCallMmiCommands is NOT supported in CDMA!"); + return false; + } + + boolean isInCall() { + CdmaCall.State foregroundCallState = getForegroundCall().getState(); + CdmaCall.State backgroundCallState = getBackgroundCall().getState(); + CdmaCall.State ringingCallState = getRingingCall().getState(); + + return (foregroundCallState.isAlive() || backgroundCallState.isAlive() || ringingCallState + .isAlive()); + } + + public void + setNetworkSelectionModeAutomatic(Message response) { + Log.e(LOG_TAG, "method setNetworkSelectionModeAutomatic is NOT supported in CDMA!"); + } + + public void unregisterForSuppServiceNotification(Handler h) { + Log.e(LOG_TAG, "method unregisterForSuppServiceNotification is NOT supported in CDMA!"); + } + + public void + acceptCall() throws CallStateException { + mCT.acceptCall(); + } + + public void + rejectCall() throws CallStateException { + mCT.rejectCall(); + } + + public void + switchHoldingAndActive() throws CallStateException { + mCT.switchWaitingOrHoldingAndActive(); + } + + public String getLine1Number() { + return mSST.getMdnNumber(); + } + + public String getCdmaPrlVersion(){ + return mSST.getPrlVersion(); + } + + public String getCdmaMin() { + return mSST.getCdmaMin(); + } + + public boolean isMinInfoReady() { + return mSST.isMinInfoReady(); + } + + public void getCallWaiting(Message onComplete) { + mCM.queryCallWaiting(CommandsInterface.SERVICE_CLASS_VOICE, onComplete); + } + + public void + setRadioPower(boolean power) { + mSST.setRadioPower(power); + } + + public String getEsn() { + return mEsn; + } + + public String getMeid() { + return mMeid; + } + + //returns MEID or ESN in CDMA + public String getDeviceId() { + String id = getMeid(); + if ((id == null) || id.matches("^0*$")) { + Log.d(LOG_TAG, "getDeviceId(): MEID is not initialized use ESN"); + id = getEsn(); + } + return id; + } + + public String getDeviceSvn() { + Log.d(LOG_TAG, "getDeviceSvn(): return 0"); + return "0"; + } + + public String getSubscriberId() { + return mSST.getImsi(); + } + + public String getImei() { + Log.e(LOG_TAG, "IMEI is not available in CDMA"); + return null; + } + + public boolean canConference() { + Log.e(LOG_TAG, "canConference: not possible in CDMA"); + return false; + } + + public CellLocation getCellLocation() { + return mSST.cellLoc; + } + + public CdmaCall getForegroundCall() { + return mCT.foregroundCall; + } + + public void + selectNetworkManually(OperatorInfo network, + Message response) { + Log.e(LOG_TAG, "selectNetworkManually: not possible in CDMA"); + } + + public void setOnPostDialCharacter(Handler h, int what, Object obj) { + mPostDialHandler = new Registrant(h, what, obj); + } + + public boolean handlePinMmi(String dialString) { + CdmaMmiCode mmi = CdmaMmiCode.newFromDialString(dialString, this); + + if (mmi == null) { + Log.e(LOG_TAG, "Mmi is NULL!"); + return false; + } else if (mmi.isPukCommand()) { + mPendingMmis.add(mmi); + mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null)); + mmi.processCode(); + return true; + } + Log.e(LOG_TAG, "Unrecognized mmi!"); + return false; + } + + /** + * Removes the given MMI from the pending list and notifies registrants that + * it is complete. + * + * @param mmi MMI that is done + */ + void onMMIDone(CdmaMmiCode mmi) { + /* + * Only notify complete if it's on the pending list. Otherwise, it's + * already been handled (eg, previously canceled). + */ + if (mPendingMmis.remove(mmi)) { + mMmiCompleteRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null)); + } + } + + public void setLine1Number(String alphaTag, String number, Message onComplete) { + Log.e(LOG_TAG, "setLine1Number: not possible in CDMA"); + } + + public void setCallWaiting(boolean enable, Message onComplete) { + Log.e(LOG_TAG, "method setCallWaiting is NOT supported in CDMA!"); + } + + public void updateServiceLocation() { + mSST.enableSingleLocationUpdate(); + } + + public void setDataRoamingEnabled(boolean enable) { + mDataConnectionTracker.setDataOnRoamingEnabled(enable); + } + + public void registerForCdmaOtaStatusChange(Handler h, int what, Object obj) { + mCM.registerForCdmaOtaProvision(h, what, obj); + } + + public void unregisterForCdmaOtaStatusChange(Handler h) { + mCM.unregisterForCdmaOtaProvision(h); + } + + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) { + mSST.registerForSubscriptionInfoReady(h, what, obj); + } + + public void unregisterForSubscriptionInfoReady(Handler h) { + mSST.unregisterForSubscriptionInfoReady(h); + } + + public void setOnEcbModeExitResponse(Handler h, int what, Object obj) { + mEcmExitRespRegistrant = new Registrant (h, what, obj); + } + + public void unsetOnEcbModeExitResponse(Handler h) { + mEcmExitRespRegistrant.clear(); + } + + public void registerForCallWaiting(Handler h, int what, Object obj) { + mCT.registerForCallWaiting(h, what, obj); + } + + public void unregisterForCallWaiting(Handler h) { + mCT.unregisterForCallWaiting(h); + } + + public void + getNeighboringCids(Message response) { + /* + * This is currently not implemented. At least as of June + * 2009, there is no neighbor cell information available for + * CDMA because some party is resisting making this + * information readily available. Consequently, calling this + * function can have no useful effect. This situation may + * (and hopefully will) change in the future. + */ + if (response != null) { + CommandException ce = new CommandException( + CommandException.Error.REQUEST_NOT_SUPPORTED); + AsyncResult.forMessage(response).exception = ce; + response.sendToTarget(); + } + } + + public PhoneConstants.DataState getDataConnectionState(String apnType) { + PhoneConstants.DataState ret = PhoneConstants.DataState.DISCONNECTED; + + if (mSST == null) { + // Radio Technology Change is ongoning, dispose() and removeReferences() have + // already been called + + ret = PhoneConstants.DataState.DISCONNECTED; + } else if (mSST.getCurrentDataConnectionState() != ServiceState.STATE_IN_SERVICE) { + // If we're out of service, open TCP sockets may still work + // but no data will flow + ret = PhoneConstants.DataState.DISCONNECTED; + } else if (mDataConnectionTracker.isApnTypeEnabled(apnType) == false || + mDataConnectionTracker.isApnTypeActive(apnType) == false) { + ret = PhoneConstants.DataState.DISCONNECTED; + } else { + switch (mDataConnectionTracker.getState(apnType)) { + case FAILED: + case IDLE: + ret = PhoneConstants.DataState.DISCONNECTED; + break; + + case CONNECTED: + case DISCONNECTING: + if ( mCT.state != PhoneConstants.State.IDLE + && !mSST.isConcurrentVoiceAndDataAllowed()) { + ret = PhoneConstants.DataState.SUSPENDED; + } else { + ret = PhoneConstants.DataState.CONNECTED; + } + break; + + case INITING: + case CONNECTING: + case SCANNING: + ret = PhoneConstants.DataState.CONNECTING; + break; + } + } + + log("getDataConnectionState apnType=" + apnType + " ret=" + ret); + return ret; + } + + public void sendUssdResponse(String ussdMessge) { + Log.e(LOG_TAG, "sendUssdResponse: not possible in CDMA"); + } + + public void sendDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "sendDtmf called with invalid character '" + c + "'"); + } else { + if (mCT.state == PhoneConstants.State.OFFHOOK) { + mCM.sendDtmf(c, null); + } + } + } + + public void startDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "startDtmf called with invalid character '" + c + "'"); + } else { + mCM.startDtmf(c, null); + } + } + + public void stopDtmf() { + mCM.stopDtmf(null); + } + + public void sendBurstDtmf(String dtmfString, int on, int off, Message onComplete) { + boolean check = true; + for (int itr = 0;itr < dtmfString.length(); itr++) { + if (!PhoneNumberUtils.is12Key(dtmfString.charAt(itr))) { + Log.e(LOG_TAG, + "sendDtmf called with invalid character '" + dtmfString.charAt(itr)+ "'"); + check = false; + break; + } + } + if ((mCT.state == PhoneConstants.State.OFFHOOK)&&(check)) { + mCM.sendBurstDtmf(dtmfString, on, off, onComplete); + } + } + + public void getAvailableNetworks(Message response) { + Log.e(LOG_TAG, "getAvailableNetworks: not possible in CDMA"); + } + + public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, Message onComplete) { + Log.e(LOG_TAG, "setOutgoingCallerIdDisplay: not possible in CDMA"); + } + + public void enableLocationUpdates() { + mSST.enableLocationUpdates(); + } + + public void disableLocationUpdates() { + mSST.disableLocationUpdates(); + } + + public void getDataCallList(Message response) { + mCM.getDataCallList(response); + } + + public boolean getDataRoamingEnabled() { + return mDataConnectionTracker.getDataOnRoamingEnabled(); + } + + public void setVoiceMailNumber(String alphaTag, + String voiceMailNumber, + Message onComplete) { + Message resp; + mVmNumber = voiceMailNumber; + resp = obtainMessage(EVENT_SET_VM_NUMBER_DONE, 0, 0, onComplete); + mIccRecords.setVoiceMailNumber(alphaTag, mVmNumber, resp); + } + + public String getVoiceMailNumber() { + String number = null; + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + // TODO: The default value of voicemail number should be read from a system property + + // Read platform settings for dynamic voicemail number + if (getContext().getResources().getBoolean(com.android.internal + .R.bool.config_telephony_use_own_number_for_voicemail)) { + number = sp.getString(VM_NUMBER_CDMA, getLine1Number()); + } else { + number = sp.getString(VM_NUMBER_CDMA, "*86"); + } + return number; + } + + /* Returns Number of Voicemails + * @hide + */ + public int getVoiceMessageCount() { + int voicemailCount = mIccRecords.getVoiceMessageCount(); + // If mRuimRecords.getVoiceMessageCount returns zero, then there is possibility + // that phone was power cycled and would have lost the voicemail count. + // So get the count from preferences. + if (voicemailCount == 0) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + voicemailCount = sp.getInt(VM_COUNT_CDMA, 0); + } + return voicemailCount; + } + + public String getVoiceMailAlphaTag() { + // TODO: Where can we get this value has to be clarified with QC. + String ret = "";//TODO: Remove = "", if we know where to get this value. + + //ret = mSIMRecords.getVoiceMailAlphaTag(); + + if (ret == null || ret.length() == 0) { + return mContext.getText( + com.android.internal.R.string.defaultVoiceMailAlphaTag).toString(); + } + + return ret; + } + + public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) { + Log.e(LOG_TAG, "getCallForwardingOption: not possible in CDMA"); + } + + public void setCallForwardingOption(int commandInterfaceCFAction, + int commandInterfaceCFReason, + String dialingNumber, + int timerSeconds, + Message onComplete) { + Log.e(LOG_TAG, "setCallForwardingOption: not possible in CDMA"); + } + + public void + getOutgoingCallerIdDisplay(Message onComplete) { + Log.e(LOG_TAG, "getOutgoingCallerIdDisplay: not possible in CDMA"); + } + + public boolean + getCallForwardingIndicator() { + Log.e(LOG_TAG, "getCallForwardingIndicator: not possible in CDMA"); + return false; + } + + public void explicitCallTransfer() { + Log.e(LOG_TAG, "explicitCallTransfer: not possible in CDMA"); + } + + public String getLine1AlphaTag() { + Log.e(LOG_TAG, "getLine1AlphaTag: not possible in CDMA"); + return null; + } + + /** + * Notify any interested party of a Phone state change {@link PhoneConstants.State} + */ + /*package*/ void notifyPhoneStateChanged() { + mNotifier.notifyPhoneState(this); + } + + /** + * Notify registrants of a change in the call state. This notifies changes in {@link Call.State} + * Use this when changes in the precise call state are needed, else use notifyPhoneStateChanged. + */ + /*package*/ void notifyPreciseCallStateChanged() { + /* we'd love it if this was package-scoped*/ + super.notifyPreciseCallStateChangedP(); + } + + void notifyServiceStateChanged(ServiceState ss) { + super.notifyServiceStateChangedP(ss); + } + + void notifyLocationChanged() { + mNotifier.notifyCellLocation(this); + } + + /*package*/ void notifyNewRingingConnection(Connection c) { + /* we'd love it if this was package-scoped*/ + super.notifyNewRingingConnectionP(c); + } + + /*package*/ void notifyDisconnect(Connection cn) { + mDisconnectRegistrants.notifyResult(cn); + } + + void notifyUnknownConnection() { + mUnknownConnectionRegistrants.notifyResult(this); + } + + public boolean isInEmergencyCall() { + return mCT.isInEmergencyCall(); + } + + public boolean isInEcm() { + return mIsPhoneInEcmState; + } + + void sendEmergencyCallbackModeChange(){ + //Send an Intent + Intent intent = new Intent(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED); + intent.putExtra(PhoneConstants.PHONE_IN_ECM_STATE, mIsPhoneInEcmState); + ActivityManagerNative.broadcastStickyIntent(intent,null); + if (DBG) Log.d(LOG_TAG, "sendEmergencyCallbackModeChange"); + } + + @Override + public void exitEmergencyCallbackMode() { + if (mWakeLock.isHeld()) { + mWakeLock.release(); + } + // Send a message which will invoke handleExitEmergencyCallbackMode + mCM.exitEmergencyCallbackMode(obtainMessage(EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE)); + } + + private void handleEnterEmergencyCallbackMode(Message msg) { + if (DBG) { + Log.d(LOG_TAG, "handleEnterEmergencyCallbackMode,mIsPhoneInEcmState= " + + mIsPhoneInEcmState); + } + // if phone is not in Ecm mode, and it's changed to Ecm mode + if (mIsPhoneInEcmState == false) { + mIsPhoneInEcmState = true; + // notify change + sendEmergencyCallbackModeChange(); + setSystemProperty(TelephonyProperties.PROPERTY_INECM_MODE, "true"); + + // Post this runnable so we will automatically exit + // if no one invokes exitEmergencyCallbackMode() directly. + long delayInMillis = SystemProperties.getLong( + TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE); + postDelayed(mExitEcmRunnable, delayInMillis); + // We don't want to go to sleep while in Ecm + mWakeLock.acquire(); + } + } + + private void handleExitEmergencyCallbackMode(Message msg) { + AsyncResult ar = (AsyncResult)msg.obj; + if (DBG) { + Log.d(LOG_TAG, "handleExitEmergencyCallbackMode,ar.exception , mIsPhoneInEcmState " + + ar.exception + mIsPhoneInEcmState); + } + // Remove pending exit Ecm runnable, if any + removeCallbacks(mExitEcmRunnable); + + if (mEcmExitRespRegistrant != null) { + mEcmExitRespRegistrant.notifyRegistrant(ar); + } + // if exiting ecm success + if (ar.exception == null) { + if (mIsPhoneInEcmState) { + mIsPhoneInEcmState = false; + setSystemProperty(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + } + // send an Intent + sendEmergencyCallbackModeChange(); + // Re-initiate data connection + mDataConnectionTracker.setInternalDataEnabled(true); + } + } + + /** + * Handle to cancel or restart Ecm timer in emergency call back mode + * if action is CANCEL_ECM_TIMER, cancel Ecm timer and notify apps the timer is canceled; + * otherwise, restart Ecm timer and notify apps the timer is restarted. + */ + void handleTimerInEmergencyCallbackMode(int action) { + switch(action) { + case CANCEL_ECM_TIMER: + removeCallbacks(mExitEcmRunnable); + mEcmTimerResetRegistrants.notifyResult(Boolean.TRUE); + break; + case RESTART_ECM_TIMER: + long delayInMillis = SystemProperties.getLong( + TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE); + postDelayed(mExitEcmRunnable, delayInMillis); + mEcmTimerResetRegistrants.notifyResult(Boolean.FALSE); + break; + default: + Log.e(LOG_TAG, "handleTimerInEmergencyCallbackMode, unsupported action " + action); + } + } + + /** + * Registration point for Ecm timer reset + * @param h handler to notify + * @param what User-defined message code + * @param obj placed in Message.obj + */ + public void registerForEcmTimerReset(Handler h, int what, Object obj) { + mEcmTimerResetRegistrants.addUnique(h, what, obj); + } + + public void unregisterForEcmTimerReset(Handler h) { + mEcmTimerResetRegistrants.remove(h); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + Message onComplete; + + switch(msg.what) { + case EVENT_RADIO_AVAILABLE: { + mCM.getBasebandVersion(obtainMessage(EVENT_GET_BASEBAND_VERSION_DONE)); + + mCM.getDeviceIdentity(obtainMessage(EVENT_GET_DEVICE_IDENTITY_DONE)); + } + break; + + case EVENT_GET_BASEBAND_VERSION_DONE:{ + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + + if (DBG) Log.d(LOG_TAG, "Baseband version: " + ar.result); + setSystemProperty(TelephonyProperties.PROPERTY_BASEBAND_VERSION, (String)ar.result); + } + break; + + case EVENT_GET_DEVICE_IDENTITY_DONE:{ + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + String[] respId = (String[])ar.result; + mImei = respId[0]; + mImeiSv = respId[1]; + mEsn = respId[2]; + mMeid = respId[3]; + } + break; + + case EVENT_EMERGENCY_CALLBACK_MODE_ENTER:{ + handleEnterEmergencyCallbackMode(msg); + } + break; + + case EVENT_ICC_RECORD_EVENTS: + ar = (AsyncResult)msg.obj; + processIccRecordEvents((Integer)ar.result); + break; + + case EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE:{ + handleExitEmergencyCallbackMode(msg); + } + break; + + case EVENT_RUIM_RECORDS_LOADED:{ + Log.d(LOG_TAG, "Event EVENT_RUIM_RECORDS_LOADED Received"); + updateCurrentCarrierInProvider(); + } + break; + + case EVENT_RADIO_OFF_OR_NOT_AVAILABLE:{ + Log.d(LOG_TAG, "Event EVENT_RADIO_OFF_OR_NOT_AVAILABLE Received"); + } + break; + + case EVENT_RADIO_ON:{ + Log.d(LOG_TAG, "Event EVENT_RADIO_ON Received"); + handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource()); + } + break; + + case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED:{ + Log.d(LOG_TAG, "EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED"); + handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource()); + } + break; + + case EVENT_SSN:{ + Log.d(LOG_TAG, "Event EVENT_SSN Received"); + } + break; + + case EVENT_REGISTERED_TO_NETWORK:{ + Log.d(LOG_TAG, "Event EVENT_REGISTERED_TO_NETWORK Received"); + } + break; + + case EVENT_NV_READY:{ + Log.d(LOG_TAG, "Event EVENT_NV_READY Received"); + prepareEri(); + } + break; + + case EVENT_SET_VM_NUMBER_DONE:{ + ar = (AsyncResult)msg.obj; + if (IccException.class.isInstance(ar.exception)) { + storeVoiceMailNumber(mVmNumber); + ar.exception = null; + } + onComplete = (Message) ar.userObj; + if (onComplete != null) { + AsyncResult.forMessage(onComplete, ar.result, ar.exception); + onComplete.sendToTarget(); + } + } + break; + + default:{ + super.handleMessage(msg); + } + } + } + + private void processIccRecordEvents(int eventCode) { + switch (eventCode) { + case RuimRecords.EVENT_MWI: + notifyMessageWaitingIndicator(); + break; + + default: + Log.e(LOG_TAG,"Unknown icc records event code " + eventCode); + break; + } + } + + /** + * Handles the call to get the subscription source + * + * @param newSubscriptionSource holds the new CDMA subscription source value + */ + private void handleCdmaSubscriptionSource(int newSubscriptionSource) { + if (newSubscriptionSource != mCdmaSubscriptionSource) { + mCdmaSubscriptionSource = newSubscriptionSource; + if (newSubscriptionSource == CDMA_SUBSCRIPTION_NV) { + // NV is ready when subscription source is NV + sendMessage(obtainMessage(EVENT_NV_READY)); + } + } + } + + /** + * Retrieves the PhoneSubInfo of the CDMAPhone + */ + public PhoneSubInfo getPhoneSubInfo() { + return mSubInfo; + } + + /** + * Retrieves the IccSmsInterfaceManager of the CDMAPhone + */ + public IccSmsInterfaceManager getIccSmsInterfaceManager() { + return mRuimSmsInterfaceManager; + } + + /** + * Retrieves the IccPhoneBookInterfaceManager of the CDMAPhone + */ + public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager() { + return mRuimPhoneBookInterfaceManager; + } + + public void registerForEriFileLoaded(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mEriFileLoadedRegistrants.add(r); + } + + public void unregisterForEriFileLoaded(Handler h) { + mEriFileLoadedRegistrants.remove(h); + } + + // override for allowing access from other classes of this package + /** + * {@inheritDoc} + */ + public final void setSystemProperty(String property, String value) { + super.setSystemProperty(property, value); + } + + /** + * Activate or deactivate cell broadcast SMS. + * + * @param activate 0 = activate, 1 = deactivate + * @param response Callback message is empty on completion + */ + public void activateCellBroadcastSms(int activate, Message response) { + Log.e(LOG_TAG, "[CDMAPhone] activateCellBroadcastSms() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + /** + * Query the current configuration of cdma cell broadcast SMS. + * + * @param response Callback message is empty on completion + */ + public void getCellBroadcastSmsConfig(Message response) { + Log.e(LOG_TAG, "[CDMAPhone] getCellBroadcastSmsConfig() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + /** + * Configure cdma cell broadcast SMS. + * + * @param response Callback message is empty on completion + */ + public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response) { + Log.e(LOG_TAG, "[CDMAPhone] setCellBroadcastSmsConfig() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + /** + * Returns true if OTA Service Provisioning needs to be performed. + */ + @Override + public boolean needsOtaServiceProvisioning() { + return mSST.getOtasp() != ServiceStateTracker.OTASP_NOT_NEEDED; + } + + private static final String IS683A_FEATURE_CODE = "*228"; + private static final int IS683A_FEATURE_CODE_NUM_DIGITS = 4; + private static final int IS683A_SYS_SEL_CODE_NUM_DIGITS = 2; + private static final int IS683A_SYS_SEL_CODE_OFFSET = 4; + + private static final int IS683_CONST_800MHZ_A_BAND = 0; + private static final int IS683_CONST_800MHZ_B_BAND = 1; + private static final int IS683_CONST_1900MHZ_A_BLOCK = 2; + private static final int IS683_CONST_1900MHZ_B_BLOCK = 3; + private static final int IS683_CONST_1900MHZ_C_BLOCK = 4; + private static final int IS683_CONST_1900MHZ_D_BLOCK = 5; + private static final int IS683_CONST_1900MHZ_E_BLOCK = 6; + private static final int IS683_CONST_1900MHZ_F_BLOCK = 7; + private static final int INVALID_SYSTEM_SELECTION_CODE = -1; + + private static boolean isIs683OtaSpDialStr(String dialStr) { + int sysSelCodeInt; + boolean isOtaspDialString = false; + int dialStrLen = dialStr.length(); + + if (dialStrLen == IS683A_FEATURE_CODE_NUM_DIGITS) { + if (dialStr.equals(IS683A_FEATURE_CODE)) { + isOtaspDialString = true; + } + } else { + sysSelCodeInt = extractSelCodeFromOtaSpNum(dialStr); + switch (sysSelCodeInt) { + case IS683_CONST_800MHZ_A_BAND: + case IS683_CONST_800MHZ_B_BAND: + case IS683_CONST_1900MHZ_A_BLOCK: + case IS683_CONST_1900MHZ_B_BLOCK: + case IS683_CONST_1900MHZ_C_BLOCK: + case IS683_CONST_1900MHZ_D_BLOCK: + case IS683_CONST_1900MHZ_E_BLOCK: + case IS683_CONST_1900MHZ_F_BLOCK: + isOtaspDialString = true; + break; + default: + break; + } + } + return isOtaspDialString; + } + /** + * This function extracts the system selection code from the dial string. + */ + private static int extractSelCodeFromOtaSpNum(String dialStr) { + int dialStrLen = dialStr.length(); + int sysSelCodeInt = INVALID_SYSTEM_SELECTION_CODE; + + if ((dialStr.regionMatches(0, IS683A_FEATURE_CODE, + 0, IS683A_FEATURE_CODE_NUM_DIGITS)) && + (dialStrLen >= (IS683A_FEATURE_CODE_NUM_DIGITS + + IS683A_SYS_SEL_CODE_NUM_DIGITS))) { + // Since we checked the condition above, the system selection code + // extracted from dialStr will not cause any exception + sysSelCodeInt = Integer.parseInt ( + dialStr.substring (IS683A_FEATURE_CODE_NUM_DIGITS, + IS683A_FEATURE_CODE_NUM_DIGITS + IS683A_SYS_SEL_CODE_NUM_DIGITS)); + } + if (DBG) Log.d(LOG_TAG, "extractSelCodeFromOtaSpNum " + sysSelCodeInt); + return sysSelCodeInt; + } + + /** + * This function checks if the system selection code extracted from + * the dial string "sysSelCodeInt' is the system selection code specified + * in the carrier ota sp number schema "sch". + */ + private static boolean + checkOtaSpNumBasedOnSysSelCode (int sysSelCodeInt, String sch[]) { + boolean isOtaSpNum = false; + try { + // Get how many number of system selection code ranges + int selRc = Integer.parseInt((String)sch[1]); + for (int i = 0; i < selRc; i++) { + if (!TextUtils.isEmpty(sch[i+2]) && !TextUtils.isEmpty(sch[i+3])) { + int selMin = Integer.parseInt((String)sch[i+2]); + int selMax = Integer.parseInt((String)sch[i+3]); + // Check if the selection code extracted from the dial string falls + // within any of the range pairs specified in the schema. + if ((sysSelCodeInt >= selMin) && (sysSelCodeInt <= selMax)) { + isOtaSpNum = true; + break; + } + } + } + } catch (NumberFormatException ex) { + // If the carrier ota sp number schema is not correct, we still allow dial + // and only log the error: + Log.e(LOG_TAG, "checkOtaSpNumBasedOnSysSelCode, error", ex); + } + return isOtaSpNum; + } + + // Define the pattern/format for carrier specified OTASP number schema. + // It separates by comma and/or whitespace. + private static Pattern pOtaSpNumSchema = Pattern.compile("[,\\s]+"); + + /** + * The following function checks if a dial string is a carrier specified + * OTASP number or not by checking against the OTASP number schema stored + * in PROPERTY_OTASP_NUM_SCHEMA. + * + * Currently, there are 2 schemas for carriers to specify the OTASP number: + * 1) Use system selection code: + * The schema is: + * SELC,the # of code pairs,min1,max1,min2,max2,... + * e.g "SELC,3,10,20,30,40,60,70" indicates that there are 3 pairs of + * selection codes, and they are {10,20}, {30,40} and {60,70} respectively. + * + * 2) Use feature code: + * The schema is: + * "FC,length of feature code,feature code". + * e.g "FC,2,*2" indicates that the length of the feature code is 2, + * and the code itself is "*2". + */ + private boolean isCarrierOtaSpNum(String dialStr) { + boolean isOtaSpNum = false; + int sysSelCodeInt = extractSelCodeFromOtaSpNum(dialStr); + if (sysSelCodeInt == INVALID_SYSTEM_SELECTION_CODE) { + return isOtaSpNum; + } + // mCarrierOtaSpNumSchema is retrieved from PROPERTY_OTASP_NUM_SCHEMA: + if (!TextUtils.isEmpty(mCarrierOtaSpNumSchema)) { + Matcher m = pOtaSpNumSchema.matcher(mCarrierOtaSpNumSchema); + if (DBG) { + Log.d(LOG_TAG, "isCarrierOtaSpNum,schema" + mCarrierOtaSpNumSchema); + } + + if (m.find()) { + String sch[] = pOtaSpNumSchema.split(mCarrierOtaSpNumSchema); + // If carrier uses system selection code mechanism + if (!TextUtils.isEmpty(sch[0]) && sch[0].equals("SELC")) { + if (sysSelCodeInt!=INVALID_SYSTEM_SELECTION_CODE) { + isOtaSpNum=checkOtaSpNumBasedOnSysSelCode(sysSelCodeInt,sch); + } else { + if (DBG) { + Log.d(LOG_TAG, "isCarrierOtaSpNum,sysSelCodeInt is invalid"); + } + } + } else if (!TextUtils.isEmpty(sch[0]) && sch[0].equals("FC")) { + int fcLen = Integer.parseInt((String)sch[1]); + String fc = (String)sch[2]; + if (dialStr.regionMatches(0,fc,0,fcLen)) { + isOtaSpNum = true; + } else { + if (DBG) Log.d(LOG_TAG, "isCarrierOtaSpNum,not otasp number"); + } + } else { + if (DBG) { + Log.d(LOG_TAG, "isCarrierOtaSpNum,ota schema not supported" + sch[0]); + } + } + } else { + if (DBG) { + Log.d(LOG_TAG, "isCarrierOtaSpNum,ota schema pattern not right" + + mCarrierOtaSpNumSchema); + } + } + } else { + if (DBG) Log.d(LOG_TAG, "isCarrierOtaSpNum,ota schema pattern empty"); + } + return isOtaSpNum; + } + + /** + * isOTASPNumber: checks a given number against the IS-683A OTASP dial string and carrier + * OTASP dial string. + * + * @param dialStr the number to look up. + * @return true if the number is in IS-683A OTASP dial string or carrier OTASP dial string + */ + @Override + public boolean isOtaSpNumber(String dialStr){ + boolean isOtaSpNum = false; + String dialableStr = PhoneNumberUtils.extractNetworkPortionAlt(dialStr); + if (dialableStr != null) { + isOtaSpNum = isIs683OtaSpDialStr(dialableStr); + if (isOtaSpNum == false) { + isOtaSpNum = isCarrierOtaSpNum(dialableStr); + } + } + if (DBG) Log.d(LOG_TAG, "isOtaSpNumber " + isOtaSpNum); + return isOtaSpNum; + } + + @Override + public int getCdmaEriIconIndex() { + return getServiceState().getCdmaEriIconIndex(); + } + + /** + * Returns the CDMA ERI icon mode, + * 0 - ON + * 1 - FLASHING + */ + @Override + public int getCdmaEriIconMode() { + return getServiceState().getCdmaEriIconMode(); + } + + /** + * Returns the CDMA ERI text, + */ + @Override + public String getCdmaEriText() { + int roamInd = getServiceState().getCdmaRoamingIndicator(); + int defRoamInd = getServiceState().getCdmaDefaultRoamingIndicator(); + return mEriManager.getCdmaEriText(roamInd, defRoamInd); + } + + /** + * Store the voicemail number in preferences + */ + private void storeVoiceMailNumber(String number) { + // Update the preference value of voicemail number + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(VM_NUMBER_CDMA, number); + editor.apply(); + } + + /** + * Sets PROPERTY_ICC_OPERATOR_ISO_COUNTRY property + * + */ + private void setIsoCountryProperty(String operatorNumeric) { + if (TextUtils.isEmpty(operatorNumeric)) { + setSystemProperty(PROPERTY_ICC_OPERATOR_ISO_COUNTRY, ""); + } else { + String iso = ""; + try { + iso = MccTable.countryCodeForMcc(Integer.parseInt( + operatorNumeric.substring(0,3))); + } catch (NumberFormatException ex) { + Log.w(LOG_TAG, "countryCodeForMcc error" + ex); + } catch (StringIndexOutOfBoundsException ex) { + Log.w(LOG_TAG, "countryCodeForMcc error" + ex); + } + + setSystemProperty(PROPERTY_ICC_OPERATOR_ISO_COUNTRY, iso); + } + } + + /** + * Sets the "current" field in the telephony provider according to the + * build-time operator numeric property + * + * @return true for success; false otherwise. + */ + boolean updateCurrentCarrierInProvider(String operatorNumeric) { + if (!TextUtils.isEmpty(operatorNumeric)) { + try { + Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current"); + ContentValues map = new ContentValues(); + map.put(Telephony.Carriers.NUMERIC, operatorNumeric); + log("updateCurrentCarrierInProvider from system: numeric=" + operatorNumeric); + getContext().getContentResolver().insert(uri, map); + + // Updates MCC MNC device configuration information + MccTable.updateMccMncConfiguration(mContext, operatorNumeric); + + return true; + } catch (SQLException e) { + Log.e(LOG_TAG, "Can't store current operator", e); + } + } + return false; + } + + /** + * Sets the "current" field in the telephony provider according to the SIM's operator. + * Implemented in {@link CDMALTEPhone} for CDMA/LTE devices. + * + * @return true for success; false otherwise. + */ + boolean updateCurrentCarrierInProvider() { + return true; + } + + public void prepareEri() { + mEriManager.loadEriFile(); + if(mEriManager.isEriFileLoaded()) { + // when the ERI file is loaded + log("ERI read, notify registrants"); + mEriFileLoadedRegistrants.notifyRegistrants(); + } + } + + public boolean isEriFileLoaded() { + return mEriManager.isEriFileLoaded(); + } + + private void registerForRuimRecordEvents() { + mIccRecords.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null); + mIccRecords.registerForRecordsLoaded(this, EVENT_RUIM_RECORDS_LOADED, null); + } + + private void unregisterForRuimRecordEvents() { + mIccRecords.unregisterForRecordsEvents(this); + mIccRecords.unregisterForRecordsLoaded(this); + } + + protected void log(String s) { + if (DBG) + Log.d(LOG_TAG, "[CDMAPhone] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CDMAPhone extends:"); + super.dump(fd, pw, args); + pw.println(" mVmNumber=" + mVmNumber); + pw.println(" mCT=" + mCT); + pw.println(" mSST=" + mSST); + pw.println(" mCdmaSSM=" + mCdmaSSM); + pw.println(" mPendingMmis=" + mPendingMmis); + pw.println(" mRuimPhoneBookInterfaceManager=" + mRuimPhoneBookInterfaceManager); + pw.println(" mRuimSmsInterfaceManager=" + mRuimSmsInterfaceManager); + pw.println(" mCdmaSubscriptionSource=" + mCdmaSubscriptionSource); + pw.println(" mSubInfo=" + mSubInfo); + pw.println(" mEriManager=" + mEriManager); + pw.println(" mWakeLock=" + mWakeLock); + pw.println(" mIsPhoneInEcmState=" + mIsPhoneInEcmState); + if (VDBG) pw.println(" mImei=" + mImei); + if (VDBG) pw.println(" mImeiSv=" + mImeiSv); + if (VDBG) pw.println(" mEsn=" + mEsn); + if (VDBG) pw.println(" mMeid=" + mMeid); + pw.println(" mCarrierOtaSpNumSchema=" + mCarrierOtaSpNumSchema); + pw.println(" getCdmaEriIconIndex()=" + getCdmaEriIconIndex()); + pw.println(" getCdmaEriIconMode()=" + getCdmaEriIconMode()); + pw.println(" getCdmaEriText()=" + getCdmaEriText()); + pw.println(" isMinInfoReady()=" + isMinInfoReady()); + pw.println(" isCspPlmnEnabled()=" + isCspPlmnEnabled()); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CallFailCause.java b/src/java/com/android/internal/telephony/cdma/CallFailCause.java new file mode 100644 index 0000000..ad6c23c --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CallFailCause.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +/** + * CDMA Call fail causes covering all the possible failures that are + * needed to be distinguished by the UI. CDMA call failure reasons + * are derived from the possible call failure scenarios described + * in "CDMA IS2000 - Release A (C.S0005-A v6.0)" standard. + * + * {@hide} + * + */ +public interface CallFailCause { + static final int NORMAL_CLEARING = 16; + // Busy Tone + static final int USER_BUSY = 17; + + static final int NORMAL_UNSPECIFIED = 31; + + // Congestion Tone + static final int NO_CIRCUIT_AVAIL = 34; + + // others + static final int ACM_LIMIT_EXCEEDED = 68; + static final int CALL_BARRED = 240; + static final int FDN_BLOCKED = 241; + + static final int CDMA_LOCKED_UNTIL_POWER_CYCLE = 1000; + static final int CDMA_DROP = 1001; + static final int CDMA_INTERCEPT = 1002; + static final int CDMA_REORDER = 1003; + static final int CDMA_SO_REJECT = 1004; + static final int CDMA_RETRY_ORDER = 1005; + static final int CDMA_ACCESS_FAILURE = 1006; + static final int CDMA_PREEMPTED = 1007; + + // For non-emergency number dialed while in emergency callback mode. + static final int CDMA_NOT_EMERGENCY = 1008; + + // Access Blocked by CDMA Network. + static final int CDMA_ACCESS_BLOCKED = 1009; + + static final int ERROR_UNSPECIFIED = 0xffff; +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaCall.java b/src/java/com/android/internal/telephony/cdma/CdmaCall.java new file mode 100644 index 0000000..4ad61bb --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaCall.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import java.util.ArrayList; +import java.util.List; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.DriverCall; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.Call.State; + +/** + * {@hide} + */ +public final class CdmaCall extends Call { + /*************************** Instance Variables **************************/ + + /*package*/ ArrayList<Connection> connections = new ArrayList<Connection>(); + /*package*/ State state = State.IDLE; + /*package*/ CdmaCallTracker owner; + + /***************************** Class Methods *****************************/ + + static State + stateFromDCState (DriverCall.State dcState) { + switch (dcState) { + case ACTIVE: return State.ACTIVE; + case HOLDING: return State.HOLDING; + case DIALING: return State.DIALING; + case ALERTING: return State.ALERTING; + case INCOMING: return State.INCOMING; + case WAITING: return State.WAITING; + default: throw new RuntimeException ("illegal call state:" + dcState); + } + } + + + /****************************** Constructors *****************************/ + /*package*/ + CdmaCall (CdmaCallTracker owner) { + this.owner = owner; + } + + public void dispose() { + } + + /************************** Overridden from Call *************************/ + public List<Connection> + getConnections() { + // FIXME should return Collections.unmodifiableList(); + return connections; + } + + public State + getState() { + return state; + } + + public Phone + getPhone() { + return owner.phone; + } + + public boolean isMultiparty() { + return connections.size() > 1; + } + + /** Please note: if this is the foreground call and a + * background call exists, the background call will be resumed + * because an AT+CHLD=1 will be sent + */ + public void + hangup() throws CallStateException { + owner.hangup(this); + } + + public String + toString() { + return state.toString(); + } + + //***** Called from CdmaConnection + + /*package*/ void + attach(Connection conn, DriverCall dc) { + connections.add(conn); + + state = stateFromDCState (dc.state); + } + + /*package*/ void + attachFake(Connection conn, State state) { + connections.add(conn); + + this.state = state; + } + + /** + * Called by CdmaConnection when it has disconnected + */ + void + connectionDisconnected(CdmaConnection conn) { + if (state != State.DISCONNECTED) { + /* If only disconnected connections remain, we are disconnected*/ + + boolean hasOnlyDisconnectedConnections = true; + + for (int i = 0, s = connections.size() ; i < s; i ++) { + if (connections.get(i).getState() + != State.DISCONNECTED + ) { + hasOnlyDisconnectedConnections = false; + break; + } + } + + if (hasOnlyDisconnectedConnections) { + state = State.DISCONNECTED; + } + } + } + + + /*package*/ void + detach(CdmaConnection conn) { + connections.remove(conn); + + if (connections.size() == 0) { + state = State.IDLE; + } + } + + /*package*/ boolean + update (CdmaConnection conn, DriverCall dc) { + State newState; + boolean changed = false; + + newState = stateFromDCState(dc.state); + + if (newState != state) { + state = newState; + changed = true; + } + + return changed; + } + + /** + * @return true if there's no space in this call for additional + * connections to be added via "conference" + */ + /*package*/ boolean + isFull() { + return connections.size() == CdmaCallTracker.MAX_CONNECTIONS_PER_CALL; + } + + //***** Called from CdmaCallTracker + + + /** + * Called when this Call is being hung up locally (eg, user pressed "end") + * Note that at this point, the hangup request has been dispatched to the radio + * but no response has yet been received so update() has not yet been called + */ + void + onHangupLocal() { + for (int i = 0, s = connections.size(); i < s; i++) { + CdmaConnection cn = (CdmaConnection)connections.get(i); + + cn.onHangupLocal(); + } + state = State.DISCONNECTING; + } + + /** + * Called when it's time to clean up disconnected Connection objects + */ + void clearDisconnected() { + for (int i = connections.size() - 1 ; i >= 0 ; i--) { + CdmaConnection cn = (CdmaConnection)connections.get(i); + + if (cn.getState() == State.DISCONNECTED) { + connections.remove(i); + } + } + + if (connections.size() == 0) { + state = State.IDLE; + } + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaCallTracker.java b/src/java/com/android/internal/telephony/cdma/CdmaCallTracker.java new file mode 100644 index 0000000..a7d5d0a --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaCallTracker.java @@ -0,0 +1,1163 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.util.Log; +import android.os.SystemProperties; + +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.CallTracker; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.DriverCall; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.TelephonyProperties; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + + +/** + * {@hide} + */ +public final class CdmaCallTracker extends CallTracker { + static final String LOG_TAG = "CDMA"; + + private static final boolean REPEAT_POLLING = false; + + private static final boolean DBG_POLL = false; + + //***** Constants + + static final int MAX_CONNECTIONS = 1; // only 1 connection allowed in CDMA + static final int MAX_CONNECTIONS_PER_CALL = 1; // only 1 connection allowed per call + + //***** Instance Variables + + CdmaConnection connections[] = new CdmaConnection[MAX_CONNECTIONS]; + RegistrantList voiceCallEndedRegistrants = new RegistrantList(); + RegistrantList voiceCallStartedRegistrants = new RegistrantList(); + RegistrantList callWaitingRegistrants = new RegistrantList(); + + + // connections dropped during last poll + ArrayList<CdmaConnection> droppedDuringPoll + = new ArrayList<CdmaConnection>(MAX_CONNECTIONS); + + CdmaCall ringingCall = new CdmaCall(this); + // A call that is ringing or (call) waiting + CdmaCall foregroundCall = new CdmaCall(this); + CdmaCall backgroundCall = new CdmaCall(this); + + CdmaConnection pendingMO; + boolean hangupPendingMO; + boolean pendingCallInEcm=false; + boolean mIsInEmergencyCall = false; + CDMAPhone phone; + + boolean desiredMute = false; // false = mute off + + int pendingCallClirMode; + PhoneConstants.State state = PhoneConstants.State.IDLE; + + private boolean mIsEcmTimerCanceled = false; + +// boolean needsPoll; + + + + //***** Events + + //***** Constructors + CdmaCallTracker(CDMAPhone phone) { + this.phone = phone; + cm = phone.mCM; + cm.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null); + cm.registerForOn(this, EVENT_RADIO_AVAILABLE, null); + cm.registerForNotAvailable(this, EVENT_RADIO_NOT_AVAILABLE, null); + cm.registerForCallWaitingInfo(this, EVENT_CALL_WAITING_INFO_CDMA, null); + foregroundCall.setGeneric(false); + } + + public void dispose() { + cm.unregisterForCallStateChanged(this); + cm.unregisterForOn(this); + cm.unregisterForNotAvailable(this); + cm.unregisterForCallWaitingInfo(this); + for(CdmaConnection c : connections) { + try { + if(c != null) hangup(c); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup during dispose"); + } + } + + try { + if(pendingMO != null) hangup(pendingMO); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup during dispose"); + } + + clearDisconnected(); + + } + + @Override + protected void finalize() { + Log.d(LOG_TAG, "CdmaCallTracker finalized"); + } + + //***** Instance Methods + + //***** Public Methods + public void registerForVoiceCallStarted(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + voiceCallStartedRegistrants.add(r); + // Notify if in call when registering + if (state != PhoneConstants.State.IDLE) { + r.notifyRegistrant(new AsyncResult(null, null, null)); + } + } + public void unregisterForVoiceCallStarted(Handler h) { + voiceCallStartedRegistrants.remove(h); + } + + public void registerForVoiceCallEnded(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + voiceCallEndedRegistrants.add(r); + } + + public void unregisterForVoiceCallEnded(Handler h) { + voiceCallEndedRegistrants.remove(h); + } + + public void registerForCallWaiting(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + callWaitingRegistrants.add(r); + } + + public void unregisterForCallWaiting(Handler h) { + callWaitingRegistrants.remove(h); + } + + private void + fakeHoldForegroundBeforeDial() { + List<Connection> connCopy; + + // We need to make a copy here, since fakeHoldBeforeDial() + // modifies the lists, and we don't want to reverse the order + connCopy = (List<Connection>) foregroundCall.connections.clone(); + + for (int i = 0, s = connCopy.size() ; i < s ; i++) { + CdmaConnection conn = (CdmaConnection)connCopy.get(i); + + conn.fakeHoldBeforeDial(); + } + } + + /** + * clirMode is one of the CLIR_ constants + */ + Connection + dial (String dialString, int clirMode) throws CallStateException { + // note that this triggers call state changed notif + clearDisconnected(); + + if (!canDial()) { + throw new CallStateException("cannot dial in current state"); + } + + String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + boolean isPhoneInEcmMode = inEcm.equals("true"); + boolean isEmergencyCall = + PhoneNumberUtils.isLocalEmergencyNumber(dialString, phone.getContext()); + + // Cancel Ecm timer if a second emergency call is originating in Ecm mode + if (isPhoneInEcmMode && isEmergencyCall) { + handleEcmTimer(phone.CANCEL_ECM_TIMER); + } + + // We are initiating a call therefore even if we previously + // didn't know the state (i.e. Generic was true) we now know + // and therefore can set Generic to false. + foregroundCall.setGeneric(false); + + // The new call must be assigned to the foreground call. + // That call must be idle, so place anything that's + // there on hold + if (foregroundCall.getState() == CdmaCall.State.ACTIVE) { + return dialThreeWay(dialString); + } + + pendingMO = new CdmaConnection(phone.getContext(), checkForTestEmergencyNumber(dialString), + this, foregroundCall); + hangupPendingMO = false; + + if (pendingMO.address == null || pendingMO.address.length() == 0 + || pendingMO.address.indexOf(PhoneNumberUtils.WILD) >= 0) { + // Phone number is invalid + pendingMO.cause = Connection.DisconnectCause.INVALID_NUMBER; + + // handlePollCalls() will notice this call not present + // and will mark it as dropped. + pollCallsWhenSafe(); + } else { + // Always unmute when initiating a new call + setMute(false); + + // Check data call + disableDataCallInEmergencyCall(dialString); + + // In Ecm mode, if another emergency call is dialed, Ecm mode will not exit. + if(!isPhoneInEcmMode || (isPhoneInEcmMode && isEmergencyCall)) { + cm.dial(pendingMO.address, clirMode, obtainCompleteMessage()); + } else { + phone.exitEmergencyCallbackMode(); + phone.setOnEcbModeExitResponse(this,EVENT_EXIT_ECM_RESPONSE_CDMA, null); + pendingCallClirMode=clirMode; + pendingCallInEcm=true; + } + } + + updatePhoneState(); + phone.notifyPreciseCallStateChanged(); + + return pendingMO; + } + + + Connection + dial (String dialString) throws CallStateException { + return dial(dialString, CommandsInterface.CLIR_DEFAULT); + } + + private Connection + dialThreeWay (String dialString) { + if (!foregroundCall.isIdle()) { + // Check data call + disableDataCallInEmergencyCall(dialString); + + // Attach the new connection to foregroundCall + pendingMO = new CdmaConnection(phone.getContext(), + checkForTestEmergencyNumber(dialString), this, foregroundCall); + cm.sendCDMAFeatureCode(pendingMO.address, + obtainMessage(EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA)); + return pendingMO; + } + return null; + } + + void + acceptCall() throws CallStateException { + if (ringingCall.getState() == CdmaCall.State.INCOMING) { + Log.i("phone", "acceptCall: incoming..."); + // Always unmute when answering a new call + setMute(false); + cm.acceptCall(obtainCompleteMessage()); + } else if (ringingCall.getState() == CdmaCall.State.WAITING) { + CdmaConnection cwConn = (CdmaConnection)(ringingCall.getLatestConnection()); + + // Since there is no network response for supplimentary + // service for CDMA, we assume call waiting is answered. + // ringing Call state change to idle is in CdmaCall.detach + // triggered by updateParent. + cwConn.updateParent(ringingCall, foregroundCall); + cwConn.onConnectedInOrOut(); + updatePhoneState(); + switchWaitingOrHoldingAndActive(); + } else { + throw new CallStateException("phone not ringing"); + } + } + + void + rejectCall () throws CallStateException { + // AT+CHLD=0 means "release held or UDUB" + // so if the phone isn't ringing, this could hang up held + if (ringingCall.getState().isRinging()) { + cm.rejectCall(obtainCompleteMessage()); + } else { + throw new CallStateException("phone not ringing"); + } + } + + void + switchWaitingOrHoldingAndActive() throws CallStateException { + // Should we bother with this check? + if (ringingCall.getState() == CdmaCall.State.INCOMING) { + throw new CallStateException("cannot be in the incoming state"); + } else if (foregroundCall.getConnections().size() > 1) { + flashAndSetGenericTrue(); + } else { + // Send a flash command to CDMA network for putting the other party on hold. + // For CDMA networks which do not support this the user would just hear a beep + // from the network. For CDMA networks which do support it will put the other + // party on hold. + cm.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT)); + } + } + + void + conference() throws CallStateException { + // Should we be checking state? + flashAndSetGenericTrue(); + } + + void + explicitCallTransfer() throws CallStateException { + cm.explicitCallTransfer(obtainCompleteMessage(EVENT_ECT_RESULT)); + } + + void + clearDisconnected() { + internalClearDisconnected(); + + updatePhoneState(); + phone.notifyPreciseCallStateChanged(); + } + + boolean + canConference() { + return foregroundCall.getState() == CdmaCall.State.ACTIVE + && backgroundCall.getState() == CdmaCall.State.HOLDING + && !backgroundCall.isFull() + && !foregroundCall.isFull(); + } + + boolean + canDial() { + boolean ret; + int serviceState = phone.getServiceState().getState(); + String disableCall = SystemProperties.get( + TelephonyProperties.PROPERTY_DISABLE_CALL, "false"); + + ret = (serviceState != ServiceState.STATE_POWER_OFF) + && pendingMO == null + && !ringingCall.isRinging() + && !disableCall.equals("true") + && (!foregroundCall.getState().isAlive() + || (foregroundCall.getState() == CdmaCall.State.ACTIVE) + || !backgroundCall.getState().isAlive()); + + if (!ret) { + log(String.format("canDial is false\n" + + "((serviceState=%d) != ServiceState.STATE_POWER_OFF)::=%s\n" + + "&& pendingMO == null::=%s\n" + + "&& !ringingCall.isRinging()::=%s\n" + + "&& !disableCall.equals(\"true\")::=%s\n" + + "&& (!foregroundCall.getState().isAlive()::=%s\n" + + " || foregroundCall.getState() == CdmaCall.State.ACTIVE::=%s\n" + + " ||!backgroundCall.getState().isAlive())::=%s)", + serviceState, + serviceState != ServiceState.STATE_POWER_OFF, + pendingMO == null, + !ringingCall.isRinging(), + !disableCall.equals("true"), + !foregroundCall.getState().isAlive(), + foregroundCall.getState() == CdmaCall.State.ACTIVE, + !backgroundCall.getState().isAlive())); + } + return ret; + } + + boolean + canTransfer() { + Log.e(LOG_TAG, "canTransfer: not possible in CDMA"); + return false; + } + + //***** Private Instance Methods + + private void + internalClearDisconnected() { + ringingCall.clearDisconnected(); + foregroundCall.clearDisconnected(); + backgroundCall.clearDisconnected(); + } + + /** + * Obtain a message to use for signalling "invoke getCurrentCalls() when + * this operation and all other pending operations are complete + */ + private Message + obtainCompleteMessage() { + return obtainCompleteMessage(EVENT_OPERATION_COMPLETE); + } + + /** + * Obtain a message to use for signalling "invoke getCurrentCalls() when + * this operation and all other pending operations are complete + */ + private Message + obtainCompleteMessage(int what) { + pendingOperations++; + lastRelevantPoll = null; + needsPoll = true; + + if (DBG_POLL) log("obtainCompleteMessage: pendingOperations=" + + pendingOperations + ", needsPoll=" + needsPoll); + + return obtainMessage(what); + } + + private void + operationComplete() { + pendingOperations--; + + if (DBG_POLL) log("operationComplete: pendingOperations=" + + pendingOperations + ", needsPoll=" + needsPoll); + + if (pendingOperations == 0 && needsPoll) { + lastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT); + cm.getCurrentCalls(lastRelevantPoll); + } else if (pendingOperations < 0) { + // this should never happen + Log.e(LOG_TAG,"CdmaCallTracker.pendingOperations < 0"); + pendingOperations = 0; + } + } + + + + private void + updatePhoneState() { + PhoneConstants.State oldState = state; + + if (ringingCall.isRinging()) { + state = PhoneConstants.State.RINGING; + } else if (pendingMO != null || + !(foregroundCall.isIdle() && backgroundCall.isIdle())) { + state = PhoneConstants.State.OFFHOOK; + } else { + state = PhoneConstants.State.IDLE; + } + + if (state == PhoneConstants.State.IDLE && oldState != state) { + voiceCallEndedRegistrants.notifyRegistrants( + new AsyncResult(null, null, null)); + } else if (oldState == PhoneConstants.State.IDLE && oldState != state) { + voiceCallStartedRegistrants.notifyRegistrants ( + new AsyncResult(null, null, null)); + } + if (Phone.DEBUG_PHONE) { + log("update phone state, old=" + oldState + " new="+ state); + } + if (state != oldState) { + phone.notifyPhoneStateChanged(); + } + } + + // ***** Overwritten from CallTracker + + protected void + handlePollCalls(AsyncResult ar) { + List polledCalls; + + if (ar.exception == null) { + polledCalls = (List)ar.result; + } else if (isCommandExceptionRadioNotAvailable(ar.exception)) { + // just a dummy empty ArrayList to cause the loop + // to hang up all the calls + polledCalls = new ArrayList(); + } else { + // Radio probably wasn't ready--try again in a bit + // But don't keep polling if the channel is closed + pollCallsAfterDelay(); + return; + } + + Connection newRinging = null; //or waiting + boolean hasNonHangupStateChanged = false; // Any change besides + // a dropped connection + boolean needsPollDelay = false; + boolean unknownConnectionAppeared = false; + + for (int i = 0, curDC = 0, dcSize = polledCalls.size() + ; i < connections.length; i++) { + CdmaConnection conn = connections[i]; + DriverCall dc = null; + + // polledCall list is sparse + if (curDC < dcSize) { + dc = (DriverCall) polledCalls.get(curDC); + + if (dc.index == i+1) { + curDC++; + } else { + dc = null; + } + } + + if (DBG_POLL) log("poll: conn[i=" + i + "]=" + + conn+", dc=" + dc); + + if (conn == null && dc != null) { + // Connection appeared in CLCC response that we don't know about + if (pendingMO != null && pendingMO.compareTo(dc)) { + + if (DBG_POLL) log("poll: pendingMO=" + pendingMO); + + // It's our pending mobile originating call + connections[i] = pendingMO; + pendingMO.index = i; + pendingMO.update(dc); + pendingMO = null; + + // Someone has already asked to hangup this call + if (hangupPendingMO) { + hangupPendingMO = false; + // Re-start Ecm timer when an uncompleted emergency call ends + if (mIsEcmTimerCanceled) { + handleEcmTimer(phone.RESTART_ECM_TIMER); + } + + try { + if (Phone.DEBUG_PHONE) log( + "poll: hangupPendingMO, hangup conn " + i); + hangup(connections[i]); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup"); + } + + // Do not continue processing this poll + // Wait for hangup and repoll + return; + } + } else { + if (Phone.DEBUG_PHONE) { + log("pendingMo=" + pendingMO + ", dc=" + dc); + } + // find if the MT call is a new ring or unknown connection + newRinging = checkMtFindNewRinging(dc,i); + if (newRinging == null) { + unknownConnectionAppeared = true; + } + checkAndEnableDataCallAfterEmergencyCallDropped(); + } + hasNonHangupStateChanged = true; + } else if (conn != null && dc == null) { + // This case means the RIL has no more active call anymore and + // we need to clean up the foregroundCall and ringingCall. + // Loop through foreground call connections as + // it contains the known logical connections. + int count = foregroundCall.connections.size(); + for (int n = 0; n < count; n++) { + if (Phone.DEBUG_PHONE) log("adding fgCall cn " + n + " to droppedDuringPoll"); + CdmaConnection cn = (CdmaConnection)foregroundCall.connections.get(n); + droppedDuringPoll.add(cn); + } + count = ringingCall.connections.size(); + // Loop through ringing call connections as + // it may contain the known logical connections. + for (int n = 0; n < count; n++) { + if (Phone.DEBUG_PHONE) log("adding rgCall cn " + n + " to droppedDuringPoll"); + CdmaConnection cn = (CdmaConnection)ringingCall.connections.get(n); + droppedDuringPoll.add(cn); + } + foregroundCall.setGeneric(false); + ringingCall.setGeneric(false); + + // Re-start Ecm timer when the connected emergency call ends + if (mIsEcmTimerCanceled) { + handleEcmTimer(phone.RESTART_ECM_TIMER); + } + // If emergency call is not going through while dialing + checkAndEnableDataCallAfterEmergencyCallDropped(); + + // Dropped connections are removed from the CallTracker + // list but kept in the Call list + connections[i] = null; + } else if (conn != null && dc != null) { /* implicit conn.compareTo(dc) */ + // Call collision case + if (conn.isIncoming != dc.isMT) { + if (dc.isMT == true){ + // Mt call takes precedence than Mo,drops Mo + droppedDuringPoll.add(conn); + // find if the MT call is a new ring or unknown connection + newRinging = checkMtFindNewRinging(dc,i); + if (newRinging == null) { + unknownConnectionAppeared = true; + } + checkAndEnableDataCallAfterEmergencyCallDropped(); + } else { + // Call info stored in conn is not consistent with the call info from dc. + // We should follow the rule of MT calls taking precedence over MO calls + // when there is conflict, so here we drop the call info from dc and + // continue to use the call info from conn, and only take a log. + Log.e(LOG_TAG,"Error in RIL, Phantom call appeared " + dc); + } + } else { + boolean changed; + changed = conn.update(dc); + hasNonHangupStateChanged = hasNonHangupStateChanged || changed; + } + } + + if (REPEAT_POLLING) { + if (dc != null) { + // FIXME with RIL, we should not need this anymore + if ((dc.state == DriverCall.State.DIALING + /*&& cm.getOption(cm.OPTION_POLL_DIALING)*/) + || (dc.state == DriverCall.State.ALERTING + /*&& cm.getOption(cm.OPTION_POLL_ALERTING)*/) + || (dc.state == DriverCall.State.INCOMING + /*&& cm.getOption(cm.OPTION_POLL_INCOMING)*/) + || (dc.state == DriverCall.State.WAITING + /*&& cm.getOption(cm.OPTION_POLL_WAITING)*/) + ) { + // Sometimes there's no unsolicited notification + // for state transitions + needsPollDelay = true; + } + } + } + } + + // This is the first poll after an ATD. + // We expect the pending call to appear in the list + // If it does not, we land here + if (pendingMO != null) { + Log.d(LOG_TAG,"Pending MO dropped before poll fg state:" + + foregroundCall.getState()); + + droppedDuringPoll.add(pendingMO); + pendingMO = null; + hangupPendingMO = false; + if( pendingCallInEcm) { + pendingCallInEcm = false; + } + } + + if (newRinging != null) { + phone.notifyNewRingingConnection(newRinging); + } + + // clear the "local hangup" and "missed/rejected call" + // cases from the "dropped during poll" list + // These cases need no "last call fail" reason + for (int i = droppedDuringPoll.size() - 1; i >= 0 ; i--) { + CdmaConnection conn = droppedDuringPoll.get(i); + + if (conn.isIncoming() && conn.getConnectTime() == 0) { + // Missed or rejected call + Connection.DisconnectCause cause; + if (conn.cause == Connection.DisconnectCause.LOCAL) { + cause = Connection.DisconnectCause.INCOMING_REJECTED; + } else { + cause = Connection.DisconnectCause.INCOMING_MISSED; + } + + if (Phone.DEBUG_PHONE) { + log("missed/rejected call, conn.cause=" + conn.cause); + log("setting cause to " + cause); + } + droppedDuringPoll.remove(i); + conn.onDisconnect(cause); + } else if (conn.cause == Connection.DisconnectCause.LOCAL) { + // Local hangup + droppedDuringPoll.remove(i); + conn.onDisconnect(Connection.DisconnectCause.LOCAL); + } else if (conn.cause == Connection.DisconnectCause.INVALID_NUMBER) { + droppedDuringPoll.remove(i); + conn.onDisconnect(Connection.DisconnectCause.INVALID_NUMBER); + } + } + + // Any non-local disconnects: determine cause + if (droppedDuringPoll.size() > 0) { + cm.getLastCallFailCause( + obtainNoPollCompleteMessage(EVENT_GET_LAST_CALL_FAIL_CAUSE)); + } + + if (needsPollDelay) { + pollCallsAfterDelay(); + } + + // Cases when we can no longer keep disconnected Connection's + // with their previous calls + // 1) the phone has started to ring + // 2) A Call/Connection object has changed state... + // we may have switched or held or answered (but not hung up) + if (newRinging != null || hasNonHangupStateChanged) { + internalClearDisconnected(); + } + + updatePhoneState(); + + if (unknownConnectionAppeared) { + phone.notifyUnknownConnection(); + } + + if (hasNonHangupStateChanged || newRinging != null) { + phone.notifyPreciseCallStateChanged(); + } + + //dumpState(); + } + + //***** Called from CdmaConnection + /*package*/ void + hangup (CdmaConnection conn) throws CallStateException { + if (conn.owner != this) { + throw new CallStateException ("CdmaConnection " + conn + + "does not belong to CdmaCallTracker " + this); + } + + if (conn == pendingMO) { + // We're hanging up an outgoing call that doesn't have it's + // GSM index assigned yet + + if (Phone.DEBUG_PHONE) log("hangup: set hangupPendingMO to true"); + hangupPendingMO = true; + } else if ((conn.getCall() == ringingCall) + && (ringingCall.getState() == CdmaCall.State.WAITING)) { + // Handle call waiting hang up case. + // + // The ringingCall state will change to IDLE in CdmaCall.detach + // if the ringing call connection size is 0. We don't specifically + // set the ringing call state to IDLE here to avoid a race condition + // where a new call waiting could get a hang up from an old call + // waiting ringingCall. + // + // PhoneApp does the call log itself since only PhoneApp knows + // the hangup reason is user ignoring or timing out. So conn.onDisconnect() + // is not called here. Instead, conn.onLocalDisconnect() is called. + conn.onLocalDisconnect(); + updatePhoneState(); + phone.notifyPreciseCallStateChanged(); + return; + } else { + try { + cm.hangupConnection (conn.getCDMAIndex(), obtainCompleteMessage()); + } catch (CallStateException ex) { + // Ignore "connection not found" + // Call may have hung up already + Log.w(LOG_TAG,"CdmaCallTracker WARN: hangup() on absent connection " + + conn); + } + } + + conn.onHangupLocal(); + } + + /*package*/ void + separate (CdmaConnection conn) throws CallStateException { + if (conn.owner != this) { + throw new CallStateException ("CdmaConnection " + conn + + "does not belong to CdmaCallTracker " + this); + } + try { + cm.separateConnection (conn.getCDMAIndex(), + obtainCompleteMessage(EVENT_SEPARATE_RESULT)); + } catch (CallStateException ex) { + // Ignore "connection not found" + // Call may have hung up already + Log.w(LOG_TAG,"CdmaCallTracker WARN: separate() on absent connection " + + conn); + } + } + + //***** Called from CDMAPhone + + /*package*/ void + setMute(boolean mute) { + desiredMute = mute; + cm.setMute(desiredMute, null); + } + + /*package*/ boolean + getMute() { + return desiredMute; + } + + + //***** Called from CdmaCall + + /* package */ void + hangup (CdmaCall call) throws CallStateException { + if (call.getConnections().size() == 0) { + throw new CallStateException("no connections in call"); + } + + if (call == ringingCall) { + if (Phone.DEBUG_PHONE) log("(ringing) hangup waiting or background"); + cm.hangupWaitingOrBackground(obtainCompleteMessage()); + } else if (call == foregroundCall) { + if (call.isDialingOrAlerting()) { + if (Phone.DEBUG_PHONE) { + log("(foregnd) hangup dialing or alerting..."); + } + hangup((CdmaConnection)(call.getConnections().get(0))); + } else { + hangupForegroundResumeBackground(); + } + } else if (call == backgroundCall) { + if (ringingCall.isRinging()) { + if (Phone.DEBUG_PHONE) { + log("hangup all conns in background call"); + } + hangupAllConnections(call); + } else { + hangupWaitingOrBackground(); + } + } else { + throw new RuntimeException ("CdmaCall " + call + + "does not belong to CdmaCallTracker " + this); + } + + call.onHangupLocal(); + phone.notifyPreciseCallStateChanged(); + } + + /* package */ + void hangupWaitingOrBackground() { + if (Phone.DEBUG_PHONE) log("hangupWaitingOrBackground"); + cm.hangupWaitingOrBackground(obtainCompleteMessage()); + } + + /* package */ + void hangupForegroundResumeBackground() { + if (Phone.DEBUG_PHONE) log("hangupForegroundResumeBackground"); + cm.hangupForegroundResumeBackground(obtainCompleteMessage()); + } + + void hangupConnectionByIndex(CdmaCall call, int index) + throws CallStateException { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + CdmaConnection cn = (CdmaConnection)call.connections.get(i); + if (cn.getCDMAIndex() == index) { + cm.hangupConnection(index, obtainCompleteMessage()); + return; + } + } + + throw new CallStateException("no gsm index found"); + } + + void hangupAllConnections(CdmaCall call) throws CallStateException{ + try { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + CdmaConnection cn = (CdmaConnection)call.connections.get(i); + cm.hangupConnection(cn.getCDMAIndex(), obtainCompleteMessage()); + } + } catch (CallStateException ex) { + Log.e(LOG_TAG, "hangupConnectionByIndex caught " + ex); + } + } + + /* package */ + CdmaConnection getConnectionByIndex(CdmaCall call, int index) + throws CallStateException { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + CdmaConnection cn = (CdmaConnection)call.connections.get(i); + if (cn.getCDMAIndex() == index) { + return cn; + } + } + + return null; + } + + private void flashAndSetGenericTrue() throws CallStateException { + cm.sendCDMAFeatureCode("", obtainMessage(EVENT_SWITCH_RESULT)); + + // Set generic to true because in CDMA it is not known what + // the status of the call is after a call waiting is answered, + // 3 way call merged or a switch between calls. + foregroundCall.setGeneric(true); + phone.notifyPreciseCallStateChanged(); + } + + private Phone.SuppService getFailedService(int what) { + switch (what) { + case EVENT_SWITCH_RESULT: + return Phone.SuppService.SWITCH; + case EVENT_CONFERENCE_RESULT: + return Phone.SuppService.CONFERENCE; + case EVENT_SEPARATE_RESULT: + return Phone.SuppService.SEPARATE; + case EVENT_ECT_RESULT: + return Phone.SuppService.TRANSFER; + } + return Phone.SuppService.UNKNOWN; + } + + private void handleRadioNotAvailable() { + // handlePollCalls will clear out its + // call list when it gets the CommandException + // error result from this + pollCallsWhenSafe(); + } + + private void notifyCallWaitingInfo(CdmaCallWaitingNotification obj) { + if (callWaitingRegistrants != null) { + callWaitingRegistrants.notifyRegistrants(new AsyncResult(null, obj, null)); + } + } + + private void handleCallWaitingInfo (CdmaCallWaitingNotification cw) { + // Check how many connections in foregroundCall. + // If the connection in foregroundCall is more + // than one, then the connection information is + // not reliable anymore since it means either + // call waiting is connected or 3 way call is + // dialed before, so set generic. + if (foregroundCall.connections.size() > 1 ) { + foregroundCall.setGeneric(true); + } + + // Create a new CdmaConnection which attaches itself to ringingCall. + ringingCall.setGeneric(false); + new CdmaConnection(phone.getContext(), cw, this, ringingCall); + updatePhoneState(); + + // Finally notify application + notifyCallWaitingInfo(cw); + } + //****** Overridden from Handler + + public void + handleMessage (Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_POLL_CALLS_RESULT:{ + Log.d(LOG_TAG, "Event EVENT_POLL_CALLS_RESULT Received"); + ar = (AsyncResult)msg.obj; + + if(msg == lastRelevantPoll) { + if(DBG_POLL) log( + "handle EVENT_POLL_CALL_RESULT: set needsPoll=F"); + needsPoll = false; + lastRelevantPoll = null; + handlePollCalls((AsyncResult)msg.obj); + } + } + break; + + case EVENT_OPERATION_COMPLETE: + operationComplete(); + break; + + case EVENT_SWITCH_RESULT: + // In GSM call operationComplete() here which gets the + // current call list. But in CDMA there is no list so + // there is nothing to do. + break; + + case EVENT_GET_LAST_CALL_FAIL_CAUSE: + int causeCode; + ar = (AsyncResult)msg.obj; + + operationComplete(); + + if (ar.exception != null) { + // An exception occurred...just treat the disconnect + // cause as "normal" + causeCode = CallFailCause.NORMAL_CLEARING; + Log.i(LOG_TAG, + "Exception during getLastCallFailCause, assuming normal disconnect"); + } else { + causeCode = ((int[])ar.result)[0]; + } + + for (int i = 0, s = droppedDuringPoll.size() + ; i < s ; i++ + ) { + CdmaConnection conn = droppedDuringPoll.get(i); + + conn.onRemoteDisconnect(causeCode); + } + + updatePhoneState(); + + phone.notifyPreciseCallStateChanged(); + droppedDuringPoll.clear(); + break; + + case EVENT_REPOLL_AFTER_DELAY: + case EVENT_CALL_STATE_CHANGE: + pollCallsWhenSafe(); + break; + + case EVENT_RADIO_AVAILABLE: + handleRadioAvailable(); + break; + + case EVENT_RADIO_NOT_AVAILABLE: + handleRadioNotAvailable(); + break; + + case EVENT_EXIT_ECM_RESPONSE_CDMA: + //no matter the result, we still do the same here + if (pendingCallInEcm) { + cm.dial(pendingMO.address, pendingCallClirMode, obtainCompleteMessage()); + pendingCallInEcm = false; + } + phone.unsetOnEcbModeExitResponse(this); + break; + + case EVENT_CALL_WAITING_INFO_CDMA: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + handleCallWaitingInfo((CdmaCallWaitingNotification)ar.result); + Log.d(LOG_TAG, "Event EVENT_CALL_WAITING_INFO_CDMA Received"); + } + break; + + case EVENT_THREE_WAY_DIAL_L2_RESULT_CDMA: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + // Assume 3 way call is connected + pendingMO.onConnectedInOrOut(); + pendingMO = null; + } + break; + + default:{ + throw new RuntimeException("unexpected event not handled"); + } + } + } + + /** + * Handle Ecm timer to be canceled or re-started + */ + private void handleEcmTimer(int action) { + phone.handleTimerInEmergencyCallbackMode(action); + switch(action) { + case CDMAPhone.CANCEL_ECM_TIMER: mIsEcmTimerCanceled = true; break; + case CDMAPhone.RESTART_ECM_TIMER: mIsEcmTimerCanceled = false; break; + default: + Log.e(LOG_TAG, "handleEcmTimer, unsupported action " + action); + } + } + + /** + * Disable data call when emergency call is connected + */ + private void disableDataCallInEmergencyCall(String dialString) { + if (PhoneNumberUtils.isLocalEmergencyNumber(dialString, phone.getContext())) { + if (Phone.DEBUG_PHONE) log("disableDataCallInEmergencyCall"); + mIsInEmergencyCall = true; + phone.mDataConnectionTracker.setInternalDataEnabled(false); + } + } + + /** + * Check and enable data call after an emergency call is dropped if it's + * not in ECM + */ + private void checkAndEnableDataCallAfterEmergencyCallDropped() { + if (mIsInEmergencyCall) { + mIsInEmergencyCall = false; + String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + if (Phone.DEBUG_PHONE) { + log("checkAndEnableDataCallAfterEmergencyCallDropped,inEcm=" + inEcm); + } + if (inEcm.compareTo("false") == 0) { + // Re-initiate data connection + phone.mDataConnectionTracker.setInternalDataEnabled(true); + } + } + } + + /** + * Check the MT call to see if it's a new ring or + * a unknown connection. + */ + private Connection checkMtFindNewRinging(DriverCall dc, int i) { + + Connection newRinging = null; + + connections[i] = new CdmaConnection(phone.getContext(), dc, this, i); + // it's a ringing call + if (connections[i].getCall() == ringingCall) { + newRinging = connections[i]; + if (Phone.DEBUG_PHONE) log("Notify new ring " + dc); + } else { + // Something strange happened: a call which is neither + // a ringing call nor the one we created. It could be the + // call collision result from RIL + Log.e(LOG_TAG,"Phantom call appeared " + dc); + // If it's a connected call, set the connect time so that + // it's non-zero. It may not be accurate, but at least + // it won't appear as a Missed Call. + if (dc.state != DriverCall.State.ALERTING + && dc.state != DriverCall.State.DIALING) { + connections[i].connectTime = System.currentTimeMillis(); + } + } + return newRinging; + } + + /** + * Check if current call is in emergency call + * + * @return true if it is in emergency call + * false if it is not in emergency call + */ + boolean isInEmergencyCall() { + return mIsInEmergencyCall; + } + + protected void log(String msg) { + Log.d(LOG_TAG, "[CdmaCallTracker] " + msg); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GsmCallTracker extends:"); + super.dump(fd, pw, args); + pw.println("droppedDuringPoll: length=" + connections.length); + for(int i=0; i < connections.length; i++) { + pw.printf(" connections[%d]=%s\n", i, connections[i]); + } + pw.println(" voiceCallEndedRegistrants=" + voiceCallEndedRegistrants); + pw.println(" voiceCallStartedRegistrants=" + voiceCallStartedRegistrants); + pw.println(" callWaitingRegistrants=" + callWaitingRegistrants); + pw.println("droppedDuringPoll: size=" + droppedDuringPoll.size()); + for(int i = 0; i < droppedDuringPoll.size(); i++) { + pw.printf( " droppedDuringPoll[%d]=%s\n", i, droppedDuringPoll.get(i)); + } + pw.println(" ringingCall=" + ringingCall); + pw.println(" foregroundCall=" + foregroundCall); + pw.println(" backgroundCall=" + backgroundCall); + pw.println(" pendingMO=" + pendingMO); + pw.println(" hangupPendingMO=" + hangupPendingMO); + pw.println(" pendingCallInEcm=" + pendingCallInEcm); + pw.println(" mIsInEmergencyCall=" + mIsInEmergencyCall); + pw.println(" phone=" + phone); + pw.println(" desiredMute=" + desiredMute); + pw.println(" pendingCallClirMode=" + pendingCallClirMode); + pw.println(" state=" + state); + pw.println(" mIsEcmTimerCanceled=" + mIsEcmTimerCanceled); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java b/src/java/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java new file mode 100644 index 0000000..0a9bdb7 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaCallWaitingNotification.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009 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.internal.telephony.cdma; + +import android.util.Log; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.PhoneConstants; + +/** + * Represents a Supplementary Service Notification received from the network. + * + * {@hide} + */ +public class CdmaCallWaitingNotification { + static final String LOG_TAG = "CDMA"; + public String number = null; + public int numberPresentation = 0; + public String name = null; + public int namePresentation = 0; + public int numberType = 0; + public int numberPlan = 0; + public int isPresent = 0; + public int signalType = 0; + public int alertPitch = 0; + public int signal = 0; + + public String toString() + { + return super.toString() + "Call Waiting Notification " + + " number: " + number + + " numberPresentation: " + numberPresentation + + " name: " + name + + " namePresentation: " + namePresentation + + " numberType: " + numberType + + " numberPlan: " + numberPlan + + " isPresent: " + isPresent + + " signalType: " + signalType + + " alertPitch: " + alertPitch + + " signal: " + signal ; + } + + public static int + presentationFromCLIP(int cli) + { + switch(cli) { + case 0: return PhoneConstants.PRESENTATION_ALLOWED; + case 1: return PhoneConstants.PRESENTATION_RESTRICTED; + case 2: return PhoneConstants.PRESENTATION_UNKNOWN; + default: + // This shouldn't happen, just log an error and treat as Unknown + Log.d(LOG_TAG, "Unexpected presentation " + cli); + return PhoneConstants.PRESENTATION_UNKNOWN; + } + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaConnection.java b/src/java/com/android/internal/telephony/cdma/CdmaConnection.java new file mode 100755 index 0000000..c75290e --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaConnection.java @@ -0,0 +1,945 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import com.android.internal.telephony.*; +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.Registrant; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.util.Log; +import android.text.TextUtils; + +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.RILConstants; + +/** + * {@hide} + */ +public class CdmaConnection extends Connection { + static final String LOG_TAG = "CDMA"; + + //***** Instance Variables + + CdmaCallTracker owner; + CdmaCall parent; + + + String address; // MAY BE NULL!!! + String dialString; // outgoing calls only + String postDialString; // outgoing calls only + boolean isIncoming; + boolean disconnected; + int index; // index in CdmaCallTracker.connections[], -1 if unassigned + + /* + * These time/timespan values are based on System.currentTimeMillis(), + * i.e., "wall clock" time. + */ + long createTime; + long connectTime; + long disconnectTime; + + /* + * These time/timespan values are based on SystemClock.elapsedRealTime(), + * i.e., time since boot. They are appropriate for comparison and + * calculating deltas. + */ + long connectTimeReal; + long duration; + long holdingStartTime; // The time when the Connection last transitioned + // into HOLDING + + int nextPostDialChar; // index into postDialString + + DisconnectCause cause = DisconnectCause.NOT_DISCONNECTED; + PostDialState postDialState = PostDialState.NOT_STARTED; + int numberPresentation = PhoneConstants.PRESENTATION_ALLOWED; + + + Handler h; + + private PowerManager.WakeLock mPartialWakeLock; + + //***** Event Constants + static final int EVENT_DTMF_DONE = 1; + static final int EVENT_PAUSE_DONE = 2; + static final int EVENT_NEXT_POST_DIAL = 3; + static final int EVENT_WAKE_LOCK_TIMEOUT = 4; + + //***** Constants + static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000; + static final int PAUSE_DELAY_MILLIS = 2 * 1000; + + //***** Inner Classes + + class MyHandler extends Handler { + MyHandler(Looper l) {super(l);} + + public void + handleMessage(Message msg) { + + switch (msg.what) { + case EVENT_NEXT_POST_DIAL: + case EVENT_DTMF_DONE: + case EVENT_PAUSE_DONE: + processNextPostDialChar(); + break; + case EVENT_WAKE_LOCK_TIMEOUT: + releaseWakeLock(); + break; + } + } + } + + //***** Constructors + + /** This is probably an MT call that we first saw in a CLCC response */ + /*package*/ + CdmaConnection (Context context, DriverCall dc, CdmaCallTracker ct, int index) { + createWakeLock(context); + acquireWakeLock(); + + owner = ct; + h = new MyHandler(owner.getLooper()); + + address = dc.number; + + isIncoming = dc.isMT; + createTime = System.currentTimeMillis(); + cnapName = dc.name; + cnapNamePresentation = dc.namePresentation; + numberPresentation = dc.numberPresentation; + + this.index = index; + + parent = parentFromDCState (dc.state); + parent.attach(this, dc); + } + + /** This is an MO call/three way call, created when dialing */ + /*package*/ + CdmaConnection(Context context, String dialString, CdmaCallTracker ct, CdmaCall parent) { + createWakeLock(context); + acquireWakeLock(); + + owner = ct; + h = new MyHandler(owner.getLooper()); + + this.dialString = dialString; + Log.d(LOG_TAG, "[CDMAConn] CdmaConnection: dialString=" + dialString); + dialString = formatDialString(dialString); + Log.d(LOG_TAG, "[CDMAConn] CdmaConnection:formated dialString=" + dialString); + + this.address = PhoneNumberUtils.extractNetworkPortionAlt(dialString); + this.postDialString = PhoneNumberUtils.extractPostDialPortion(dialString); + + index = -1; + + isIncoming = false; + cnapName = null; + cnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED; + numberPresentation = PhoneConstants.PRESENTATION_ALLOWED; + createTime = System.currentTimeMillis(); + + if (parent != null) { + this.parent = parent; + + //for the three way call case, not change parent state + if (parent.state == CdmaCall.State.ACTIVE) { + parent.attachFake(this, CdmaCall.State.ACTIVE); + } else { + parent.attachFake(this, CdmaCall.State.DIALING); + } + } + } + + /** This is a Call waiting call*/ + CdmaConnection(Context context, CdmaCallWaitingNotification cw, CdmaCallTracker ct, + CdmaCall parent) { + createWakeLock(context); + acquireWakeLock(); + + owner = ct; + h = new MyHandler(owner.getLooper()); + address = cw.number; + numberPresentation = cw.numberPresentation; + cnapName = cw.name; + cnapNamePresentation = cw.namePresentation; + index = -1; + isIncoming = true; + createTime = System.currentTimeMillis(); + connectTime = 0; + this.parent = parent; + parent.attachFake(this, CdmaCall.State.WAITING); + } + + public void dispose() { + } + + static boolean + equalsHandlesNulls (Object a, Object b) { + return (a == null) ? (b == null) : a.equals (b); + } + + /*package*/ boolean + compareTo(DriverCall c) { + // On mobile originated (MO) calls, the phone number may have changed + // due to a SIM Toolkit call control modification. + // + // We assume we know when MO calls are created (since we created them) + // and therefore don't need to compare the phone number anyway. + if (! (isIncoming || c.isMT)) return true; + + // ... but we can compare phone numbers on MT calls, and we have + // no control over when they begin, so we might as well + + String cAddress = PhoneNumberUtils.stringFromStringAndTOA(c.number, c.TOA); + return isIncoming == c.isMT && equalsHandlesNulls(address, cAddress); + } + + + public String getOrigDialString(){ + return dialString; + } + + public String getAddress() { + return address; + } + + public CdmaCall getCall() { + return parent; + } + + public long getCreateTime() { + return createTime; + } + + public long getConnectTime() { + return connectTime; + } + + public long getDisconnectTime() { + return disconnectTime; + } + + public long getDurationMillis() { + if (connectTimeReal == 0) { + return 0; + } else if (duration == 0) { + return SystemClock.elapsedRealtime() - connectTimeReal; + } else { + return duration; + } + } + + public long getHoldDurationMillis() { + if (getState() != CdmaCall.State.HOLDING) { + // If not holding, return 0 + return 0; + } else { + return SystemClock.elapsedRealtime() - holdingStartTime; + } + } + + public DisconnectCause getDisconnectCause() { + return cause; + } + + public boolean isIncoming() { + return isIncoming; + } + + public CdmaCall.State getState() { + if (disconnected) { + return CdmaCall.State.DISCONNECTED; + } else { + return super.getState(); + } + } + + public void hangup() throws CallStateException { + if (!disconnected) { + owner.hangup(this); + } else { + throw new CallStateException ("disconnected"); + } + } + + public void separate() throws CallStateException { + if (!disconnected) { + owner.separate(this); + } else { + throw new CallStateException ("disconnected"); + } + } + + public PostDialState getPostDialState() { + return postDialState; + } + + public void proceedAfterWaitChar() { + if (postDialState != PostDialState.WAIT) { + Log.w(LOG_TAG, "CdmaConnection.proceedAfterWaitChar(): Expected " + + "getPostDialState() to be WAIT but was " + postDialState); + return; + } + + setPostDialState(PostDialState.STARTED); + + processNextPostDialChar(); + } + + public void proceedAfterWildChar(String str) { + if (postDialState != PostDialState.WILD) { + Log.w(LOG_TAG, "CdmaConnection.proceedAfterWaitChar(): Expected " + + "getPostDialState() to be WILD but was " + postDialState); + return; + } + + setPostDialState(PostDialState.STARTED); + + if (false) { + boolean playedTone = false; + int len = (str != null ? str.length() : 0); + + for (int i=0; i<len; i++) { + char c = str.charAt(i); + Message msg = null; + + if (i == len-1) { + msg = h.obtainMessage(EVENT_DTMF_DONE); + } + + if (PhoneNumberUtils.is12Key(c)) { + owner.cm.sendDtmf(c, msg); + playedTone = true; + } + } + + if (!playedTone) { + processNextPostDialChar(); + } + } else { + // make a new postDialString, with the wild char replacement string + // at the beginning, followed by the remaining postDialString. + + StringBuilder buf = new StringBuilder(str); + buf.append(postDialString.substring(nextPostDialChar)); + postDialString = buf.toString(); + nextPostDialChar = 0; + if (Phone.DEBUG_PHONE) { + log("proceedAfterWildChar: new postDialString is " + + postDialString); + } + + processNextPostDialChar(); + } + } + + public void cancelPostDial() { + setPostDialState(PostDialState.CANCELLED); + } + + /** + * Called when this Connection is being hung up locally (eg, user pressed "end") + * Note that at this point, the hangup request has been dispatched to the radio + * but no response has yet been received so update() has not yet been called + */ + void + onHangupLocal() { + cause = DisconnectCause.LOCAL; + } + + DisconnectCause + disconnectCauseFromCode(int causeCode) { + /** + * See 22.001 Annex F.4 for mapping of cause codes + * to local tones + */ + + switch (causeCode) { + case CallFailCause.USER_BUSY: + return DisconnectCause.BUSY; + case CallFailCause.NO_CIRCUIT_AVAIL: + return DisconnectCause.CONGESTION; + case CallFailCause.ACM_LIMIT_EXCEEDED: + return DisconnectCause.LIMIT_EXCEEDED; + case CallFailCause.CALL_BARRED: + return DisconnectCause.CALL_BARRED; + case CallFailCause.FDN_BLOCKED: + return DisconnectCause.FDN_BLOCKED; + case CallFailCause.CDMA_LOCKED_UNTIL_POWER_CYCLE: + return DisconnectCause.CDMA_LOCKED_UNTIL_POWER_CYCLE; + case CallFailCause.CDMA_DROP: + return DisconnectCause.CDMA_DROP; + case CallFailCause.CDMA_INTERCEPT: + return DisconnectCause.CDMA_INTERCEPT; + case CallFailCause.CDMA_REORDER: + return DisconnectCause.CDMA_REORDER; + case CallFailCause.CDMA_SO_REJECT: + return DisconnectCause.CDMA_SO_REJECT; + case CallFailCause.CDMA_RETRY_ORDER: + return DisconnectCause.CDMA_RETRY_ORDER; + case CallFailCause.CDMA_ACCESS_FAILURE: + return DisconnectCause.CDMA_ACCESS_FAILURE; + case CallFailCause.CDMA_PREEMPTED: + return DisconnectCause.CDMA_PREEMPTED; + case CallFailCause.CDMA_NOT_EMERGENCY: + return DisconnectCause.CDMA_NOT_EMERGENCY; + case CallFailCause.CDMA_ACCESS_BLOCKED: + return DisconnectCause.CDMA_ACCESS_BLOCKED; + case CallFailCause.ERROR_UNSPECIFIED: + case CallFailCause.NORMAL_CLEARING: + default: + CDMAPhone phone = owner.phone; + int serviceState = phone.getServiceState().getState(); + if (serviceState == ServiceState.STATE_POWER_OFF) { + return DisconnectCause.POWER_OFF; + } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE + || serviceState == ServiceState.STATE_EMERGENCY_ONLY) { + return DisconnectCause.OUT_OF_SERVICE; + } else if (phone.mCdmaSubscriptionSource == + CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM + && phone.getIccCard().getState() != IccCardConstants.State.READY) { + return DisconnectCause.ICC_ERROR; + } else if (causeCode==CallFailCause.NORMAL_CLEARING) { + return DisconnectCause.NORMAL; + } else { + return DisconnectCause.ERROR_UNSPECIFIED; + } + } + } + + /*package*/ void + onRemoteDisconnect(int causeCode) { + onDisconnect(disconnectCauseFromCode(causeCode)); + } + + /** Called when the radio indicates the connection has been disconnected */ + /*package*/ void + onDisconnect(DisconnectCause cause) { + this.cause = cause; + + if (!disconnected) { + doDisconnect(); + if (false) Log.d(LOG_TAG, + "[CDMAConn] onDisconnect: cause=" + cause); + + owner.phone.notifyDisconnect(this); + + if (parent != null) { + parent.connectionDisconnected(this); + } + } + releaseWakeLock(); + } + + /** Called when the call waiting connection has been hung up */ + /*package*/ void + onLocalDisconnect() { + if (!disconnected) { + doDisconnect(); + if (false) Log.d(LOG_TAG, + "[CDMAConn] onLoalDisconnect" ); + + if (parent != null) { + parent.detach(this); + } + } + releaseWakeLock(); + } + + // Returns true if state has changed, false if nothing changed + /*package*/ boolean + update (DriverCall dc) { + CdmaCall newParent; + boolean changed = false; + boolean wasConnectingInOrOut = isConnectingInOrOut(); + boolean wasHolding = (getState() == CdmaCall.State.HOLDING); + + newParent = parentFromDCState(dc.state); + + if (Phone.DEBUG_PHONE) log("parent= " +parent +", newParent= " + newParent); + + if (!equalsHandlesNulls(address, dc.number)) { + if (Phone.DEBUG_PHONE) log("update: phone # changed!"); + address = dc.number; + changed = true; + } + + // A null cnapName should be the same as "" + if (TextUtils.isEmpty(dc.name)) { + if (!TextUtils.isEmpty(cnapName)) { + changed = true; + cnapName = ""; + } + } else if (!dc.name.equals(cnapName)) { + changed = true; + cnapName = dc.name; + } + + if (Phone.DEBUG_PHONE) log("--dssds----"+cnapName); + cnapNamePresentation = dc.namePresentation; + numberPresentation = dc.numberPresentation; + + if (newParent != parent) { + if (parent != null) { + parent.detach(this); + } + newParent.attach(this, dc); + parent = newParent; + changed = true; + } else { + boolean parentStateChange; + parentStateChange = parent.update (this, dc); + changed = changed || parentStateChange; + } + + /** Some state-transition events */ + + if (Phone.DEBUG_PHONE) log( + "Update, wasConnectingInOrOut=" + wasConnectingInOrOut + + ", wasHolding=" + wasHolding + + ", isConnectingInOrOut=" + isConnectingInOrOut() + + ", changed=" + changed); + + + if (wasConnectingInOrOut && !isConnectingInOrOut()) { + onConnectedInOrOut(); + } + + if (changed && !wasHolding && (getState() == CdmaCall.State.HOLDING)) { + // We've transitioned into HOLDING + onStartedHolding(); + } + + return changed; + } + + /** + * Called when this Connection is in the foregroundCall + * when a dial is initiated. + * We know we're ACTIVE, and we know we're going to end up + * HOLDING in the backgroundCall + */ + void + fakeHoldBeforeDial() { + if (parent != null) { + parent.detach(this); + } + + parent = owner.backgroundCall; + parent.attachFake(this, CdmaCall.State.HOLDING); + + onStartedHolding(); + } + + /*package*/ int + getCDMAIndex() throws CallStateException { + if (index >= 0) { + return index + 1; + } else { + throw new CallStateException ("CDMA connection index not assigned"); + } + } + + /** + * An incoming or outgoing call has connected + */ + void + onConnectedInOrOut() { + connectTime = System.currentTimeMillis(); + connectTimeReal = SystemClock.elapsedRealtime(); + duration = 0; + + // bug #678474: incoming call interpreted as missed call, even though + // it sounds like the user has picked up the call. + if (Phone.DEBUG_PHONE) { + log("onConnectedInOrOut: connectTime=" + connectTime); + } + + if (!isIncoming) { + // outgoing calls only + processNextPostDialChar(); + } else { + // Only release wake lock for incoming calls, for outgoing calls the wake lock + // will be released after any pause-dial is completed + releaseWakeLock(); + } + } + + private void + doDisconnect() { + index = -1; + disconnectTime = System.currentTimeMillis(); + duration = SystemClock.elapsedRealtime() - connectTimeReal; + disconnected = true; + } + + private void + onStartedHolding() { + holdingStartTime = SystemClock.elapsedRealtime(); + } + /** + * Performs the appropriate action for a post-dial char, but does not + * notify application. returns false if the character is invalid and + * should be ignored + */ + private boolean + processPostDialChar(char c) { + if (PhoneNumberUtils.is12Key(c)) { + owner.cm.sendDtmf(c, h.obtainMessage(EVENT_DTMF_DONE)); + } else if (c == PhoneNumberUtils.PAUSE) { + setPostDialState(PostDialState.PAUSE); + + // Upon occurrences of the separator, the UE shall + // pause again for 2 seconds before sending any + // further DTMF digits. + h.sendMessageDelayed(h.obtainMessage(EVENT_PAUSE_DONE), + PAUSE_DELAY_MILLIS); + } else if (c == PhoneNumberUtils.WAIT) { + setPostDialState(PostDialState.WAIT); + } else if (c == PhoneNumberUtils.WILD) { + setPostDialState(PostDialState.WILD); + } else { + return false; + } + + return true; + } + + public String getRemainingPostDialString() { + if (postDialState == PostDialState.CANCELLED + || postDialState == PostDialState.COMPLETE + || postDialString == null + || postDialString.length() <= nextPostDialChar) { + return ""; + } + + String subStr = postDialString.substring(nextPostDialChar); + if (subStr != null) { + int wIndex = subStr.indexOf(PhoneNumberUtils.WAIT); + int pIndex = subStr.indexOf(PhoneNumberUtils.PAUSE); + + if (wIndex > 0 && (wIndex < pIndex || pIndex <= 0)) { + subStr = subStr.substring(0, wIndex); + } else if (pIndex > 0) { + subStr = subStr.substring(0, pIndex); + } + } + return subStr; + } + + public void updateParent(CdmaCall oldParent, CdmaCall newParent){ + if (newParent != oldParent) { + if (oldParent != null) { + oldParent.detach(this); + } + newParent.attachFake(this, CdmaCall.State.ACTIVE); + parent = newParent; + } + } + + @Override + protected void finalize() + { + /** + * It is understood that This finializer is not guaranteed + * to be called and the release lock call is here just in + * case there is some path that doesn't call onDisconnect + * and or onConnectedInOrOut. + */ + if (mPartialWakeLock.isHeld()) { + Log.e(LOG_TAG, "[CdmaConn] UNEXPECTED; mPartialWakeLock is held when finalizing."); + } + releaseWakeLock(); + } + + void processNextPostDialChar() { + char c = 0; + Registrant postDialHandler; + + if (postDialState == PostDialState.CANCELLED) { + releaseWakeLock(); + //Log.v("CDMA", "##### processNextPostDialChar: postDialState == CANCELLED, bail"); + return; + } + + if (postDialString == null || + postDialString.length() <= nextPostDialChar) { + setPostDialState(PostDialState.COMPLETE); + + // We were holding a wake lock until pause-dial was complete, so give it up now + releaseWakeLock(); + + // notifyMessage.arg1 is 0 on complete + c = 0; + } else { + boolean isValid; + + setPostDialState(PostDialState.STARTED); + + c = postDialString.charAt(nextPostDialChar++); + + isValid = processPostDialChar(c); + + if (!isValid) { + // Will call processNextPostDialChar + h.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget(); + // Don't notify application + Log.e("CDMA", "processNextPostDialChar: c=" + c + " isn't valid!"); + return; + } + } + + postDialHandler = owner.phone.mPostDialHandler; + + Message notifyMessage; + + if (postDialHandler != null && + (notifyMessage = postDialHandler.messageForRegistrant()) != null) { + // The AsyncResult.result is the Connection object + PostDialState state = postDialState; + AsyncResult ar = AsyncResult.forMessage(notifyMessage); + ar.result = this; + ar.userObj = state; + + // arg1 is the character that was/is being processed + notifyMessage.arg1 = c; + + notifyMessage.sendToTarget(); + } + } + + + /** "connecting" means "has never been ACTIVE" for both incoming + * and outgoing calls + */ + private boolean + isConnectingInOrOut() { + return parent == null || parent == owner.ringingCall + || parent.state == CdmaCall.State.DIALING + || parent.state == CdmaCall.State.ALERTING; + } + + private CdmaCall + parentFromDCState (DriverCall.State state) { + switch (state) { + case ACTIVE: + case DIALING: + case ALERTING: + return owner.foregroundCall; + //break; + + case HOLDING: + return owner.backgroundCall; + //break; + + case INCOMING: + case WAITING: + return owner.ringingCall; + //break; + + default: + throw new RuntimeException("illegal call state: " + state); + } + } + + /** + * Set post dial state and acquire wake lock while switching to "started" or "wait" + * state, the wake lock will be released if state switches out of "started" or "wait" + * state or after WAKE_LOCK_TIMEOUT_MILLIS. + * @param s new PostDialState + */ + private void setPostDialState(PostDialState s) { + if (s == PostDialState.STARTED || + s == PostDialState.PAUSE) { + synchronized (mPartialWakeLock) { + if (mPartialWakeLock.isHeld()) { + h.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); + } else { + acquireWakeLock(); + } + Message msg = h.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT); + h.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS); + } + } else { + h.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); + releaseWakeLock(); + } + postDialState = s; + } + + private void createWakeLock(Context context) { + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); + } + + private void acquireWakeLock() { + log("acquireWakeLock"); + mPartialWakeLock.acquire(); + } + + private void releaseWakeLock() { + synchronized (mPartialWakeLock) { + if (mPartialWakeLock.isHeld()) { + log("releaseWakeLock"); + mPartialWakeLock.release(); + } + } + } + + private static boolean isPause(char c) { + return c == PhoneNumberUtils.PAUSE; + } + + private static boolean isWait(char c) { + return c == PhoneNumberUtils.WAIT; + } + + // This function is to find the next PAUSE character index if + // multiple pauses in a row. Otherwise it finds the next non PAUSE or + // non WAIT character index. + private static int + findNextPCharOrNonPOrNonWCharIndex(String phoneNumber, int currIndex) { + boolean wMatched = isWait(phoneNumber.charAt(currIndex)); + int index = currIndex + 1; + int length = phoneNumber.length(); + while (index < length) { + char cNext = phoneNumber.charAt(index); + // if there is any W inside P/W sequence,mark it + if (isWait(cNext)) { + wMatched = true; + } + // if any characters other than P/W chars after P/W sequence + // we break out the loop and append the correct + if (!isWait(cNext) && !isPause(cNext)) { + break; + } + index++; + } + + // It means the PAUSE character(s) is in the middle of dial string + // and it needs to be handled one by one. + if ((index < length) && (index > (currIndex + 1)) && + ((wMatched == false) && isPause(phoneNumber.charAt(currIndex)))) { + return (currIndex + 1); + } + return index; + } + + // This function returns either PAUSE or WAIT character to append. + // It is based on the next non PAUSE/WAIT character in the phoneNumber and the + // index for the current PAUSE/WAIT character + private static char + findPOrWCharToAppend(String phoneNumber, int currPwIndex, int nextNonPwCharIndex) { + char c = phoneNumber.charAt(currPwIndex); + char ret; + + // Append the PW char + ret = (isPause(c)) ? PhoneNumberUtils.PAUSE : PhoneNumberUtils.WAIT; + + // If the nextNonPwCharIndex is greater than currPwIndex + 1, + // it means the PW sequence contains not only P characters. + // Since for the sequence that only contains P character, + // the P character is handled one by one, the nextNonPwCharIndex + // equals to currPwIndex + 1. + // In this case, skip P, append W. + if (nextNonPwCharIndex > (currPwIndex + 1)) { + ret = PhoneNumberUtils.WAIT; + } + return ret; + } + + /** + * format original dial string + * 1) convert international dialing prefix "+" to + * string specified per region + * + * 2) handle corner cases for PAUSE/WAIT dialing: + * + * If PAUSE/WAIT sequence at the end, ignore them. + * + * If consecutive PAUSE/WAIT sequence in the middle of the string, + * and if there is any WAIT in PAUSE/WAIT sequence, treat them like WAIT. + */ + public static String formatDialString(String phoneNumber) { + /** + * TODO(cleanup): This function should move to PhoneNumberUtils, and + * tests should be added. + */ + + if (phoneNumber == null) { + return null; + } + int length = phoneNumber.length(); + StringBuilder ret = new StringBuilder(); + char c; + int currIndex = 0; + + while (currIndex < length) { + c = phoneNumber.charAt(currIndex); + if (isPause(c) || isWait(c)) { + if (currIndex < length - 1) { + // if PW not at the end + int nextIndex = findNextPCharOrNonPOrNonWCharIndex(phoneNumber, currIndex); + // If there is non PW char following PW sequence + if (nextIndex < length) { + char pC = findPOrWCharToAppend(phoneNumber, currIndex, nextIndex); + ret.append(pC); + // If PW char sequence has more than 2 PW characters, + // skip to the last PW character since the sequence already be + // converted to WAIT character + if (nextIndex > (currIndex + 1)) { + currIndex = nextIndex - 1; + } + } else if (nextIndex == length) { + // It means PW characters at the end, ignore + currIndex = length - 1; + } + } + } else { + ret.append(c); + } + currIndex++; + } + return PhoneNumberUtils.cdmaCheckAndProcessPlusCode(ret.toString()); + } + + private void log(String msg) { + Log.d(LOG_TAG, "[CDMAConn] " + msg); + } + + @Override + public int getNumberPresentation() { + return numberPresentation; + } + + @Override + public UUSInfo getUUSInfo() { + // UUS information not supported in CDMA + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaDataConnection.java b/src/java/com/android/internal/telephony/cdma/CdmaDataConnection.java new file mode 100644 index 0000000..8761828 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaDataConnection.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.RetryManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * {@hide} + */ +public class CdmaDataConnection extends DataConnection { + + private static final String LOG_TAG = "CDMA"; + + // ***** Constructor + private CdmaDataConnection(CDMAPhone phone, String name, int id, RetryManager rm, + DataConnectionTracker dct) { + super(phone, name, id, rm, dct); + } + + /** + * Create the connection object + * + * @param phone the Phone + * @param id the connection id + * @param rm the RetryManager + * @return CdmaDataConnection that was created. + */ + static CdmaDataConnection makeDataConnection(CDMAPhone phone, int id, RetryManager rm, + DataConnectionTracker dct) { + CdmaDataConnection cdmaDc = new CdmaDataConnection(phone, + "CdmaDC-" + mCount.incrementAndGet(), id, rm, dct); + cdmaDc.start(); + if (DBG) cdmaDc.log("Made " + cdmaDc.getName()); + return cdmaDc; + } + + /** + * Begin setting up a data connection, calls setupDataCall + * and the ConnectionParams will be returned with the + * EVENT_SETUP_DATA_CONNECTION_DONE AsyncResul.userObj. + * + * @param cp is the connection parameters + */ + @Override + protected void onConnect(ConnectionParams cp) { + if (DBG) log("CdmaDataConnection Connecting..."); + + mApn = cp.apn; + createTime = -1; + lastFailTime = -1; + lastFailCause = FailCause.NONE; + int dataProfile; + if ((cp.apn != null) && (cp.apn.types.length > 0) && (cp.apn.types[0] != null) && + (cp.apn.types[0].equals(PhoneConstants.APN_TYPE_DUN))) { + if (DBG) log("CdmaDataConnection using DUN"); + dataProfile = RILConstants.DATA_PROFILE_TETHERED; + } else { + dataProfile = RILConstants.DATA_PROFILE_DEFAULT; + } + + // msg.obj will be returned in AsyncResult.userObj; + Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp); + msg.obj = cp; + phone.mCM.setupDataCall( + Integer.toString(getRilRadioTechnology(RILConstants.SETUP_DATA_TECH_CDMA)), + Integer.toString(dataProfile), + null, null, null, + Integer.toString(RILConstants.SETUP_DATA_AUTH_PAP_CHAP), + RILConstants.SETUP_DATA_PROTOCOL_IP, msg); + } + + @Override + public String toString() { + return "State=" + getCurrentState().getName() + " create=" + createTime + " lastFail=" + + lastFailTime + " lastFasilCause=" + lastFailCause; + } + + @Override + protected boolean isDnsOk(String[] domainNameServers) { + if (NULL_IP.equals(domainNameServers[0]) + && NULL_IP.equals(domainNameServers[1]) + && !phone.isDnsCheckDisabled()) { + return false; + } else { + return true; + } + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[" + getName() + "] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CdmaDataConnection extends:"); + super.dump(fd, pw, args); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaDataConnectionTracker.java b/src/java/com/android/internal/telephony/cdma/CdmaDataConnectionTracker.java new file mode 100644 index 0000000..1088131 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaDataConnectionTracker.java @@ -0,0 +1,1045 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.TrafficStats; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.telephony.cdma.CdmaCellLocation; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; + +import com.android.internal.telephony.ApnSetting; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.DataCallState; +import com.android.internal.telephony.DataConnection.FailCause; +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.DataConnectionAc; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.DctConstants; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.RetryManager; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.Phone; +import com.android.internal.util.AsyncChannel; +import com.android.internal.telephony.RILConstants; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * {@hide} + */ +public final class CdmaDataConnectionTracker extends DataConnectionTracker { + protected final String LOG_TAG = "CDMA"; + + private CDMAPhone mCdmaPhone; + private CdmaSubscriptionSourceManager mCdmaSSM; + + /** The DataConnection being setup */ + private CdmaDataConnection mPendingDataConnection; + + private boolean mPendingRestartRadio = false; + private static final int TIME_DELAYED_TO_RESTART_RADIO = + SystemProperties.getInt("ro.cdma.timetoradiorestart", 60000); + + /** + * Pool size of CdmaDataConnection objects. + */ + private static final int DATA_CONNECTION_POOL_SIZE = 1; + + private static final String INTENT_RECONNECT_ALARM = + "com.android.internal.telephony.cdma-reconnect"; + + private static final String INTENT_DATA_STALL_ALARM = + "com.android.internal.telephony.cdma-data-stall"; + + + /** + * Constants for the data connection activity: + * physical link down/up + */ + private static final int DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE = 0; + private static final int DATA_CONNECTION_ACTIVE_PH_LINK_DOWN = 1; + private static final int DATA_CONNECTION_ACTIVE_PH_LINK_UP = 2; + + private static final String[] mSupportedApnTypes = { + PhoneConstants.APN_TYPE_DEFAULT, + PhoneConstants.APN_TYPE_MMS, + PhoneConstants.APN_TYPE_DUN, + PhoneConstants.APN_TYPE_HIPRI }; + + private static final String[] mDefaultApnTypes = { + PhoneConstants.APN_TYPE_DEFAULT, + PhoneConstants.APN_TYPE_MMS, + PhoneConstants.APN_TYPE_HIPRI }; + + private String[] mDunApnTypes = { + PhoneConstants.APN_TYPE_DUN }; + + private static final int mDefaultApnId = DctConstants.APN_DEFAULT_ID; + + /* Constructor */ + + CdmaDataConnectionTracker(CDMAPhone p) { + super(p); + mCdmaPhone = p; + + p.mCM.registerForAvailable (this, DctConstants.EVENT_RADIO_AVAILABLE, null); + p.mCM.registerForOffOrNotAvailable(this, DctConstants.EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + p.mIccRecords.registerForRecordsLoaded(this, DctConstants.EVENT_RECORDS_LOADED, null); + p.mCM.registerForDataNetworkStateChanged (this, DctConstants.EVENT_DATA_STATE_CHANGED, null); + p.mCT.registerForVoiceCallEnded (this, DctConstants.EVENT_VOICE_CALL_ENDED, null); + p.mCT.registerForVoiceCallStarted (this, DctConstants.EVENT_VOICE_CALL_STARTED, null); + p.mSST.registerForDataConnectionAttached(this, DctConstants.EVENT_TRY_SETUP_DATA, null); + p.mSST.registerForDataConnectionDetached(this, DctConstants.EVENT_CDMA_DATA_DETACHED, null); + p.mSST.registerForRoamingOn(this, DctConstants.EVENT_ROAMING_ON, null); + p.mSST.registerForRoamingOff(this, DctConstants.EVENT_ROAMING_OFF, null); + p.mCM.registerForCdmaOtaProvision(this, DctConstants.EVENT_CDMA_OTA_PROVISION, null); + mCdmaSSM = CdmaSubscriptionSourceManager.getInstance (p.getContext(), p.mCM, this, + DctConstants.EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null); + + mDataConnectionTracker = this; + + createAllDataConnectionList(); + broadcastMessenger(); + + Context c = mCdmaPhone.getContext(); + String[] t = c.getResources().getStringArray( + com.android.internal.R.array.config_cdma_dun_supported_types); + if (t != null && t.length > 0) { + ArrayList<String> temp = new ArrayList<String>(); + for(int i=0; i< t.length; i++) { + if (!PhoneConstants.APN_TYPE_DUN.equalsIgnoreCase(t[i])) { + temp.add(t[i]); + } + } + temp.add(0,PhoneConstants.APN_TYPE_DUN); + mDunApnTypes = temp.toArray(t); + } + + } + + @Override + public void dispose() { + cleanUpConnection(false, null, false); + + super.dispose(); + + // Unregister from all events + mPhone.mCM.unregisterForAvailable(this); + mPhone.mCM.unregisterForOffOrNotAvailable(this); + mCdmaPhone.mIccRecords.unregisterForRecordsLoaded(this); + mPhone.mCM.unregisterForDataNetworkStateChanged(this); + mCdmaPhone.mCT.unregisterForVoiceCallEnded(this); + mCdmaPhone.mCT.unregisterForVoiceCallStarted(this); + mCdmaPhone.mSST.unregisterForDataConnectionAttached(this); + mCdmaPhone.mSST.unregisterForDataConnectionDetached(this); + mCdmaPhone.mSST.unregisterForRoamingOn(this); + mCdmaPhone.mSST.unregisterForRoamingOff(this); + mCdmaSSM.dispose(this); + mPhone.mCM.unregisterForCdmaOtaProvision(this); + + destroyAllDataConnectionList(); + } + + @Override + protected void finalize() { + if(DBG) log("CdmaDataConnectionTracker finalized"); + } + + @Override + protected String getActionIntentReconnectAlarm() { + return INTENT_RECONNECT_ALARM; + } + + @Override + protected String getActionIntentDataStallAlarm() { + return INTENT_DATA_STALL_ALARM; + } + + @Override + protected void restartDataStallAlarm() {} + + @Override + protected void setState(DctConstants.State s) { + if (DBG) log ("setState: " + s); + if (mState != s) { + EventLog.writeEvent(EventLogTags.CDMA_DATA_STATE_CHANGE, + mState.toString(), s.toString()); + mState = s; + } + } + + @Override + public synchronized DctConstants.State getState(String apnType) { + return mState; + } + + @Override + protected boolean isApnTypeAvailable(String type) { + for (String s : mSupportedApnTypes) { + if (TextUtils.equals(type, s)) { + return true; + } + } + return false; + } + + @Override + protected boolean isDataAllowed() { + final boolean internalDataEnabled; + synchronized (mDataEnabledLock) { + internalDataEnabled = mInternalDataEnabled; + } + + int psState = mCdmaPhone.mSST.getCurrentDataConnectionState(); + boolean roaming = (mPhone.getServiceState().getRoaming() && !getDataOnRoamingEnabled()); + boolean desiredPowerState = mCdmaPhone.mSST.getDesiredPowerState(); + boolean subscriptionFromNv = (mCdmaSSM.getCdmaSubscriptionSource() + == CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_NV); + + boolean allowed = + (psState == ServiceState.STATE_IN_SERVICE || + mAutoAttachOnCreation) && + (subscriptionFromNv || + mCdmaPhone.mIccRecords.getRecordsLoaded()) && + (mCdmaPhone.mSST.isConcurrentVoiceAndDataAllowed() || + mPhone.getState() ==PhoneConstants.State.IDLE) && + !roaming && + internalDataEnabled && + desiredPowerState && + !mPendingRestartRadio && + ((mPhone.getLteOnCdmaMode() ==PhoneConstants.LTE_ON_CDMA_TRUE) || + !mCdmaPhone.needsOtaServiceProvisioning()); + if (!allowed && DBG) { + String reason = ""; + if (!((psState == ServiceState.STATE_IN_SERVICE) || mAutoAttachOnCreation)) { + reason += " - psState= " + psState; + } + if (!subscriptionFromNv && + !mCdmaPhone.mIccRecords.getRecordsLoaded()) { + reason += " - RUIM not loaded"; + } + if (!(mCdmaPhone.mSST.isConcurrentVoiceAndDataAllowed() || + mPhone.getState() ==PhoneConstants.State.IDLE)) { + reason += " - concurrentVoiceAndData not allowed and state= " + mPhone.getState(); + } + if (roaming) reason += " - Roaming"; + if (!internalDataEnabled) reason += " - mInternalDataEnabled= false"; + if (!desiredPowerState) reason += " - desiredPowerState= false"; + if (mPendingRestartRadio) reason += " - mPendingRestartRadio= true"; + if (mCdmaPhone.needsOtaServiceProvisioning()) reason += " - needs Provisioning"; + log("Data not allowed due to" + reason); + } + return allowed; + } + + @Override + protected boolean isDataPossible(String apnType) { + boolean possible = isDataAllowed() && !(getAnyDataEnabled() && + (mState == DctConstants.State.FAILED || mState == DctConstants.State.IDLE)); + if (!possible && DBG && isDataAllowed()) { + log("Data not possible. No coverage: dataState = " + mState); + } + return possible; + } + + private boolean trySetupData(String reason) { + if (DBG) log("***trySetupData due to " + (reason == null ? "(unspecified)" : reason)); + + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + setState(DctConstants.State.CONNECTED); + notifyDataConnection(reason); + notifyOffApnsOfAvailability(reason); + + log("(fix?) We're on the simulator; assuming data is connected"); + return true; + } + + int psState = mCdmaPhone.mSST.getCurrentDataConnectionState(); + boolean roaming = mPhone.getServiceState().getRoaming(); + boolean desiredPowerState = mCdmaPhone.mSST.getDesiredPowerState(); + + if ((mState == DctConstants.State.IDLE || mState == DctConstants.State.SCANNING) && + isDataAllowed() && getAnyDataEnabled() && !isEmergency()) { + boolean retValue = setupData(reason); + notifyOffApnsOfAvailability(reason); + return retValue; + } else { + notifyOffApnsOfAvailability(reason); + return false; + } + } + + /** + * Cleanup the CDMA data connection (only one is supported) + * + * @param tearDown true if the underlying DataConnection should be disconnected. + * @param reason for the clean up. + */ + private void cleanUpConnection(boolean tearDown, String reason, boolean doAll) { + if (DBG) log("cleanUpConnection: reason: " + reason); + + // Clear the reconnect alarm, if set. + if (mReconnectIntent != null) { + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + am.cancel(mReconnectIntent); + mReconnectIntent = null; + } + + setState(DctConstants.State.DISCONNECTING); + notifyOffApnsOfAvailability(reason); + + boolean notificationDeferred = false; + for (DataConnection conn : mDataConnections.values()) { + if(conn != null) { + DataConnectionAc dcac = + mDataConnectionAsyncChannels.get(conn.getDataConnectionId()); + if (tearDown) { + if (doAll) { + if (DBG) log("cleanUpConnection: teardown, conn.tearDownAll"); + conn.tearDownAll(reason, obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, + conn.getDataConnectionId(), 0, reason)); + } else { + if (DBG) log("cleanUpConnection: teardown, conn.tearDown"); + conn.tearDown(reason, obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, + conn.getDataConnectionId(), 0, reason)); + } + notificationDeferred = true; + } else { + if (DBG) log("cleanUpConnection: !tearDown, call conn.resetSynchronously"); + if (dcac != null) { + dcac.resetSync(); + } + notificationDeferred = false; + } + } + } + + stopNetStatPoll(); + + if (!notificationDeferred) { + if (DBG) log("cleanupConnection: !notificationDeferred"); + gotoIdleAndNotifyDataConnection(reason); + } + } + + private CdmaDataConnection findFreeDataConnection() { + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + if (dcac.isInactiveSync()) { + log("found free GsmDataConnection"); + return (CdmaDataConnection) dcac.dataConnection; + } + } + log("NO free CdmaDataConnection"); + return null; + } + + private boolean setupData(String reason) { + CdmaDataConnection conn = findFreeDataConnection(); + + if (conn == null) { + if (DBG) log("setupData: No free CdmaDataConnection found!"); + return false; + } + + /** TODO: We probably want the connection being setup to a parameter passed around */ + mPendingDataConnection = conn; + String[] types; + int apnId; + if (mRequestedApnType.equals(PhoneConstants.APN_TYPE_DUN)) { + types = mDunApnTypes; + apnId = DctConstants.APN_DUN_ID; + } else { + types = mDefaultApnTypes; + apnId = mDefaultApnId; + } + mActiveApn = new ApnSetting(apnId, "", "", "", "", "", "", "", "", "", + "", 0, types, "IP", "IP", true, 0); + if (DBG) log("call conn.bringUp mActiveApn=" + mActiveApn); + + Message msg = obtainMessage(); + msg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE; + msg.obj = reason; + conn.bringUp(msg, mActiveApn); + + setState(DctConstants.State.INITING); + notifyDataConnection(reason); + return true; + } + + private void notifyDefaultData(String reason) { + setState(DctConstants.State.CONNECTED); + notifyDataConnection(reason); + startNetStatPoll(); + mDataConnections.get(0).resetRetryCount(); + } + + private void resetPollStats() { + mTxPkts = -1; + mRxPkts = -1; + mSentSinceLastRecv = 0; + mNetStatPollPeriod = POLL_NETSTAT_MILLIS; + mNoRecvPollCount = 0; + } + + @Override + protected void startNetStatPoll() { + if (mState == DctConstants.State.CONNECTED && mNetStatPollEnabled == false) { + log("[DataConnection] Start poll NetStat"); + resetPollStats(); + mNetStatPollEnabled = true; + mPollNetStat.run(); + } + } + + @Override + protected void stopNetStatPoll() { + mNetStatPollEnabled = false; + removeCallbacks(mPollNetStat); + log("[DataConnection] Stop poll NetStat"); + } + + @Override + protected void restartRadio() { + if (DBG) log("Cleanup connection and wait " + + (TIME_DELAYED_TO_RESTART_RADIO / 1000) + "s to restart radio"); + cleanUpAllConnections(null); + sendEmptyMessageDelayed(DctConstants.EVENT_RESTART_RADIO, TIME_DELAYED_TO_RESTART_RADIO); + mPendingRestartRadio = true; + } + + private Runnable mPollNetStat = new Runnable() { + + public void run() { + long sent, received; + long preTxPkts = -1, preRxPkts = -1; + + DctConstants.Activity newActivity; + + preTxPkts = mTxPkts; + preRxPkts = mRxPkts; + + mTxPkts = TrafficStats.getMobileTxPackets(); + mRxPkts = TrafficStats.getMobileRxPackets(); + + //log("rx " + String.valueOf(rxPkts) + " tx " + String.valueOf(txPkts)); + + if (mNetStatPollEnabled && (preTxPkts > 0 || preRxPkts > 0)) { + sent = mTxPkts - preTxPkts; + received = mRxPkts - preRxPkts; + + if ( sent > 0 && received > 0 ) { + mSentSinceLastRecv = 0; + newActivity = DctConstants.Activity.DATAINANDOUT; + } else if (sent > 0 && received == 0) { + if (mPhone.getState() ==PhoneConstants.State.IDLE) { + mSentSinceLastRecv += sent; + } else { + mSentSinceLastRecv = 0; + } + newActivity = DctConstants.Activity.DATAOUT; + } else if (sent == 0 && received > 0) { + mSentSinceLastRecv = 0; + newActivity = DctConstants.Activity.DATAIN; + } else if (sent == 0 && received == 0) { + newActivity = (mActivity == DctConstants.Activity.DORMANT) ? + mActivity : DctConstants.Activity.NONE; + } else { + mSentSinceLastRecv = 0; + newActivity = (mActivity == DctConstants.Activity.DORMANT) ? + mActivity : DctConstants.Activity.NONE; + } + + if (mActivity != newActivity && mIsScreenOn) { + mActivity = newActivity; + mPhone.notifyDataActivity(); + } + } + + if (mSentSinceLastRecv >= NUMBER_SENT_PACKETS_OF_HANG) { + // Packets sent without ack exceeded threshold. + + if (mNoRecvPollCount == 0) { + EventLog.writeEvent( + EventLogTags.PDP_RADIO_RESET_COUNTDOWN_TRIGGERED, + mSentSinceLastRecv); + } + + if (mNoRecvPollCount < NO_RECV_POLL_LIMIT) { + mNoRecvPollCount++; + // Slow down the poll interval to let things happen + mNetStatPollPeriod = POLL_NETSTAT_SLOW_MILLIS; + } else { + if (DBG) log("Sent " + String.valueOf(mSentSinceLastRecv) + + " pkts since last received"); + // We've exceeded the threshold. Restart the radio. + mNetStatPollEnabled = false; + stopNetStatPoll(); + restartRadio(); + EventLog.writeEvent(EventLogTags.PDP_RADIO_RESET, NO_RECV_POLL_LIMIT); + } + } else { + mNoRecvPollCount = 0; + mNetStatPollPeriod = POLL_NETSTAT_MILLIS; + } + + if (mNetStatPollEnabled) { + mDataConnectionTracker.postDelayed(this, mNetStatPollPeriod); + } + } + }; + + /** + * Returns true if the last fail cause is something that + * seems like it deserves an error notification. + * Transient errors are ignored + */ + private boolean + shouldPostNotification(FailCause cause) { + return (cause != FailCause.UNKNOWN); + } + + /** + * Return true if data connection need to be setup after disconnected due to + * reason. + * + * @param reason the reason why data is disconnected + * @return true if try setup data connection is need for this reason + */ + private boolean retryAfterDisconnected(String reason) { + boolean retry = true; + + if ( Phone.REASON_RADIO_TURNED_OFF.equals(reason) ) { + retry = false; + } + return retry; + } + + private void reconnectAfterFail(FailCause lastFailCauseCode, String reason, int retryOverride) { + if (mState == DctConstants.State.FAILED) { + /** + * For now With CDMA we never try to reconnect on + * error and instead just continue to retry + * at the last time until the state is changed. + * TODO: Make this configurable? + */ + int nextReconnectDelay = retryOverride; + if (nextReconnectDelay < 0) { + nextReconnectDelay = mDataConnections.get(0).getRetryTimer(); + mDataConnections.get(0).increaseRetryCount(); + } + startAlarmForReconnect(nextReconnectDelay, reason); + + if (!shouldPostNotification(lastFailCauseCode)) { + log("NOT Posting Data Connection Unavailable notification " + + "-- likely transient error"); + } else { + notifyNoData(lastFailCauseCode); + } + } + } + + private void startAlarmForReconnect(int delay, String reason) { + + log("Data Connection activate failed. Scheduling next attempt for " + + (delay / 1000) + "s"); + + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(INTENT_RECONNECT_ALARM); + intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_REASON, reason); + mReconnectIntent = PendingIntent.getBroadcast( + mPhone.getContext(), 0, intent, 0); + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + delay, mReconnectIntent); + + } + + private void notifyNoData(FailCause lastFailCauseCode) { + setState(DctConstants.State.FAILED); + notifyOffApnsOfAvailability(null); + } + + protected void gotoIdleAndNotifyDataConnection(String reason) { + if (DBG) log("gotoIdleAndNotifyDataConnection: reason=" + reason); + setState(DctConstants.State.IDLE); + notifyDataConnection(reason); + mActiveApn = null; + } + + protected void onRecordsLoaded() { + if (mState == DctConstants.State.FAILED) { + cleanUpAllConnections(null); + } + sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, Phone.REASON_SIM_LOADED)); + } + + protected void onNVReady() { + if (mState == DctConstants.State.FAILED) { + cleanUpAllConnections(null); + } + sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA)); + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onEnableNewApn() { + // No mRequestedApnType check; only one connection is supported + cleanUpConnection(true, Phone.REASON_APN_SWITCHED, false); + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected boolean onTrySetupData(String reason) { + return trySetupData(reason); + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onRoamingOff() { + if (mUserDataEnabled == false) return; + + if (getDataOnRoamingEnabled() == false) { + notifyOffApnsOfAvailability(Phone.REASON_ROAMING_OFF); + trySetupData(Phone.REASON_ROAMING_OFF); + } else { + notifyDataConnection(Phone.REASON_ROAMING_OFF); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onRoamingOn() { + if (mUserDataEnabled == false) return; + + if (getDataOnRoamingEnabled()) { + trySetupData(Phone.REASON_ROAMING_ON); + notifyDataConnection(Phone.REASON_ROAMING_ON); + } else { + if (DBG) log("Tear down data connection on roaming."); + cleanUpAllConnections(null); + notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onRadioAvailable() { + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + setState(DctConstants.State.CONNECTED); + notifyDataConnection(null); + + log("We're on the simulator; assuming data is connected"); + } + + notifyOffApnsOfAvailability(null); + + if (mState != DctConstants.State.IDLE) { + cleanUpAllConnections(null); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onRadioOffOrNotAvailable() { + mDataConnections.get(0).resetRetryCount(); + + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + log("We're on the simulator; assuming radio off is meaningless"); + } else { + if (DBG) log("Radio is off and clean up all connection"); + cleanUpAllConnections(null); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onDataSetupComplete(AsyncResult ar) { + String reason = null; + if (ar.userObj instanceof String) { + reason = (String) ar.userObj; + } + + if (isDataSetupCompleteOk(ar)) { + // Everything is setup + notifyDefaultData(reason); + } else { + FailCause cause = (FailCause) (ar.result); + if(DBG) log("Data Connection setup failed " + cause); + + // No try for permanent failure + if (cause.isPermanentFail()) { + notifyNoData(cause); + return; + } + + int retryOverride = -1; + if (ar.exception instanceof DataConnection.CallSetupException) { + retryOverride = + ((DataConnection.CallSetupException)ar.exception).getRetryOverride(); + } + if (retryOverride == RILConstants.MAX_INT) { + if (DBG) log("No retry is suggested."); + } else { + startDelayedRetry(cause, reason, retryOverride); + } + } + } + + /** + * Called when DctConstants.EVENT_DISCONNECT_DONE is received. + */ + @Override + protected void onDisconnectDone(int connId, AsyncResult ar) { + if(DBG) log("EVENT_DISCONNECT_DONE connId=" + connId); + String reason = null; + if (ar.userObj instanceof String) { + reason = (String) ar.userObj; + } + setState(DctConstants.State.IDLE); + + // Since the pending request to turn off or restart radio will be processed here, + // remove the pending event to restart radio from the message queue. + if (mPendingRestartRadio) removeMessages(DctConstants.EVENT_RESTART_RADIO); + + // Process the pending request to turn off radio in ServiceStateTracker first. + // If radio is turned off in ServiceStateTracker, ignore the pending event to restart radio. + CdmaServiceStateTracker ssTracker = mCdmaPhone.mSST; + if (ssTracker.processPendingRadioPowerOffAfterDataOff()) { + mPendingRestartRadio = false; + } else { + onRestartRadio(); + } + + notifyDataConnection(reason); + mActiveApn = null; + if (retryAfterDisconnected(reason)) { + // Wait a bit before trying, so we're not tying up RIL command channel. + startAlarmForReconnect(APN_DELAY_MILLIS, reason); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onVoiceCallStarted() { + if (mState == DctConstants.State.CONNECTED && + !mCdmaPhone.mSST.isConcurrentVoiceAndDataAllowed()) { + stopNetStatPoll(); + notifyDataConnection(Phone.REASON_VOICE_CALL_STARTED); + notifyOffApnsOfAvailability(Phone.REASON_VOICE_CALL_STARTED); + } + } + + /** + * @override com.android.internal.telephony.DataConnectionTracker + */ + @Override + protected void onVoiceCallEnded() { + if (mState == DctConstants.State.CONNECTED) { + if (!mCdmaPhone.mSST.isConcurrentVoiceAndDataAllowed()) { + startNetStatPoll(); + notifyDataConnection(Phone.REASON_VOICE_CALL_ENDED); + } else { + // clean slate after call end. + resetPollStats(); + } + notifyOffApnsOfAvailability(Phone.REASON_VOICE_CALL_ENDED); + } else { + mDataConnections.get(0).resetRetryCount(); + // in case data setup was attempted when we were on a voice call + trySetupData(Phone.REASON_VOICE_CALL_ENDED); + } + } + + @Override + protected void onCleanUpConnection(boolean tearDown, int apnId, String reason) { + // No apnId check; only one connection is supported + cleanUpConnection(tearDown, reason, (apnId == DctConstants.APN_DUN_ID)); + } + + @Override + protected void onCleanUpAllConnections(String cause) { + // Only one CDMA connection is supported + cleanUpConnection(true, cause, false); + } + + private void createAllDataConnectionList() { + CdmaDataConnection dataConn; + + String retryConfig = SystemProperties.get("ro.cdma.data_retry_config"); + for (int i = 0; i < DATA_CONNECTION_POOL_SIZE; i++) { + RetryManager rm = new RetryManager(); + if (!rm.configure(retryConfig)) { + if (!rm.configure(DEFAULT_DATA_RETRY_CONFIG)) { + // Should never happen, log an error and default to a simple linear sequence. + log("Could not configure using DEFAULT_DATA_RETRY_CONFIG=" + + DEFAULT_DATA_RETRY_CONFIG); + rm.configure(20, 2000, 1000); + } + } + + int id = mUniqueIdGenerator.getAndIncrement(); + dataConn = CdmaDataConnection.makeDataConnection(mCdmaPhone, id, rm, this); + mDataConnections.put(id, dataConn); + DataConnectionAc dcac = new DataConnectionAc(dataConn, LOG_TAG); + int status = dcac.fullyConnectSync(mPhone.getContext(), this, dataConn.getHandler()); + if (status == AsyncChannel.STATUS_SUCCESSFUL) { + log("Fully connected"); + mDataConnectionAsyncChannels.put(dcac.dataConnection.getDataConnectionId(), dcac); + } else { + log("Could not connect to dcac.dataConnection=" + dcac.dataConnection + + " status=" + status); + } + + } + } + + private void destroyAllDataConnectionList() { + if(mDataConnections != null) { + mDataConnections.clear(); + } + } + + private void onCdmaDataDetached() { + if (mState == DctConstants.State.CONNECTED) { + startNetStatPoll(); + notifyDataConnection(Phone.REASON_CDMA_DATA_DETACHED); + } else { + if (mState == DctConstants.State.FAILED) { + cleanUpConnection(false, Phone.REASON_CDMA_DATA_DETACHED, false); + mDataConnections.get(0).resetRetryCount(); + + CdmaCellLocation loc = (CdmaCellLocation)(mPhone.getCellLocation()); + EventLog.writeEvent(EventLogTags.CDMA_DATA_SETUP_FAILED, + loc != null ? loc.getBaseStationId() : -1, + TelephonyManager.getDefault().getNetworkType()); + } + trySetupData(Phone.REASON_CDMA_DATA_DETACHED); + } + } + + private void onCdmaOtaProvision(AsyncResult ar) { + if (ar.exception != null) { + int [] otaPrivision = (int [])ar.result; + if ((otaPrivision != null) && (otaPrivision.length > 1)) { + switch (otaPrivision[0]) { + case Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED: + case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED: + mDataConnections.get(0).resetRetryCount(); + break; + default: + break; + } + } + } + } + + private void onRestartRadio() { + if (mPendingRestartRadio) { + log("************TURN OFF RADIO**************"); + mPhone.mCM.setRadioPower(false, null); + /* Note: no need to call setRadioPower(true). Assuming the desired + * radio power state is still ON (as tracked by ServiceStateTracker), + * ServiceStateTracker will call setRadioPower when it receives the + * RADIO_STATE_CHANGED notification for the power off. And if the + * desired power state has changed in the interim, we don't want to + * override it with an unconditional power on. + */ + mPendingRestartRadio = false; + } + } + + private void writeEventLogCdmaDataDrop() { + CdmaCellLocation loc = (CdmaCellLocation)(mPhone.getCellLocation()); + EventLog.writeEvent(EventLogTags.CDMA_DATA_DROP, + loc != null ? loc.getBaseStationId() : -1, + TelephonyManager.getDefault().getNetworkType()); + } + + protected void onDataStateChanged(AsyncResult ar) { + ArrayList<DataCallState> dataCallStates = (ArrayList<DataCallState>)(ar.result); + + if (ar.exception != null) { + // This is probably "radio not available" or something + // of that sort. If so, the whole connection is going + // to come down soon anyway + return; + } + + if (mState == DctConstants.State.CONNECTED) { + boolean isActiveOrDormantConnectionPresent = false; + int connectionState = DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE; + + // Check for an active or dormant connection element in + // the DATA_CALL_LIST array + for (int index = 0; index < dataCallStates.size(); index++) { + connectionState = dataCallStates.get(index).active; + if (connectionState != DATA_CONNECTION_ACTIVE_PH_LINK_INACTIVE) { + isActiveOrDormantConnectionPresent = true; + break; + } + } + + if (!isActiveOrDormantConnectionPresent) { + // No active or dormant connection + log("onDataStateChanged: No active connection" + + "state is CONNECTED, disconnecting/cleanup"); + writeEventLogCdmaDataDrop(); + cleanUpConnection(true, null, false); + return; + } + + switch (connectionState) { + case DATA_CONNECTION_ACTIVE_PH_LINK_UP: + log("onDataStateChanged: active=LINK_ACTIVE && CONNECTED, ignore"); + mActivity = DctConstants.Activity.NONE; + mPhone.notifyDataActivity(); + startNetStatPoll(); + break; + + case DATA_CONNECTION_ACTIVE_PH_LINK_DOWN: + log("onDataStateChanged active=LINK_DOWN && CONNECTED, dormant"); + mActivity = DctConstants.Activity.DORMANT; + mPhone.notifyDataActivity(); + stopNetStatPoll(); + break; + + default: + log("onDataStateChanged: IGNORE unexpected DataCallState.active=" + + connectionState); + } + } else { + // TODO: Do we need to do anything? + log("onDataStateChanged: not connected, state=" + mState + " ignoring"); + } + } + + private void startDelayedRetry(FailCause cause, String reason, int retryOverride) { + notifyNoData(cause); + reconnectAfterFail(cause, reason, retryOverride); + } + + @Override + public void handleMessage (Message msg) { + if (DBG) log("CdmaDCT handleMessage msg=" + msg); + + if (!mPhone.mIsTheCurrentActivePhone || mIsDisposed) { + log("Ignore CDMA msgs since CDMA phone is inactive"); + return; + } + + switch (msg.what) { + case DctConstants.EVENT_RECORDS_LOADED: + onRecordsLoaded(); + break; + + case DctConstants.EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED: + if(mCdmaSSM.getCdmaSubscriptionSource() == + CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_NV) { + onNVReady(); + } + break; + + case DctConstants.EVENT_CDMA_DATA_DETACHED: + onCdmaDataDetached(); + break; + + case DctConstants.EVENT_DATA_STATE_CHANGED: + onDataStateChanged((AsyncResult) msg.obj); + break; + + case DctConstants.EVENT_CDMA_OTA_PROVISION: + onCdmaOtaProvision((AsyncResult) msg.obj); + break; + + case DctConstants.EVENT_RESTART_RADIO: + if (DBG) log("EVENT_RESTART_RADIO"); + onRestartRadio(); + break; + + default: + // handle the message in the super class DataConnectionTracker + super.handleMessage(msg); + break; + } + } + + @Override + public boolean isDisconnected() { + return ((mState == DctConstants.State.IDLE) || (mState == DctConstants.State.FAILED)); + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[CdmaDCT] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[CdmaDCT] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CdmaDataConnectionTracker extends:"); + super.dump(fd, pw, args); + pw.println(" mCdmaPhone=" + mCdmaPhone); + pw.println(" mCdmaSSM=" + mCdmaSSM); + pw.println(" mPendingDataConnection=" + mPendingDataConnection); + pw.println(" mPendingRestartRadio=" + mPendingRestartRadio); + pw.println(" mSupportedApnTypes=" + mSupportedApnTypes); + pw.println(" mDefaultApnTypes=" + mDefaultApnTypes); + pw.println(" mDunApnTypes=" + mDunApnTypes); + pw.println(" mDefaultApnId=" + mDefaultApnId); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaInformationRecords.java b/src/java/com/android/internal/telephony/cdma/CdmaInformationRecords.java new file mode 100644 index 0000000..ce6530a --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaInformationRecords.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2009 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.internal.telephony.cdma; +import static com.android.internal.telephony.RILConstants.*; +import android.os.Parcel; + +public final class CdmaInformationRecords { + public Object record; + + /** + * Record type identifier + */ + public static final int RIL_CDMA_DISPLAY_INFO_REC = 0; + public static final int RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC = 1; + public static final int RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC = 2; + public static final int RIL_CDMA_CONNECTED_NUMBER_INFO_REC = 3; + public static final int RIL_CDMA_SIGNAL_INFO_REC = 4; + public static final int RIL_CDMA_REDIRECTING_NUMBER_INFO_REC = 5; + public static final int RIL_CDMA_LINE_CONTROL_INFO_REC = 6; + public static final int RIL_CDMA_EXTENDED_DISPLAY_INFO_REC = 7; + public static final int RIL_CDMA_T53_CLIR_INFO_REC = 8; + public static final int RIL_CDMA_T53_RELEASE_INFO_REC = 9; + public static final int RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC = 10; + + public CdmaInformationRecords(Parcel p) { + int id = p.readInt(); + switch (id) { + case RIL_CDMA_DISPLAY_INFO_REC: + case RIL_CDMA_EXTENDED_DISPLAY_INFO_REC: + record = new CdmaDisplayInfoRec(id, p.readString()); + break; + + case RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC: + case RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC: + case RIL_CDMA_CONNECTED_NUMBER_INFO_REC: + record = new CdmaNumberInfoRec(id, p.readString(), p.readInt(), p.readInt(), + p.readInt(), p.readInt()); + break; + + case RIL_CDMA_SIGNAL_INFO_REC: + record = new CdmaSignalInfoRec(p.readInt(), p.readInt(), p.readInt(), p.readInt()); + break; + + case RIL_CDMA_REDIRECTING_NUMBER_INFO_REC: + record = new CdmaRedirectingNumberInfoRec(p.readString(), p.readInt(), p.readInt(), + p.readInt(), p.readInt(), p.readInt()); + break; + + case RIL_CDMA_LINE_CONTROL_INFO_REC: + record = new CdmaLineControlInfoRec(p.readInt(), p.readInt(), p.readInt(), + p.readInt()); + break; + + case RIL_CDMA_T53_CLIR_INFO_REC: + record = new CdmaT53ClirInfoRec(p.readInt()); + break; + + case RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC: + record = new CdmaT53AudioControlInfoRec(p.readInt(), p.readInt()); + break; + + case RIL_CDMA_T53_RELEASE_INFO_REC: + // TODO: WHAT to do, for now fall through and throw exception + default: + throw new RuntimeException("RIL_UNSOL_CDMA_INFO_REC: unsupported record. Got " + + CdmaInformationRecords.idToString(id) + " "); + + } + } + + public static String idToString(int id) { + switch(id) { + case RIL_CDMA_DISPLAY_INFO_REC: return "RIL_CDMA_DISPLAY_INFO_REC"; + case RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC: return "RIL_CDMA_CALLED_PARTY_NUMBER_INFO_REC"; + case RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC: return "RIL_CDMA_CALLING_PARTY_NUMBER_INFO_REC"; + case RIL_CDMA_CONNECTED_NUMBER_INFO_REC: return "RIL_CDMA_CONNECTED_NUMBER_INFO_REC"; + case RIL_CDMA_SIGNAL_INFO_REC: return "RIL_CDMA_SIGNAL_INFO_REC"; + case RIL_CDMA_REDIRECTING_NUMBER_INFO_REC: return "RIL_CDMA_REDIRECTING_NUMBER_INFO_REC"; + case RIL_CDMA_LINE_CONTROL_INFO_REC: return "RIL_CDMA_LINE_CONTROL_INFO_REC"; + case RIL_CDMA_EXTENDED_DISPLAY_INFO_REC: return "RIL_CDMA_EXTENDED_DISPLAY_INFO_REC"; + case RIL_CDMA_T53_CLIR_INFO_REC: return "RIL_CDMA_T53_CLIR_INFO_REC"; + case RIL_CDMA_T53_RELEASE_INFO_REC: return "RIL_CDMA_T53_RELEASE_INFO_REC"; + case RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC: return "RIL_CDMA_T53_AUDIO_CONTROL_INFO_REC"; + default: return "<unknown record>"; + } + } + + /** + * Signal Information record from 3GPP2 C.S005 3.7.5.5 + */ + public static class CdmaSignalInfoRec { + public boolean isPresent; /* non-zero if signal information record is present */ + public int signalType; + public int alertPitch; + public int signal; + + public CdmaSignalInfoRec() {} + + public CdmaSignalInfoRec(int isPresent, int signalType, int alertPitch, int signal) { + this.isPresent = isPresent != 0; + this.signalType = signalType; + this.alertPitch = alertPitch; + this.signal = signal; + } + + @Override + public String toString() { + return "CdmaSignalInfo: {" + + " isPresent: " + isPresent + + ", signalType: " + signalType + + ", alertPitch: " + alertPitch + + ", signal: " + signal + + " }"; + } + } + + public static class CdmaDisplayInfoRec { + public int id; + public String alpha; + + public CdmaDisplayInfoRec(int id, String alpha) { + this.id = id; + this.alpha = alpha; + } + + @Override + public String toString() { + return "CdmaDisplayInfoRec: {" + + " id: " + CdmaInformationRecords.idToString(id) + + ", alpha: " + alpha + + " }"; + } + } + + public static class CdmaNumberInfoRec { + public int id; + public String number; + public byte numberType; + public byte numberPlan; + public byte pi; + public byte si; + + public CdmaNumberInfoRec(int id, String number, int numberType, int numberPlan, int pi, + int si) { + this.number = number; + this.numberType = (byte)numberType; + this.numberPlan = (byte)numberPlan; + this.pi = (byte)pi; + this.si = (byte)si; + } + + @Override + public String toString() { + return "CdmaNumberInfoRec: {" + + " id: " + CdmaInformationRecords.idToString(id) + + ", number: " + number + + ", numberType: " + numberType + + ", numberPlan: " + numberPlan + + ", pi: " + pi + + ", si: " + si + + " }"; + } + } + + public static class CdmaRedirectingNumberInfoRec { + public static final int REASON_UNKNOWN = 0; + public static final int REASON_CALL_FORWARDING_BUSY = 1; + public static final int REASON_CALL_FORWARDING_NO_REPLY = 2; + public static final int REASON_CALLED_DTE_OUT_OF_ORDER = 9; + public static final int REASON_CALL_FORWARDING_BY_THE_CALLED_DTE = 10; + public static final int REASON_CALL_FORWARDING_UNCONDITIONAL = 15; + + public CdmaNumberInfoRec numberInfoRec; + public int redirectingReason; + + public CdmaRedirectingNumberInfoRec(String number, int numberType, int numberPlan, + int pi, int si, int reason) { + numberInfoRec = new CdmaNumberInfoRec(RIL_CDMA_REDIRECTING_NUMBER_INFO_REC, + number, numberType, numberPlan, pi, si); + redirectingReason = reason; + } + + @Override + public String toString() { + return "CdmaNumberInfoRec: {" + + " numberInfoRec: " + numberInfoRec + + ", redirectingReason: " + redirectingReason + + " }"; + } + } + + public static class CdmaLineControlInfoRec { + public byte lineCtrlPolarityIncluded; + public byte lineCtrlToggle; + public byte lineCtrlReverse; + public byte lineCtrlPowerDenial; + + public CdmaLineControlInfoRec(int lineCtrlPolarityIncluded, int lineCtrlToggle, + int lineCtrlReverse, int lineCtrlPowerDenial) { + this.lineCtrlPolarityIncluded = (byte)lineCtrlPolarityIncluded; + this.lineCtrlToggle = (byte)lineCtrlToggle; + this.lineCtrlReverse = (byte)lineCtrlReverse; + this.lineCtrlPowerDenial = (byte)lineCtrlPowerDenial; + } + + @Override + public String toString() { + return "CdmaLineControlInfoRec: {" + + " lineCtrlPolarityIncluded: " + lineCtrlPolarityIncluded + + " lineCtrlToggle: " + lineCtrlToggle + + " lineCtrlReverse: " + lineCtrlReverse + + " lineCtrlPowerDenial: " + lineCtrlPowerDenial + + " }"; + } + } + + public static class CdmaT53ClirInfoRec { + public byte cause; + + public CdmaT53ClirInfoRec(int cause) { + this.cause = (byte)cause; + } + + @Override + public String toString() { + return "CdmaT53ClirInfoRec: {" + + " cause: " + cause + + " }"; + } + } + + public static class CdmaT53AudioControlInfoRec { + public byte uplink; + public byte downlink; + + public CdmaT53AudioControlInfoRec(int uplink, int downlink) { + this.uplink = (byte) uplink; + this.downlink = (byte) downlink; + } + + @Override + public String toString() { + return "CdmaT53AudioControlInfoRec: {" + + " uplink: " + uplink + + " downlink: " + downlink + + " }"; + } + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaLteServiceStateTracker.java b/src/java/com/android/internal/telephony/cdma/CdmaLteServiceStateTracker.java new file mode 100644 index 0000000..0c5c342 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaLteServiceStateTracker.java @@ -0,0 +1,546 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.IccCard; + +import android.content.Intent; +import android.telephony.SignalStrength; +import android.telephony.ServiceState; +import android.telephony.cdma.CdmaCellLocation; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemProperties; + +import android.text.TextUtils; +import android.util.Log; +import android.util.EventLog; + +import com.android.internal.telephony.gsm.GsmDataConnectionTracker; +import com.android.internal.telephony.IccCardConstants; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +public class CdmaLteServiceStateTracker extends CdmaServiceStateTracker { + CDMALTEPhone mCdmaLtePhone; + + private ServiceState mLteSS; // The last LTE state from Voice Registration + + public CdmaLteServiceStateTracker(CDMALTEPhone phone) { + super(phone); + mCdmaLtePhone = phone; + + mLteSS = new ServiceState(); + if (DBG) log("CdmaLteServiceStateTracker Constructors"); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + int[] ints; + String[] strings; + switch (msg.what) { + case EVENT_POLL_STATE_GPRS: + if (DBG) log("handleMessage EVENT_POLL_STATE_GPRS"); + ar = (AsyncResult)msg.obj; + handlePollStateResult(msg.what, ar); + break; + case EVENT_RUIM_RECORDS_LOADED: + CdmaLteUiccRecords sim = (CdmaLteUiccRecords)phone.mIccRecords; + if ((sim != null) && sim.isProvisioned()) { + mMdn = sim.getMdn(); + mMin = sim.getMin(); + parseSidNid(sim.getSid(), sim.getNid()); + mPrlVersion = sim.getPrlVersion();; + mIsMinInfoReady = true; + updateOtaspState(); + } + // SID/NID/PRL is loaded. Poll service state + // again to update to the roaming state with + // the latest variables. + pollState(); + break; + default: + super.handleMessage(msg); + } + } + + /** + * Set the cdmaSS for EVENT_POLL_STATE_REGISTRATION_CDMA + */ + @Override + protected void setCdmaTechnology(int radioTechnology) { + // Called on voice registration state response. + // Just record new CDMA radio technology + newSS.setRadioTechnology(radioTechnology); + } + + /** + * Handle the result of one of the pollState()-related requests + */ + @Override + protected void handlePollStateResultMessage(int what, AsyncResult ar) { + if (what == EVENT_POLL_STATE_GPRS) { + if (DBG) log("handlePollStateResultMessage: EVENT_POLL_STATE_GPRS"); + String states[] = (String[])ar.result; + + int type = 0; + int regState = -1; + if (states.length > 0) { + try { + regState = Integer.parseInt(states[0]); + + // states[3] (if present) is the current radio technology + if (states.length >= 4 && states[3] != null) { + type = Integer.parseInt(states[3]); + } + } catch (NumberFormatException ex) { + loge("handlePollStateResultMessage: error parsing GprsRegistrationState: " + + ex); + } + } + + mLteSS.setRadioTechnology(type); + mLteSS.setState(regCodeToServiceState(regState)); + } else { + super.handlePollStateResultMessage(what, ar); + } + } + + @Override + protected void setSignalStrengthDefaultValues() { + // TODO Make a constructor only has boolean gsm as parameter + mSignalStrength = new SignalStrength(99, -1, -1, -1, -1, -1, -1, + -1, -1, -1, SignalStrength.INVALID_SNR, -1, false); + } + + @Override + protected void pollState() { + pollingContext = new int[1]; + pollingContext[0] = 0; + + switch (cm.getRadioState()) { + case RADIO_UNAVAILABLE: + newSS.setStateOutOfService(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + + pollStateDone(); + break; + + case RADIO_OFF: + newSS.setStateOff(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + + pollStateDone(); + break; + + default: + // Issue all poll-related commands at once, then count + // down the responses which are allowed to arrive + // out-of-order. + + pollingContext[0]++; + // RIL_REQUEST_OPERATOR is necessary for CDMA + cm.getOperator(obtainMessage(EVENT_POLL_STATE_OPERATOR_CDMA, pollingContext)); + + pollingContext[0]++; + // RIL_REQUEST_VOICE_REGISTRATION_STATE is necessary for CDMA + cm.getVoiceRegistrationState(obtainMessage(EVENT_POLL_STATE_REGISTRATION_CDMA, + pollingContext)); + + int networkMode = android.provider.Settings.Secure.getInt(phone.getContext() + .getContentResolver(), + android.provider.Settings.Secure.PREFERRED_NETWORK_MODE, + RILConstants.PREFERRED_NETWORK_MODE); + if (DBG) log("pollState: network mode here is = " + networkMode); + if ((networkMode == RILConstants.NETWORK_MODE_GLOBAL) + || (networkMode == RILConstants.NETWORK_MODE_LTE_ONLY)) { + pollingContext[0]++; + // RIL_REQUEST_DATA_REGISTRATION_STATE + cm.getDataRegistrationState(obtainMessage(EVENT_POLL_STATE_GPRS, + pollingContext)); + } + break; + } + } + + @Override + protected void pollStateDone() { + // determine data RadioTechnology from both LET and CDMA SS + if (mLteSS.getState() == ServiceState.STATE_IN_SERVICE) { + //in LTE service + mNewRilRadioTechnology = mLteSS.getRilRadioTechnology(); + mNewDataConnectionState = mLteSS.getState(); + newSS.setRadioTechnology(mNewRilRadioTechnology); + log("pollStateDone LTE/eHRPD STATE_IN_SERVICE mNewRilRadioTechnology = " + + mNewRilRadioTechnology); + } else { + // LTE out of service, get CDMA Service State + mNewRilRadioTechnology = newSS.getRilRadioTechnology(); + mNewDataConnectionState = radioTechnologyToDataServiceState(mNewRilRadioTechnology); + log("pollStateDone CDMA STATE_IN_SERVICE mNewRilRadioTechnology = " + + mNewRilRadioTechnology + " mNewDataConnectionState = " + + mNewDataConnectionState); + } + + // TODO: Add proper support for LTE Only, we should be looking at + // the preferred network mode, to know when newSS state should + // be coming from mLteSs state. This was needed to pass a VZW + // LTE Only test. + // + // If CDMA service is OOS, double check if the device is running with LTE only + // mode. If that is the case, derive the service state from LTE side. + // To set in LTE only mode, sqlite3 /data/data/com.android.providers.settings/ + // databases/settings.db "update secure set value='11' where name='preferred_network_mode'" + if (newSS.getState() == ServiceState.STATE_OUT_OF_SERVICE) { + int networkMode = android.provider.Settings.Secure.getInt(phone.getContext() + .getContentResolver(), + android.provider.Settings.Secure.PREFERRED_NETWORK_MODE, + RILConstants.PREFERRED_NETWORK_MODE); + if (networkMode == RILConstants.NETWORK_MODE_LTE_ONLY) { + if (DBG) log("pollState: LTE Only mode"); + newSS.setState(mLteSS.getState()); + } + } + + if (DBG) log("pollStateDone: oldSS=[" + ss + "] newSS=[" + newSS + "]"); + + boolean hasRegistered = ss.getState() != ServiceState.STATE_IN_SERVICE + && newSS.getState() == ServiceState.STATE_IN_SERVICE; + + boolean hasDeregistered = ss.getState() == ServiceState.STATE_IN_SERVICE + && newSS.getState() != ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionAttached = + mDataConnectionState != ServiceState.STATE_IN_SERVICE + && mNewDataConnectionState == ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionDetached = + mDataConnectionState == ServiceState.STATE_IN_SERVICE + && mNewDataConnectionState != ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionChanged = + mDataConnectionState != mNewDataConnectionState; + + boolean hasRadioTechnologyChanged = mRilRadioTechnology != mNewRilRadioTechnology; + + boolean hasChanged = !newSS.equals(ss); + + boolean hasRoamingOn = !ss.getRoaming() && newSS.getRoaming(); + + boolean hasRoamingOff = ss.getRoaming() && !newSS.getRoaming(); + + boolean hasLocationChanged = !newCellLoc.equals(cellLoc); + + boolean has4gHandoff = + mNewDataConnectionState == ServiceState.STATE_IN_SERVICE && + (((mRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && + (mNewRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD)) || + ((mRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD) && + (mNewRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_LTE))); + + boolean hasMultiApnSupport = + (((mNewRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_LTE) || + (mNewRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD)) && + ((mRilRadioTechnology != ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && + (mRilRadioTechnology != ServiceState.RIL_RADIO_TECHNOLOGY_EHRPD))); + + boolean hasLostMultiApnSupport = + ((mNewRilRadioTechnology >= ServiceState.RIL_RADIO_TECHNOLOGY_IS95A) && + (mNewRilRadioTechnology <= ServiceState.RIL_RADIO_TECHNOLOGY_EVDO_A)); + + if (DBG) { + log("pollStateDone:" + + " hasRegistered=" + hasRegistered + + " hasDeegistered=" + hasDeregistered + + " hasCdmaDataConnectionAttached=" + hasCdmaDataConnectionAttached + + " hasCdmaDataConnectionDetached=" + hasCdmaDataConnectionDetached + + " hasCdmaDataConnectionChanged=" + hasCdmaDataConnectionChanged + + " hasRadioTechnologyChanged = " + hasRadioTechnologyChanged + + " hasChanged=" + hasChanged + + " hasRoamingOn=" + hasRoamingOn + + " hasRoamingOff=" + hasRoamingOff + + " hasLocationChanged=" + hasLocationChanged + + " has4gHandoff = " + has4gHandoff + + " hasMultiApnSupport=" + hasMultiApnSupport + + " hasLostMultiApnSupport=" + hasLostMultiApnSupport); + } + // Add an event log when connection state changes + if (ss.getState() != newSS.getState() + || mDataConnectionState != mNewDataConnectionState) { + EventLog.writeEvent(EventLogTags.CDMA_SERVICE_STATE_CHANGE, ss.getState(), + mDataConnectionState, newSS.getState(), mNewDataConnectionState); + } + + ServiceState tss; + tss = ss; + ss = newSS; + newSS = tss; + // clean slate for next time + newSS.setStateOutOfService(); + mLteSS.setStateOutOfService(); + + if ((hasMultiApnSupport) + && (phone.mDataConnectionTracker instanceof CdmaDataConnectionTracker)) { + if (DBG) log("GsmDataConnectionTracker Created"); + phone.mDataConnectionTracker.dispose(); + phone.mDataConnectionTracker = new GsmDataConnectionTracker(mCdmaLtePhone); + } + + if ((hasLostMultiApnSupport) + && (phone.mDataConnectionTracker instanceof GsmDataConnectionTracker)) { + if (DBG)log("GsmDataConnectionTracker disposed"); + phone.mDataConnectionTracker.dispose(); + phone.mDataConnectionTracker = new CdmaDataConnectionTracker(phone); + } + + CdmaCellLocation tcl = cellLoc; + cellLoc = newCellLoc; + newCellLoc = tcl; + + mDataConnectionState = mNewDataConnectionState; + mRilRadioTechnology = mNewRilRadioTechnology; + mNewRilRadioTechnology = 0; + + newSS.setStateOutOfService(); // clean slate for next time + + if (hasRadioTechnologyChanged) { + phone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, + ServiceState.rilRadioTechnologyToString(mRilRadioTechnology)); + } + + if (hasRegistered) { + mNetworkAttachedRegistrants.notifyRegistrants(); + } + + if (hasChanged) { + if (phone.isEriFileLoaded()) { + String eriText; + // Now the CDMAPhone sees the new ServiceState so it can get the + // new ERI text + if (ss.getState() == ServiceState.STATE_IN_SERVICE) { + eriText = phone.getCdmaEriText(); + } else if (ss.getState() == ServiceState.STATE_POWER_OFF) { + eriText = phone.mIccRecords.getServiceProviderName(); + if (TextUtils.isEmpty(eriText)) { + // Sets operator alpha property by retrieving from + // build-time system property + eriText = SystemProperties.get("ro.cdma.home.operator.alpha"); + } + } else { + // Note that ServiceState.STATE_OUT_OF_SERVICE is valid used + // for mRegistrationState 0,2,3 and 4 + eriText = phone.getContext() + .getText(com.android.internal.R.string.roamingTextSearching).toString(); + } + ss.setOperatorAlphaLong(eriText); + } + + if (phone.getIccCard().getState() == IccCardConstants.State.READY) { + // SIM is found on the device. If ERI roaming is OFF, and SID/NID matches + // one configfured in SIM, use operator name from CSIM record. + boolean showSpn = + ((CdmaLteUiccRecords)phone.mIccRecords).getCsimSpnDisplayCondition(); + int iconIndex = ss.getCdmaEriIconIndex(); + + if (showSpn && (iconIndex == EriInfo.ROAMING_INDICATOR_OFF) && + isInHomeSidNid(ss.getSystemId(), ss.getNetworkId())) { + ss.setOperatorAlphaLong(phone.mIccRecords.getServiceProviderName()); + } + } + + String operatorNumeric; + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ALPHA, + ss.getOperatorAlphaLong()); + + String prevOperatorNumeric = + SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, ""); + operatorNumeric = ss.getOperatorNumeric(); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, operatorNumeric); + + if (operatorNumeric == null) { + if (DBG) log("operatorNumeric is null"); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, ""); + mGotCountryCode = false; + } else { + String isoCountryCode = ""; + String mcc = operatorNumeric.substring(0, 3); + try { + isoCountryCode = MccTable.countryCodeForMcc(Integer.parseInt(operatorNumeric + .substring(0, 3))); + } catch (NumberFormatException ex) { + loge("countryCodeForMcc error" + ex); + } catch (StringIndexOutOfBoundsException ex) { + loge("countryCodeForMcc error" + ex); + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, + isoCountryCode); + mGotCountryCode = true; + + if (shouldFixTimeZoneNow(phone, operatorNumeric, prevOperatorNumeric, + mNeedFixZone)) { + fixTimeZone(isoCountryCode); + } + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISROAMING, + ss.getRoaming() ? "true" : "false"); + + updateSpnDisplay(); + phone.notifyServiceStateChanged(ss); + } + + if (hasCdmaDataConnectionAttached || has4gHandoff) { + mAttachedRegistrants.notifyRegistrants(); + } + + if (hasCdmaDataConnectionDetached) { + mDetachedRegistrants.notifyRegistrants(); + } + + if ((hasCdmaDataConnectionChanged || hasRadioTechnologyChanged)) { + phone.notifyDataConnection(null); + } + + if (hasRoamingOn) { + mRoamingOnRegistrants.notifyRegistrants(); + } + + if (hasRoamingOff) { + mRoamingOffRegistrants.notifyRegistrants(); + } + + if (hasLocationChanged) { + phone.notifyLocationChanged(); + } + } + + @Override + protected void onSignalStrengthResult(AsyncResult ar) { + SignalStrength oldSignalStrength = mSignalStrength; + + if (ar.exception != null) { + // Most likely radio is resetting/disconnected change to default + // values. + setSignalStrengthDefaultValues(); + } else { + int[] ints = (int[])ar.result; + + int lteRssi = -1; + int lteRsrp = -1; + int lteRsrq = -1; + int lteRssnr = SignalStrength.INVALID_SNR; + int lteCqi = -1; + + int offset = 2; + int cdmaDbm = (ints[offset] > 0) ? -ints[offset] : -120; + int cdmaEcio = (ints[offset + 1] > 0) ? -ints[offset + 1] : -160; + int evdoRssi = (ints[offset + 2] > 0) ? -ints[offset + 2] : -120; + int evdoEcio = (ints[offset + 3] > 0) ? -ints[offset + 3] : -1; + int evdoSnr = ((ints[offset + 4] > 0) && (ints[offset + 4] <= 8)) ? ints[offset + 4] + : -1; + + if (mRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_LTE) { + lteRssi = ints[offset+5]; + lteRsrp = ints[offset+6]; + lteRsrq = ints[offset+7]; + lteRssnr = ints[offset+8]; + lteCqi = ints[offset+9]; + } + + if (mRilRadioTechnology != ServiceState.RIL_RADIO_TECHNOLOGY_LTE) { + mSignalStrength = new SignalStrength(99, -1, cdmaDbm, cdmaEcio, evdoRssi, evdoEcio, + evdoSnr, false); + } else { + mSignalStrength = new SignalStrength(99, -1, cdmaDbm, cdmaEcio, evdoRssi, evdoEcio, + evdoSnr, lteRssi, lteRsrp, lteRsrq, lteRssnr, lteCqi, true); + } + } + + try { + phone.notifySignalStrength(); + } catch (NullPointerException ex) { + loge("onSignalStrengthResult() Phone already destroyed: " + ex + + "SignalStrength not notified"); + } + } + + @Override + public boolean isConcurrentVoiceAndDataAllowed() { + // Note: it needs to be confirmed which CDMA network types + // can support voice and data calls concurrently. + // For the time-being, the return value will be false. + return (mRilRadioTechnology == ServiceState.RIL_RADIO_TECHNOLOGY_LTE); + } + + /** + * Check whether the specified SID and NID pair appears in the HOME SID/NID list + * read from NV or SIM. + * + * @return true if provided sid/nid pair belongs to operator's home network. + */ + private boolean isInHomeSidNid(int sid, int nid) { + // if SID/NID is not available, assume this is home network. + if (isSidsAllZeros()) return true; + + // length of SID/NID shold be same + if (mHomeSystemId.length != mHomeNetworkId.length) return true; + + if (sid == 0) return true; + + for (int i = 0; i < mHomeSystemId.length; i++) { + // Use SID only if NID is a reserved value. + // SID 0 and NID 0 and 65535 are reserved. (C.0005 2.6.5.2) + if ((mHomeSystemId[i] == sid) && + ((mHomeNetworkId[i] == 0) || (mHomeNetworkId[i] == 65535) || + (nid == 0) || (nid == 65535) || (mHomeNetworkId[i] == nid))) { + return true; + } + } + // SID/NID are not in the list. So device is not in home network + return false; + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[CdmaLteSST] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[CdmaLteSST] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CdmaLteServiceStateTracker extends:"); + super.dump(fd, pw, args); + pw.println(" mCdmaLtePhone=" + mCdmaLtePhone); + pw.println(" mLteSS=" + mLteSS); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaLteUiccFileHandler.java b/src/java/com/android/internal/telephony/cdma/CdmaLteUiccFileHandler.java new file mode 100644 index 0000000..93a6290 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaLteUiccFileHandler.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2011 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.internal.telephony.cdma; + +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccFileHandler; +import android.os.Message; + +/** + * {@hide} + */ +public final class CdmaLteUiccFileHandler extends IccFileHandler { + static final String LOG_TAG = "CDMA"; + + public CdmaLteUiccFileHandler(IccCard card, String aid, CommandsInterface ci) { + super(card, aid, ci); + } + + protected String getEFPath(int efid) { + switch(efid) { + case EF_CSIM_SPN: + case EF_CSIM_LI: + case EF_CSIM_MDN: + case EF_CSIM_IMSIM: + case EF_CSIM_CDMAHOME: + case EF_CSIM_EPRL: + return MF_SIM + DF_CDMA; + case EF_AD: + return MF_SIM + DF_GSM; + case EF_IMPI: + case EF_DOMAIN: + case EF_IMPU: + return MF_SIM + DF_ADFISIM; + } + return getCommonIccEFPath(efid); + } + + @Override + public void loadEFTransparent(int fileid, Message onLoaded) { + if (fileid == EF_CSIM_EPRL) { + // Entire PRL could be huge. We are only interested in + // the first 4 bytes of the record. + mCi.iccIOForApp(COMMAND_READ_BINARY, fileid, getEFPath(fileid), + 0, 0, 4, null, null, mAid, + obtainMessage(EVENT_READ_BINARY_DONE, + fileid, 0, onLoaded)); + } else { + super.loadEFTransparent(fileid, onLoaded); + } + } + + + protected void logd(String msg) { + Log.d(LOG_TAG, "[CdmaLteUiccFileHandler] " + msg); + } + + protected void loge(String msg) { + Log.e(LOG_TAG, "[CdmaLteUiccFileHandler] " + msg); + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaLteUiccRecords.java b/src/java/com/android/internal/telephony/cdma/CdmaLteUiccRecords.java new file mode 100755 index 0000000..eaa2ede --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaLteUiccRecords.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2011 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.internal.telephony.cdma; + +import android.content.Context; +import android.os.AsyncResult; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.telephony.AdnRecordLoader; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccCardApplication.AppType; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.telephony.gsm.SIMRecords; +import com.android.internal.telephony.ims.IsimRecords; +import com.android.internal.telephony.ims.IsimUiccRecords; + +import java.util.ArrayList; +import java.util.Locale; + +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_TEST_CSIM; + +/** + * {@hide} + */ +public final class CdmaLteUiccRecords extends SIMRecords { + // From CSIM application + private byte[] mEFpl = null; + private byte[] mEFli = null; + boolean mCsimSpnDisplayCondition = false; + private String mMdn; + private String mMin; + private String mPrlVersion; + private String mHomeSystemId; + private String mHomeNetworkId; + + private final IsimUiccRecords mIsimUiccRecords = new IsimUiccRecords(); + + public CdmaLteUiccRecords(IccCard card, Context c, CommandsInterface ci) { + super(card, c, ci); + } + + // Refer to ETSI TS 102.221 + private class EfPlLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_PL"; + } + + public void onRecordLoaded(AsyncResult ar) { + mEFpl = (byte[]) ar.result; + if (DBG) log("EF_PL=" + IccUtils.bytesToHexString(mEFpl)); + } + } + + // Refer to C.S0065 5.2.26 + private class EfCsimLiLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_LI"; + } + + public void onRecordLoaded(AsyncResult ar) { + mEFli = (byte[]) ar.result; + // convert csim efli data to iso 639 format + for (int i = 0; i < mEFli.length; i+=2) { + switch(mEFli[i+1]) { + case 0x01: mEFli[i] = 'e'; mEFli[i+1] = 'n';break; + case 0x02: mEFli[i] = 'f'; mEFli[i+1] = 'r';break; + case 0x03: mEFli[i] = 'e'; mEFli[i+1] = 's';break; + case 0x04: mEFli[i] = 'j'; mEFli[i+1] = 'a';break; + case 0x05: mEFli[i] = 'k'; mEFli[i+1] = 'o';break; + case 0x06: mEFli[i] = 'z'; mEFli[i+1] = 'h';break; + case 0x07: mEFli[i] = 'h'; mEFli[i+1] = 'e';break; + default: mEFli[i] = ' '; mEFli[i+1] = ' '; + } + } + + if (DBG) log("EF_LI=" + IccUtils.bytesToHexString(mEFli)); + } + } + + // Refer to C.S0065 5.2.32 + private class EfCsimSpnLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_SPN"; + } + + public void onRecordLoaded(AsyncResult ar) { + byte[] data = (byte[]) ar.result; + if (DBG) log("CSIM_SPN=" + + IccUtils.bytesToHexString(data)); + + // C.S0065 for EF_SPN decoding + mCsimSpnDisplayCondition = ((0x01 & data[0]) != 0); + + int encoding = data[1]; + int language = data[2]; + byte[] spnData = new byte[32]; + System.arraycopy(data, 3, spnData, 0, (data.length < 32) ? data.length : 32); + + int numBytes; + for (numBytes = 0; numBytes < spnData.length; numBytes++) { + if ((spnData[numBytes] & 0xFF) == 0xFF) break; + } + + if (numBytes == 0) { + spn = ""; + return; + } + try { + switch (encoding) { + case UserData.ENCODING_OCTET: + case UserData.ENCODING_LATIN: + spn = new String(spnData, 0, numBytes, "ISO-8859-1"); + break; + case UserData.ENCODING_IA5: + case UserData.ENCODING_GSM_7BIT_ALPHABET: + case UserData.ENCODING_7BIT_ASCII: + spn = GsmAlphabet.gsm7BitPackedToString(spnData, 0, (numBytes*8)/7); + break; + case UserData.ENCODING_UNICODE_16: + spn = new String(spnData, 0, numBytes, "utf-16"); + break; + default: + log("SPN encoding not supported"); + } + } catch(Exception e) { + log("spn decode error: " + e); + } + if (DBG) log("spn=" + spn); + if (DBG) log("spnCondition=" + mCsimSpnDisplayCondition); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ALPHA, spn); + } + } + + private class EfCsimMdnLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_MDN"; + } + + public void onRecordLoaded(AsyncResult ar) { + byte[] data = (byte[]) ar.result; + if (DBG) log("CSIM_MDN=" + IccUtils.bytesToHexString(data)); + int mdnDigitsNum = 0x0F & data[0]; + mMdn = IccUtils.cdmaBcdToString(data, 1, mdnDigitsNum); + if (DBG) log("CSIM MDN=" + mMdn); + } + } + + private class EfCsimImsimLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_IMSIM"; + } + + public void onRecordLoaded(AsyncResult ar) { + byte[] data = (byte[]) ar.result; + if (DBG) log("CSIM_IMSIM=" + IccUtils.bytesToHexString(data)); + // C.S0065 section 5.2.2 for IMSI_M encoding + // C.S0005 section 2.3.1 for MIN encoding in IMSI_M. + boolean provisioned = ((data[7] & 0x80) == 0x80); + + if (provisioned) { + int first3digits = ((0x03 & data[2]) << 8) + (0xFF & data[1]); + int second3digits = (((0xFF & data[5]) << 8) | (0xFF & data[4])) >> 6; + int digit7 = 0x0F & (data[4] >> 2); + if (digit7 > 0x09) digit7 = 0; + int last3digits = ((0x03 & data[4]) << 8) | (0xFF & data[3]); + first3digits = adjstMinDigits(first3digits); + second3digits = adjstMinDigits(second3digits); + last3digits = adjstMinDigits(last3digits); + + StringBuilder builder = new StringBuilder(); + builder.append(String.format(Locale.US, "%03d", first3digits)); + builder.append(String.format(Locale.US, "%03d", second3digits)); + builder.append(String.format(Locale.US, "%d", digit7)); + builder.append(String.format(Locale.US, "%03d", last3digits)); + mMin = builder.toString(); + if (DBG) log("min present=" + mMin); + } else { + if (DBG) log("min not present"); + } + } + } + + private class EfCsimCdmaHomeLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_CDMAHOME"; + } + + public void onRecordLoaded(AsyncResult ar) { + // Per C.S0065 section 5.2.8 + ArrayList<byte[]> dataList = (ArrayList<byte[]>) ar.result; + if (DBG) log("CSIM_CDMAHOME data size=" + dataList.size()); + if (dataList.isEmpty()) { + return; + } + StringBuilder sidBuf = new StringBuilder(); + StringBuilder nidBuf = new StringBuilder(); + + for (byte[] data : dataList) { + if (data.length == 5) { + int sid = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF); + int nid = ((data[3] & 0xFF) << 8) | (data[2] & 0xFF); + sidBuf.append(sid).append(','); + nidBuf.append(nid).append(','); + } + } + // remove trailing "," + sidBuf.setLength(sidBuf.length()-1); + nidBuf.setLength(nidBuf.length()-1); + + mHomeSystemId = sidBuf.toString(); + mHomeNetworkId = nidBuf.toString(); + } + } + + private class EfCsimEprlLoaded implements IccRecordLoaded { + public String getEfName() { + return "EF_CSIM_EPRL"; + } + public void onRecordLoaded(AsyncResult ar) { + onGetCSimEprlDone(ar); + } + } + + @Override + protected void onRecordLoaded() { + // One record loaded successfully or failed, In either case + // we need to update the recordsToLoad count + recordsToLoad -= 1; + + if (recordsToLoad == 0 && recordsRequested == true) { + onAllRecordsLoaded(); + } else if (recordsToLoad < 0) { + Log.e(LOG_TAG, "SIMRecords: recordsToLoad <0, programmer error suspected"); + recordsToLoad = 0; + } + } + + @Override + protected void onAllRecordsLoaded() { + setLocaleFromCsim(); + super.onAllRecordsLoaded(); // broadcasts ICC state change to "LOADED" + } + + @Override + protected void fetchSimRecords() { + recordsRequested = true; + + mCi.getIMSIForApp(mParentCard.getAid(), obtainMessage(EVENT_GET_IMSI_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_AD, obtainMessage(EVENT_GET_AD_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_PL, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfPlLoaded())); + recordsToLoad++; + + new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, EF_EXT1, 1, + obtainMessage(EVENT_GET_MSISDN_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_SST, obtainMessage(EVENT_GET_SST_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_CSIM_LI, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimLiLoaded())); + recordsToLoad++; + + mFh.loadEFTransparent(EF_CSIM_SPN, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimSpnLoaded())); + recordsToLoad++; + + mFh.loadEFLinearFixed(EF_CSIM_MDN, 1, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimMdnLoaded())); + recordsToLoad++; + + mFh.loadEFTransparent(EF_CSIM_IMSIM, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimImsimLoaded())); + recordsToLoad++; + + mFh.loadEFLinearFixedAll(EF_CSIM_CDMAHOME, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimCdmaHomeLoaded())); + recordsToLoad++; + + mFh.loadEFTransparent(EF_CSIM_EPRL, + obtainMessage(EVENT_GET_ICC_RECORD_DONE, new EfCsimEprlLoaded())); + recordsToLoad++; + + // load ISIM records + recordsToLoad += mIsimUiccRecords.fetchIsimRecords(mFh, this); + } + + private int adjstMinDigits (int digits) { + // Per C.S0005 section 2.3.1. + digits += 111; + digits = (digits % 10 == 0)?(digits - 10):digits; + digits = ((digits / 10) % 10 == 0)?(digits - 100):digits; + digits = ((digits / 100) % 10 == 0)?(digits - 1000):digits; + return digits; + } + + private void onGetCSimEprlDone(AsyncResult ar) { + // C.S0065 section 5.2.57 for EFeprl encoding + // C.S0016 section 3.5.5 for PRL format. + byte[] data = (byte[]) ar.result; + if (DBG) log("CSIM_EPRL=" + IccUtils.bytesToHexString(data)); + + // Only need the first 4 bytes of record + if (data.length > 3) { + int prlId = ((data[2] & 0xFF) << 8) | (data[3] & 0xFF); + mPrlVersion = Integer.toString(prlId); + } + if (DBG) log("CSIM PRL version=" + mPrlVersion); + } + + private void setLocaleFromCsim() { + String prefLang = null; + // check EFli then EFpl + prefLang = findBestLanguage(mEFli); + + if (prefLang == null) { + prefLang = findBestLanguage(mEFpl); + } + + if (prefLang != null) { + // check country code from SIM + String imsi = getIMSI(); + String country = null; + if (imsi != null) { + country = MccTable.countryCodeForMcc( + Integer.parseInt(imsi.substring(0,3))); + } + log("Setting locale to " + prefLang + "_" + country); + MccTable.setSystemLocale(mContext, prefLang, country); + } else { + log ("No suitable CSIM selected locale"); + } + } + + private String findBestLanguage(byte[] languages) { + String bestMatch = null; + String[] locales = mContext.getAssets().getLocales(); + + if ((languages == null) || (locales == null)) return null; + + // Each 2-bytes consists of one language + for (int i = 0; (i + 1) < languages.length; i += 2) { + try { + String lang = new String(languages, i, 2, "ISO-8859-1"); + for (int j = 0; j < locales.length; j++) { + if (locales[j] != null && locales[j].length() >= 2 && + locales[j].substring(0, 2).equals(lang)) { + return lang; + } + } + if (bestMatch != null) break; + } catch(java.io.UnsupportedEncodingException e) { + log ("Failed to parse SIM language records"); + } + } + // no match found. return null + return null; + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[CSIM] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[CSIM] " + s); + } + + public String getMdn() { + return mMdn; + } + + public String getMin() { + return mMin; + } + + public String getSid() { + return mHomeSystemId; + } + + public String getNid() { + return mHomeNetworkId; + } + + public String getPrlVersion() { + return mPrlVersion; + } + + public boolean getCsimSpnDisplayCondition() { + return mCsimSpnDisplayCondition; + } + + @Override + public IsimRecords getIsimRecords() { + return mIsimUiccRecords; + } + + @Override + public boolean isProvisioned() { + // If UICC card has CSIM app, look for MDN and MIN field + // to determine if the SIM is provisioned. Otherwise, + // consider the SIM is provisioned. (for case of ordinal + // USIM only UICC.) + // If PROPERTY_TEST_CSIM is defined, bypess provision check + // and consider the SIM is provisioned. + if (SystemProperties.getBoolean(PROPERTY_TEST_CSIM, false)) { + return true; + } + + if (mParentCard == null) { + return false; + } + + if (mParentCard.isApplicationOnIcc(AppType.APPTYPE_CSIM) && + ((mMdn == null) || (mMin == null))) { + return false; + } + return true; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaMmiCode.java b/src/java/com/android/internal/telephony/cdma/CdmaMmiCode.java new file mode 100644 index 0000000..8dd8c2e --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaMmiCode.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +import android.content.Context; + +import com.android.internal.telephony.CommandException; +import com.android.internal.telephony.MmiCode; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * This class can handle Puk code Mmi + * + * {@hide} + * + */ +public final class CdmaMmiCode extends Handler implements MmiCode { + static final String LOG_TAG = "CDMA_MMI"; + + // Constants + + // From TS 22.030 6.5.2 + static final String ACTION_REGISTER = "**"; + + // Supp Service codes from TS 22.030 Annex B + static final String SC_PUK = "05"; + + // Event Constant + + static final int EVENT_SET_COMPLETE = 1; + + // Instance Variables + + CDMAPhone phone; + Context context; + + String action; // ACTION_REGISTER + String sc; // Service Code + String sia, sib, sic; // Service Info a,b,c + String poundString; // Entire MMI string up to and including # + String dialingNumber; + String pwd; // For password registration + + State state = State.PENDING; + CharSequence message; + + // Class Variables + + static Pattern sPatternSuppService = Pattern.compile( + "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)(.*)"); +/* 1 2 3 4 5 6 7 8 9 10 11 12 + + 1 = Full string up to and including # + 2 = action + 3 = service code + 5 = SIA + 7 = SIB + 9 = SIC + 10 = dialing number +*/ + + static final int MATCH_GROUP_POUND_STRING = 1; + static final int MATCH_GROUP_ACTION = 2; + static final int MATCH_GROUP_SERVICE_CODE = 3; + static final int MATCH_GROUP_SIA = 5; + static final int MATCH_GROUP_SIB = 7; + static final int MATCH_GROUP_SIC = 9; + static final int MATCH_GROUP_PWD_CONFIRM = 11; + static final int MATCH_GROUP_DIALING_NUMBER = 12; + + + // Public Class methods + + /** + * Check if provided string contains Mmi code in it and create corresponding + * Mmi if it does + */ + + public static CdmaMmiCode + newFromDialString(String dialString, CDMAPhone phone) { + Matcher m; + CdmaMmiCode ret = null; + + m = sPatternSuppService.matcher(dialString); + + // Is this formatted like a standard supplementary service code? + if (m.matches()) { + ret = new CdmaMmiCode(phone); + ret.poundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING)); + ret.action = makeEmptyNull(m.group(MATCH_GROUP_ACTION)); + ret.sc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE)); + ret.sia = makeEmptyNull(m.group(MATCH_GROUP_SIA)); + ret.sib = makeEmptyNull(m.group(MATCH_GROUP_SIB)); + ret.sic = makeEmptyNull(m.group(MATCH_GROUP_SIC)); + ret.pwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM)); + ret.dialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER)); + + } + + return ret; + } + + // Private Class methods + + /** make empty strings be null. + * Regexp returns empty strings for empty groups + */ + private static String + makeEmptyNull (String s) { + if (s != null && s.length() == 0) return null; + + return s; + } + + // Constructor + + CdmaMmiCode (CDMAPhone phone) { + super(phone.getHandler().getLooper()); + this.phone = phone; + this.context = phone.getContext(); + } + + // MmiCode implementation + + public State + getState() { + return state; + } + + public CharSequence + getMessage() { + return message; + } + + // inherited javadoc suffices + public void + cancel() { + // Complete or failed cannot be cancelled + if (state == State.COMPLETE || state == State.FAILED) { + return; + } + + state = State.CANCELLED; + phone.onMMIDone (this); + } + + public boolean isCancelable() { + return false; + } + + // Instance Methods + + /** + * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related + */ + boolean isPukCommand() { + return sc != null && sc.equals(SC_PUK); + } + + boolean isRegister() { + return action != null && action.equals(ACTION_REGISTER); + } + + public boolean isUssdRequest() { + Log.w(LOG_TAG, "isUssdRequest is not implemented in CdmaMmiCode"); + return false; + } + + /** Process a MMI PUK code */ + void + processCode () { + try { + if (isPukCommand()) { + // sia = old PUK + // sib = new PIN + // sic = new PIN + String oldPinOrPuk = sia; + String newPin = sib; + int pinLen = newPin.length(); + if (isRegister()) { + if (!newPin.equals(sic)) { + // password mismatch; return error + handlePasswordError(com.android.internal.R.string.mismatchPin); + } else if (pinLen < 4 || pinLen > 8 ) { + // invalid length + handlePasswordError(com.android.internal.R.string.invalidPin); + } else { + phone.mCM.supplyIccPuk(oldPinOrPuk, newPin, + obtainMessage(EVENT_SET_COMPLETE, this)); + } + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } catch (RuntimeException exc) { + state = State.FAILED; + message = context.getText(com.android.internal.R.string.mmiError); + phone.onMMIDone(this); + } + } + + private void handlePasswordError(int res) { + state = State.FAILED; + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + sb.append(context.getText(res)); + message = sb; + phone.onMMIDone(this); + } + + public void + handleMessage (Message msg) { + AsyncResult ar; + + if (msg.what == EVENT_SET_COMPLETE) { + ar = (AsyncResult) (msg.obj); + onSetComplete(ar); + } else { + Log.e(LOG_TAG, "Unexpected reply"); + } + } + // Private instance methods + + private CharSequence getScString() { + if (sc != null) { + if (isPukCommand()) { + return context.getText(com.android.internal.R.string.PinMmi); + } + } + + return ""; + } + + private void + onSetComplete(AsyncResult ar){ + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + + if (ar.exception != null) { + state = State.FAILED; + if (ar.exception instanceof CommandException) { + CommandException.Error err = ((CommandException)(ar.exception)).getCommandError(); + if (err == CommandException.Error.PASSWORD_INCORRECT) { + if (isPukCommand()) { + sb.append(context.getText( + com.android.internal.R.string.badPuk)); + } else { + sb.append(context.getText( + com.android.internal.R.string.passwordIncorrect)); + } + } else { + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + } else { + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + } else if (isRegister()) { + state = State.COMPLETE; + sb.append(context.getText( + com.android.internal.R.string.serviceRegistered)); + } else { + state = State.FAILED; + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + + message = sb; + phone.onMMIDone(this); + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/src/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java new file mode 100755 index 0000000..0f64e1d --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.ContentValues; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.SQLException; +import android.os.Message; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.provider.Telephony; +import android.provider.Telephony.Sms.Intents; +import android.telephony.SmsCbMessage; +import android.telephony.SmsManager; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SMSDispatcher; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsStorageMonitor; +import com.android.internal.telephony.SmsUsageMonitor; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.WspTypeDecoder; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import android.content.res.Resources; + + +final class CdmaSMSDispatcher extends SMSDispatcher { + private static final String TAG = "CDMA"; + + private byte[] mLastDispatchedSmsFingerprint; + private byte[] mLastAcknowledgedSmsFingerprint; + + private final boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean( + com.android.internal.R.bool.config_duplicate_port_omadm_wappush); + + CdmaSMSDispatcher(CDMAPhone phone, SmsStorageMonitor storageMonitor, + SmsUsageMonitor usageMonitor) { + super(phone, storageMonitor, usageMonitor); + mCm.setOnNewCdmaSms(this, EVENT_NEW_SMS, null); + } + + @Override + public void dispose() { + mCm.unSetOnNewCdmaSms(this); + } + + @Override + protected String getFormat() { + return android.telephony.SmsMessage.FORMAT_3GPP2; + } + + private void handleCdmaStatusReport(SmsMessage sms) { + for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { + SmsTracker tracker = deliveryPendingList.get(i); + if (tracker.mMessageRef == sms.messageRef) { + // Found it. Remove from list and broadcast. + deliveryPendingList.remove(i); + PendingIntent intent = tracker.mDeliveryIntent; + Intent fillIn = new Intent(); + fillIn.putExtra("pdu", sms.getPdu()); + fillIn.putExtra("format", android.telephony.SmsMessage.FORMAT_3GPP2); + try { + intent.send(mContext, Activity.RESULT_OK, fillIn); + } catch (CanceledException ex) {} + break; // Only expect to see one tracker matching this message. + } + } + } + + /** + * Dispatch service category program data to the CellBroadcastReceiver app, which filters + * the broadcast alerts to display. + * @param sms the SMS message containing one or more + * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects. + */ + private void handleServiceCategoryProgramData(SmsMessage sms) { + List<CdmaSmsCbProgramData> programDataList = sms.getSmsCbProgramData(); + if (programDataList == null) { + Log.e(TAG, "handleServiceCategoryProgramData: program data list is null!"); + return; + } + + Intent intent = new Intent(Intents.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION); + intent.putExtra("program_data_list", (CdmaSmsCbProgramData[]) programDataList.toArray()); + dispatch(intent, RECEIVE_SMS_PERMISSION); + } + + /** {@inheritDoc} */ + @Override + public int dispatchMessage(SmsMessageBase smsb) { + + // If sms is null, means there was a parsing error. + if (smsb == null) { + Log.e(TAG, "dispatchMessage: message is null"); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + + String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + if (inEcm.equals("true")) { + return Activity.RESULT_OK; + } + + if (mSmsReceiveDisabled) { + // Device doesn't support receiving SMS, + Log.d(TAG, "Received short message on device which doesn't support " + + "receiving SMS. Ignored."); + return Intents.RESULT_SMS_HANDLED; + } + + SmsMessage sms = (SmsMessage) smsb; + + // Handle CMAS emergency broadcast messages. + if (SmsEnvelope.MESSAGE_TYPE_BROADCAST == sms.getMessageType()) { + Log.d(TAG, "Broadcast type message"); + SmsCbMessage message = sms.parseBroadcastSms(); + if (message != null) { + dispatchBroadcastMessage(message); + } + return Intents.RESULT_SMS_HANDLED; + } + + // See if we have a network duplicate SMS. + mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint(); + if (mLastAcknowledgedSmsFingerprint != null && + Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) { + return Intents.RESULT_SMS_HANDLED; + } + // Decode BD stream and set sms variables. + sms.parseSms(); + int teleService = sms.getTeleService(); + boolean handled = false; + + if ((SmsEnvelope.TELESERVICE_VMN == teleService) || + (SmsEnvelope.TELESERVICE_MWI == teleService)) { + // handling Voicemail + int voicemailCount = sms.getNumOfVoicemails(); + Log.d(TAG, "Voicemail count=" + voicemailCount); + // Store the voicemail count in preferences. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences( + mContext); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(CDMAPhone.VM_COUNT_CDMA, voicemailCount); + editor.apply(); + mPhone.setVoiceMessageWaiting(1, voicemailCount); + handled = true; + } else if (((SmsEnvelope.TELESERVICE_WMT == teleService) || + (SmsEnvelope.TELESERVICE_WEMT == teleService)) && + sms.isStatusReportMessage()) { + handleCdmaStatusReport(sms); + handled = true; + } else if (SmsEnvelope.TELESERVICE_SCPT == teleService) { + handleServiceCategoryProgramData(sms); + handled = true; + } else if ((sms.getUserData() == null)) { + if (false) { + Log.d(TAG, "Received SMS without user data"); + } + handled = true; + } + + if (handled) { + return Intents.RESULT_SMS_HANDLED; + } + + if (!mStorageMonitor.isStorageAvailable() && + sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) { + // It's a storable message and there's no storage available. Bail. + // (See C.S0015-B v2.0 for a description of "Immediate Display" + // messages, which we represent as CLASS_0.) + return Intents.RESULT_SMS_OUT_OF_MEMORY; + } + + if (SmsEnvelope.TELESERVICE_WAP == teleService) { + return processCdmaWapPdu(sms.getUserData(), sms.messageRef, + sms.getOriginatingAddress()); + } + + // Reject (NAK) any messages with teleservice ids that have + // not yet been handled and also do not correspond to the two + // kinds that are processed below. + if ((SmsEnvelope.TELESERVICE_WMT != teleService) && + (SmsEnvelope.TELESERVICE_WEMT != teleService) && + (SmsEnvelope.MESSAGE_TYPE_BROADCAST != sms.getMessageType())) { + return Intents.RESULT_SMS_UNSUPPORTED; + } + + return dispatchNormalMessage(smsb); + } + + /** + * Processes inbound messages that are in the WAP-WDP PDU format. See + * wap-259-wdp-20010614-a section 6.5 for details on the WAP-WDP PDU format. + * WDP segments are gathered until a datagram completes and gets dispatched. + * + * @param pdu The WAP-WDP PDU segment + * @return a result code from {@link Telephony.Sms.Intents}, or + * {@link Activity#RESULT_OK} if the message has been broadcast + * to applications + */ + protected int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address) { + int index = 0; + + int msgType = (0xFF & pdu[index++]); + if (msgType != 0) { + Log.w(TAG, "Received a WAP SMS which is not WDP. Discard."); + return Intents.RESULT_SMS_HANDLED; + } + int totalSegments = (0xFF & pdu[index++]); // >= 1 + int segment = (0xFF & pdu[index++]); // >= 0 + + if (segment >= totalSegments) { + Log.e(TAG, "WDP bad segment #" + segment + " expecting 0-" + (totalSegments - 1)); + return Intents.RESULT_SMS_HANDLED; + } + + // Only the first segment contains sourcePort and destination Port + int sourcePort = 0; + int destinationPort = 0; + if (segment == 0) { + //process WDP segment + sourcePort = (0xFF & pdu[index++]) << 8; + sourcePort |= 0xFF & pdu[index++]; + destinationPort = (0xFF & pdu[index++]) << 8; + destinationPort |= 0xFF & pdu[index++]; + // Some carriers incorrectly send duplicate port fields in omadm wap pushes. + // If configured, check for that here + if (mCheckForDuplicatePortsInOmadmWapPush) { + if (checkDuplicatePortOmadmWappush(pdu,index)) { + index = index + 4; // skip duplicate port fields + } + } + } + + // Lookup all other related parts + Log.i(TAG, "Received WAP PDU. Type = " + msgType + ", originator = " + address + + ", src-port = " + sourcePort + ", dst-port = " + destinationPort + + ", ID = " + referenceNumber + ", segment# = " + segment + '/' + totalSegments); + + // pass the user data portion of the PDU to the shared handler in SMSDispatcher + byte[] userData = new byte[pdu.length - index]; + System.arraycopy(pdu, index, userData, 0, pdu.length - index); + + return processMessagePart(userData, address, referenceNumber, segment, totalSegments, + 0L, destinationPort, true); + } + + /** {@inheritDoc} */ + @Override + protected void sendData(String destAddr, String scAddr, int destPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( + scAddr, destAddr, destPort, data, (deliveryIntent != null)); + sendSubmitPdu(pdu, sentIntent, deliveryIntent, destAddr); + } + + /** {@inheritDoc} */ + @Override + protected void sendText(String destAddr, String scAddr, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( + scAddr, destAddr, text, (deliveryIntent != null), null); + sendSubmitPdu(pdu, sentIntent, deliveryIntent, destAddr); + } + + /** {@inheritDoc} */ + @Override + protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody, + boolean use7bitOnly) { + return SmsMessage.calculateLength(messageBody, use7bitOnly); + } + + /** {@inheritDoc} */ + @Override + protected void sendNewSubmitPdu(String destinationAddress, String scAddress, + String message, SmsHeader smsHeader, int encoding, + PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart) { + UserData uData = new UserData(); + uData.payloadStr = message; + uData.userDataHeader = smsHeader; + if (encoding == SmsConstants.ENCODING_7BIT) { + uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + } else { // assume UTF-16 + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + } + uData.msgEncodingSet = true; + + /* By setting the statusReportRequested bit only for the + * last message fragment, this will result in only one + * callback to the sender when that last fragment delivery + * has been acknowledged. */ + SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destinationAddress, + uData, (deliveryIntent != null) && lastPart); + + sendSubmitPdu(submitPdu, sentIntent, deliveryIntent, destinationAddress); + } + + protected void sendSubmitPdu(SmsMessage.SubmitPdu pdu, + PendingIntent sentIntent, PendingIntent deliveryIntent, String destAddr) { + if (SystemProperties.getBoolean(TelephonyProperties.PROPERTY_INECM_MODE, false)) { + if (sentIntent != null) { + try { + sentIntent.send(SmsManager.RESULT_ERROR_NO_SERVICE); + } catch (CanceledException ex) {} + } + if (false) { + Log.d(TAG, "Block SMS in Emergency Callback mode"); + } + return; + } + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, destAddr); + } + + /** {@inheritDoc} */ + @Override + protected void sendSms(SmsTracker tracker) { + HashMap<String, Object> map = tracker.mData; + + // byte smsc[] = (byte[]) map.get("smsc"); // unused for CDMA + byte pdu[] = (byte[]) map.get("pdu"); + + Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); + mCm.sendCdmaSms(pdu, reply); + } + + /** {@inheritDoc} */ + @Override + protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) { + String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false"); + if (inEcm.equals("true")) { + return; + } + + int causeCode = resultToCause(result); + mCm.acknowledgeLastIncomingCdmaSms(success, causeCode, response); + + if (causeCode == 0) { + mLastAcknowledgedSmsFingerprint = mLastDispatchedSmsFingerprint; + } + mLastDispatchedSmsFingerprint = null; + } + + private static int resultToCause(int rc) { + switch (rc) { + case Activity.RESULT_OK: + case Intents.RESULT_SMS_HANDLED: + // Cause code is ignored on success. + return 0; + case Intents.RESULT_SMS_OUT_OF_MEMORY: + return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE; + case Intents.RESULT_SMS_UNSUPPORTED: + return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID; + case Intents.RESULT_SMS_GENERIC_ERROR: + default: + return CommandsInterface.CDMA_SMS_FAIL_CAUSE_ENCODING_PROBLEM; + } + } + + /** + * Optional check to see if the received WapPush is an OMADM notification with erroneous + * extra port fields. + * - Some carriers make this mistake. + * ex: MSGTYPE-TotalSegments-CurrentSegment + * -SourcePortDestPort-SourcePortDestPort-OMADM PDU + * @param origPdu The WAP-WDP PDU segment + * @param index Current Index while parsing the PDU. + * @return True if OrigPdu is OmaDM Push Message which has duplicate ports. + * False if OrigPdu is NOT OmaDM Push Message which has duplicate ports. + */ + private static boolean checkDuplicatePortOmadmWappush(byte[] origPdu, int index) { + index += 4; + byte[] omaPdu = new byte[origPdu.length - index]; + System.arraycopy(origPdu, index, omaPdu, 0, omaPdu.length); + + WspTypeDecoder pduDecoder = new WspTypeDecoder(omaPdu); + int wspIndex = 2; + + // Process header length field + if (pduDecoder.decodeUintvarInteger(wspIndex) == false) { + return false; + } + + wspIndex += pduDecoder.getDecodedDataLength(); // advance to next field + + // Process content type field + if (pduDecoder.decodeContentType(wspIndex) == false) { + return false; + } + + String mimeType = pduDecoder.getValueString(); + if (mimeType != null && mimeType.equals(WspTypeDecoder.CONTENT_TYPE_B_PUSH_SYNCML_NOTI)) { + return true; + } + return false; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java b/src/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java new file mode 100755 index 0000000..5a4af7a --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaServiceStateTracker.java @@ -0,0 +1,1752 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import com.android.internal.telephony.CommandException; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.ServiceStateTracker; +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.CommandsInterface.RadioState; + +import android.app.AlarmManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.provider.Settings.SettingNotFoundException; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.cdma.CdmaCellLocation; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; +import android.util.TimeUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +/** + * {@hide} + */ +public class CdmaServiceStateTracker extends ServiceStateTracker { + static final String LOG_TAG = "CDMA"; + + CDMAPhone phone; + CdmaCellLocation cellLoc; + CdmaCellLocation newCellLoc; + + // Min values used to by getOtasp() + private static final String UNACTIVATED_MIN2_VALUE = "000000"; + private static final String UNACTIVATED_MIN_VALUE = "1111110111"; + + // Current Otasp value + int mCurrentOtaspMode = OTASP_UNINITIALIZED; + + /** if time between NITZ updates is less than mNitzUpdateSpacing the update may be ignored. */ + private static final int NITZ_UPDATE_SPACING_DEFAULT = 1000 * 60 * 10; + private int mNitzUpdateSpacing = SystemProperties.getInt("ro.nitz_update_spacing", + NITZ_UPDATE_SPACING_DEFAULT); + + /** If mNitzUpdateSpacing hasn't been exceeded but update is > mNitzUpdate do the update */ + private static final int NITZ_UPDATE_DIFF_DEFAULT = 2000; + private int mNitzUpdateDiff = SystemProperties.getInt("ro.nitz_update_diff", + NITZ_UPDATE_DIFF_DEFAULT); + + private boolean mCdmaRoaming = false; + private int mRoamingIndicator; + private boolean mIsInPrl; + private int mDefaultRoamingIndicator; + + /** + * Initially assume no data connection. + */ + protected int mDataConnectionState = ServiceState.STATE_OUT_OF_SERVICE; + protected int mNewDataConnectionState = ServiceState.STATE_OUT_OF_SERVICE; + protected int mRegistrationState = -1; + protected RegistrantList cdmaForSubscriptionInfoReadyRegistrants = new RegistrantList(); + + /** + * Sometimes we get the NITZ time before we know what country we + * are in. Keep the time zone information from the NITZ string so + * we can fix the time zone once know the country. + */ + protected boolean mNeedFixZone = false; + private int mZoneOffset; + private boolean mZoneDst; + private long mZoneTime; + protected boolean mGotCountryCode = false; + String mSavedTimeZone; + long mSavedTime; + long mSavedAtTime; + + /** + * We can't register for SIM_RECORDS_LOADED immediately because the + * SIMRecords object may not be instantiated yet. + */ + private boolean mNeedToRegForRuimLoaded = false; + + /** Wake lock used while setting time of day. */ + private PowerManager.WakeLock mWakeLock; + private static final String WAKELOCK_TAG = "ServiceStateTracker"; + + /** Contains the name of the registered network in CDMA (either ONS or ERI text). */ + protected String mCurPlmn = null; + + protected String mMdn; + protected int mHomeSystemId[] = null; + protected int mHomeNetworkId[] = null; + protected String mMin; + protected String mPrlVersion; + protected boolean mIsMinInfoReady = false; + + private boolean isEriTextLoaded = false; + protected boolean isSubscriptionFromRuim = false; + private CdmaSubscriptionSourceManager mCdmaSSM; + + /* Used only for debugging purposes. */ + private String mRegistrationDeniedReason; + + private ContentResolver cr; + private String currentCarrier = null; + + private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + if (DBG) log("Auto time state changed"); + revertToNitzTime(); + } + }; + + private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + if (DBG) log("Auto time zone state changed"); + revertToNitzTimeZone(); + } + }; + + public CdmaServiceStateTracker(CDMAPhone phone) { + super(); + + this.phone = phone; + cr = phone.getContext().getContentResolver(); + cm = phone.mCM; + ss = new ServiceState(); + newSS = new ServiceState(); + cellLoc = new CdmaCellLocation(); + newCellLoc = new CdmaCellLocation(); + mSignalStrength = new SignalStrength(); + + mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(phone.getContext(), cm, this, + EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null); + isSubscriptionFromRuim = (mCdmaSSM.getCdmaSubscriptionSource() == + CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM); + + PowerManager powerManager = + (PowerManager)phone.getContext().getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG); + + cm.registerForRadioStateChanged(this, EVENT_RADIO_STATE_CHANGED, null); + + cm.registerForVoiceNetworkStateChanged(this, EVENT_NETWORK_STATE_CHANGED_CDMA, null); + cm.setOnNITZTime(this, EVENT_NITZ_TIME, null); + cm.setOnSignalStrengthUpdate(this, EVENT_SIGNAL_STRENGTH_UPDATE, null); + + cm.registerForCdmaPrlChanged(this, EVENT_CDMA_PRL_VERSION_CHANGED, null); + phone.registerForEriFileLoaded(this, EVENT_ERI_FILE_LOADED, null); + cm.registerForCdmaOtaProvision(this,EVENT_OTA_PROVISION_STATUS_CHANGE, null); + + // System setting property AIRPLANE_MODE_ON is set in Settings. + int airplaneMode = Settings.System.getInt(cr, Settings.System.AIRPLANE_MODE_ON, 0); + mDesiredPowerState = ! (airplaneMode > 0); + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.AUTO_TIME), true, + mAutoTimeObserver); + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.AUTO_TIME_ZONE), true, + mAutoTimeZoneObserver); + setSignalStrengthDefaultValues(); + + mNeedToRegForRuimLoaded = true; + } + + public void dispose() { + // Unregister for all events. + cm.unregisterForRadioStateChanged(this); + cm.unregisterForVoiceNetworkStateChanged(this); + phone.getIccCard().unregisterForReady(this); + cm.unregisterForCdmaOtaProvision(this); + phone.unregisterForEriFileLoaded(this); + phone.mIccRecords.unregisterForRecordsLoaded(this); + cm.unSetOnSignalStrengthUpdate(this); + cm.unSetOnNITZTime(this); + cr.unregisterContentObserver(mAutoTimeObserver); + cr.unregisterContentObserver(mAutoTimeZoneObserver); + mCdmaSSM.dispose(this); + cm.unregisterForCdmaPrlChanged(this); + } + + @Override + protected void finalize() { + if (DBG) log("CdmaServiceStateTracker finalized"); + } + + /** + * Registration point for subscription info ready + * @param h handler to notify + * @param what what code of message when delivered + * @param obj placed in Message.obj + */ + public void registerForSubscriptionInfoReady(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + cdmaForSubscriptionInfoReadyRegistrants.add(r); + + if (isMinInfoReady()) { + r.notifyRegistrant(); + } + } + + public void unregisterForSubscriptionInfoReady(Handler h) { + cdmaForSubscriptionInfoReadyRegistrants.remove(h); + } + + /** + * Save current source of cdma subscription + * @param source - 1 for NV, 0 for RUIM + */ + private void saveCdmaSubscriptionSource(int source) { + log("Storing cdma subscription source: " + source); + Secure.putInt(phone.getContext().getContentResolver(), + Secure.CDMA_SUBSCRIPTION_MODE, + source ); + } + + private void getSubscriptionInfoAndStartPollingThreads() { + cm.getCDMASubscription(obtainMessage(EVENT_POLL_STATE_CDMA_SUBSCRIPTION)); + + // Get Registration Information + pollState(); + } + + @Override + public void handleMessage (Message msg) { + AsyncResult ar; + int[] ints; + String[] strings; + + if (!phone.mIsTheCurrentActivePhone) { + loge("Received message " + msg + "[" + msg.what + "]" + + " while being destroyed. Ignoring."); + return; + } + + switch (msg.what) { + case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED: + handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource()); + break; + + case EVENT_RUIM_READY: + // TODO: Consider calling setCurrentPreferredNetworkType as we do in GsmSST. + // cm.setCurrentPreferredNetworkType(); + + // The RUIM is now ready i.e if it was locked it has been + // unlocked. At this stage, the radio is already powered on. + if (mNeedToRegForRuimLoaded) { + phone.mIccRecords.registerForRecordsLoaded(this, + EVENT_RUIM_RECORDS_LOADED, null); + mNeedToRegForRuimLoaded = false; + } + + if (phone.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE) { + // Subscription will be read from SIM I/O + if (DBG) log("Receive EVENT_RUIM_READY"); + pollState(); + } else { + if (DBG) log("Receive EVENT_RUIM_READY and Send Request getCDMASubscription."); + getSubscriptionInfoAndStartPollingThreads(); + } + phone.prepareEri(); + break; + + case EVENT_NV_READY: + // For Non-RUIM phones, the subscription information is stored in + // Non Volatile. Here when Non-Volatile is ready, we can poll the CDMA + // subscription info. + getSubscriptionInfoAndStartPollingThreads(); + break; + + case EVENT_RADIO_STATE_CHANGED: + if(cm.getRadioState() == RadioState.RADIO_ON) { + handleCdmaSubscriptionSource(mCdmaSSM.getCdmaSubscriptionSource()); + + // Signal strength polling stops when radio is off. + queueNextSignalStrengthPoll(); + } + // This will do nothing in the 'radio not available' case. + setPowerStateToDesired(); + pollState(); + break; + + case EVENT_NETWORK_STATE_CHANGED_CDMA: + pollState(); + break; + + case EVENT_GET_SIGNAL_STRENGTH: + // This callback is called when signal strength is polled + // all by itself. + + if (!(cm.getRadioState().isOn())) { + // Polling will continue when radio turns back on. + return; + } + ar = (AsyncResult) msg.obj; + onSignalStrengthResult(ar); + queueNextSignalStrengthPoll(); + + break; + + case EVENT_GET_LOC_DONE_CDMA: + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + String states[] = (String[])ar.result; + int baseStationId = -1; + int baseStationLatitude = CdmaCellLocation.INVALID_LAT_LONG; + int baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG; + int systemId = -1; + int networkId = -1; + + if (states.length > 9) { + try { + if (states[4] != null) { + baseStationId = Integer.parseInt(states[4]); + } + if (states[5] != null) { + baseStationLatitude = Integer.parseInt(states[5]); + } + if (states[6] != null) { + baseStationLongitude = Integer.parseInt(states[6]); + } + // Some carriers only return lat-lngs of 0,0 + if (baseStationLatitude == 0 && baseStationLongitude == 0) { + baseStationLatitude = CdmaCellLocation.INVALID_LAT_LONG; + baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG; + } + if (states[8] != null) { + systemId = Integer.parseInt(states[8]); + } + if (states[9] != null) { + networkId = Integer.parseInt(states[9]); + } + } catch (NumberFormatException ex) { + loge("error parsing cell location data: " + ex); + } + } + + cellLoc.setCellLocationData(baseStationId, baseStationLatitude, + baseStationLongitude, systemId, networkId); + phone.notifyLocationChanged(); + } + + // Release any temporary cell lock, which could have been + // acquired to allow a single-shot location update. + disableSingleLocationUpdate(); + break; + + case EVENT_POLL_STATE_REGISTRATION_CDMA: + case EVENT_POLL_STATE_OPERATOR_CDMA: + ar = (AsyncResult) msg.obj; + handlePollStateResult(msg.what, ar); + break; + + case EVENT_POLL_STATE_CDMA_SUBSCRIPTION: // Handle RIL_CDMA_SUBSCRIPTION + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + String cdmaSubscription[] = (String[])ar.result; + if (cdmaSubscription != null && cdmaSubscription.length >= 5) { + mMdn = cdmaSubscription[0]; + parseSidNid(cdmaSubscription[1], cdmaSubscription[2]); + + mMin = cdmaSubscription[3]; + mPrlVersion = cdmaSubscription[4]; + if (DBG) log("GET_CDMA_SUBSCRIPTION: MDN=" + mMdn); + + mIsMinInfoReady = true; + + updateOtaspState(); + phone.getIccCard().broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_IMSI, null); + } else { + if (DBG) { + log("GET_CDMA_SUBSCRIPTION: error parsing cdmaSubscription params num=" + + cdmaSubscription.length); + } + } + } + break; + + case EVENT_POLL_SIGNAL_STRENGTH: + // Just poll signal strength...not part of pollState() + + cm.getSignalStrength(obtainMessage(EVENT_GET_SIGNAL_STRENGTH)); + break; + + case EVENT_NITZ_TIME: + ar = (AsyncResult) msg.obj; + + String nitzString = (String)((Object[])ar.result)[0]; + long nitzReceiveTime = ((Long)((Object[])ar.result)[1]).longValue(); + + setTimeFromNITZString(nitzString, nitzReceiveTime); + break; + + case EVENT_SIGNAL_STRENGTH_UPDATE: + // This is a notification from CommandsInterface.setOnSignalStrengthUpdate. + + ar = (AsyncResult) msg.obj; + + // The radio is telling us about signal strength changes, + // so we don't have to ask it. + dontPollSignalStrength = true; + + onSignalStrengthResult(ar); + break; + + case EVENT_RUIM_RECORDS_LOADED: + updateSpnDisplay(); + break; + + case EVENT_LOCATION_UPDATES_ENABLED: + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + cm.getVoiceRegistrationState(obtainMessage(EVENT_GET_LOC_DONE_CDMA, null)); + } + break; + + case EVENT_ERI_FILE_LOADED: + // Repoll the state once the ERI file has been loaded. + if (DBG) log("[CdmaServiceStateTracker] ERI file has been loaded, repolling."); + pollState(); + break; + + case EVENT_OTA_PROVISION_STATUS_CHANGE: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + ints = (int[]) ar.result; + int otaStatus = ints[0]; + if (otaStatus == Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED + || otaStatus == Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED) { + if (DBG) log("EVENT_OTA_PROVISION_STATUS_CHANGE: Complete, Reload MDN"); + cm.getCDMASubscription( obtainMessage(EVENT_POLL_STATE_CDMA_SUBSCRIPTION)); + } + } + break; + + case EVENT_CDMA_PRL_VERSION_CHANGED: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + ints = (int[]) ar.result; + mPrlVersion = Integer.toString(ints[0]); + } + break; + + default: + super.handleMessage(msg); + break; + } + } + + //***** Private Instance Methods + + private void handleCdmaSubscriptionSource(int newSubscriptionSource) { + log("Subscription Source : " + newSubscriptionSource); + isSubscriptionFromRuim = + (newSubscriptionSource == CdmaSubscriptionSourceManager.SUBSCRIPTION_FROM_RUIM); + saveCdmaSubscriptionSource(newSubscriptionSource); + if (!isSubscriptionFromRuim) { + // NV is ready when subscription source is NV + sendMessage(obtainMessage(EVENT_NV_READY)); + } else { + phone.getIccCard().registerForReady(this, EVENT_RUIM_READY, null); + } + } + + @Override + protected void setPowerStateToDesired() { + // If we want it on and it's off, turn it on + if (mDesiredPowerState + && cm.getRadioState() == CommandsInterface.RadioState.RADIO_OFF) { + cm.setRadioPower(true, null); + } else if (!mDesiredPowerState && cm.getRadioState().isOn()) { + DataConnectionTracker dcTracker = phone.mDataConnectionTracker; + + // If it's on and available and we want it off gracefully + powerOffRadioSafely(dcTracker); + } // Otherwise, we're in the desired state + } + + @Override + protected void updateSpnDisplay() { + // mOperatorAlphaLong contains the ERI text + String plmn = ss.getOperatorAlphaLong(); + if (!TextUtils.equals(plmn, mCurPlmn)) { + // Allow A blank plmn, "" to set showPlmn to true. Previously, we + // would set showPlmn to true only if plmn was not empty, i.e. was not + // null and not blank. But this would cause us to incorrectly display + // "No Service". Now showPlmn is set to true for any non null string. + boolean showPlmn = plmn != null; + if (DBG) { + log(String.format("updateSpnDisplay: changed sending intent" + + " showPlmn='%b' plmn='%s'", showPlmn, plmn)); + } + Intent intent = new Intent(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra(TelephonyIntents.EXTRA_SHOW_SPN, false); + intent.putExtra(TelephonyIntents.EXTRA_SPN, ""); + intent.putExtra(TelephonyIntents.EXTRA_SHOW_PLMN, showPlmn); + intent.putExtra(TelephonyIntents.EXTRA_PLMN, plmn); + phone.getContext().sendStickyBroadcast(intent); + } + + mCurPlmn = plmn; + } + + @Override + protected Phone getPhone() { + return phone; + } + + /** + * Determine data network type based on radio technology. + */ + protected void setCdmaTechnology(int radioTech){ + mNewDataConnectionState = radioTechnologyToDataServiceState(radioTech); + newSS.setRadioTechnology(radioTech); + mNewRilRadioTechnology = radioTech; + } + + /** + * Hanlde the PollStateResult message + */ + protected void handlePollStateResultMessage(int what, AsyncResult ar){ + int ints[]; + String states[]; + switch (what) { + case EVENT_POLL_STATE_REGISTRATION_CDMA: // Handle RIL_REQUEST_REGISTRATION_STATE. + states = (String[])ar.result; + + int registrationState = 4; //[0] registrationState + int radioTechnology = -1; //[3] radioTechnology + int baseStationId = -1; //[4] baseStationId + //[5] baseStationLatitude + int baseStationLatitude = CdmaCellLocation.INVALID_LAT_LONG; + //[6] baseStationLongitude + int baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG; + int cssIndicator = 0; //[7] init with 0, because it is treated as a boolean + int systemId = 0; //[8] systemId + int networkId = 0; //[9] networkId + int roamingIndicator = -1; //[10] Roaming indicator + int systemIsInPrl = 0; //[11] Indicates if current system is in PRL + int defaultRoamingIndicator = 0; //[12] Is default roaming indicator from PRL + int reasonForDenial = 0; //[13] Denial reason if registrationState = 3 + + if (states.length >= 14) { + try { + if (states[0] != null) { + registrationState = Integer.parseInt(states[0]); + } + if (states[3] != null) { + radioTechnology = Integer.parseInt(states[3]); + } + if (states[4] != null) { + baseStationId = Integer.parseInt(states[4]); + } + if (states[5] != null) { + baseStationLatitude = Integer.parseInt(states[5]); + } + if (states[6] != null) { + baseStationLongitude = Integer.parseInt(states[6]); + } + // Some carriers only return lat-lngs of 0,0 + if (baseStationLatitude == 0 && baseStationLongitude == 0) { + baseStationLatitude = CdmaCellLocation.INVALID_LAT_LONG; + baseStationLongitude = CdmaCellLocation.INVALID_LAT_LONG; + } + if (states[7] != null) { + cssIndicator = Integer.parseInt(states[7]); + } + if (states[8] != null) { + systemId = Integer.parseInt(states[8]); + } + if (states[9] != null) { + networkId = Integer.parseInt(states[9]); + } + if (states[10] != null) { + roamingIndicator = Integer.parseInt(states[10]); + } + if (states[11] != null) { + systemIsInPrl = Integer.parseInt(states[11]); + } + if (states[12] != null) { + defaultRoamingIndicator = Integer.parseInt(states[12]); + } + if (states[13] != null) { + reasonForDenial = Integer.parseInt(states[13]); + } + } catch (NumberFormatException ex) { + loge("EVENT_POLL_STATE_REGISTRATION_CDMA: error parsing: " + ex); + } + } else { + throw new RuntimeException("Warning! Wrong number of parameters returned from " + + "RIL_REQUEST_REGISTRATION_STATE: expected 14 or more " + + "strings and got " + states.length + " strings"); + } + + mRegistrationState = registrationState; + // When registration state is roaming and TSB58 + // roaming indicator is not in the carrier-specified + // list of ERIs for home system, mCdmaRoaming is true. + mCdmaRoaming = + regCodeIsRoaming(registrationState) && !isRoamIndForHomeSystem(states[10]); + newSS.setState (regCodeToServiceState(registrationState)); + + setCdmaTechnology(radioTechnology); + + newSS.setCssIndicator(cssIndicator); + newSS.setSystemAndNetworkId(systemId, networkId); + mRoamingIndicator = roamingIndicator; + mIsInPrl = (systemIsInPrl == 0) ? false : true; + mDefaultRoamingIndicator = defaultRoamingIndicator; + + + // Values are -1 if not available. + newCellLoc.setCellLocationData(baseStationId, baseStationLatitude, + baseStationLongitude, systemId, networkId); + + if (reasonForDenial == 0) { + mRegistrationDeniedReason = ServiceStateTracker.REGISTRATION_DENIED_GEN; + } else if (reasonForDenial == 1) { + mRegistrationDeniedReason = ServiceStateTracker.REGISTRATION_DENIED_AUTH; + } else { + mRegistrationDeniedReason = ""; + } + + if (mRegistrationState == 3) { + if (DBG) log("Registration denied, " + mRegistrationDeniedReason); + } + break; + + case EVENT_POLL_STATE_OPERATOR_CDMA: // Handle RIL_REQUEST_OPERATOR + String opNames[] = (String[])ar.result; + + if (opNames != null && opNames.length >= 3) { + // If the NUMERIC field isn't valid use PROPERTY_CDMA_HOME_OPERATOR_NUMERIC + if ((opNames[2] == null) || (opNames[2].length() < 5) + || ("00000".equals(opNames[2]))) { + opNames[2] = SystemProperties.get( + CDMAPhone.PROPERTY_CDMA_HOME_OPERATOR_NUMERIC, "00000"); + if (DBG) { + log("RIL_REQUEST_OPERATOR.response[2], the numeric, " + + " is bad. Using SystemProperties '" + + CDMAPhone.PROPERTY_CDMA_HOME_OPERATOR_NUMERIC + + "'= " + opNames[2]); + } + } + + if (!isSubscriptionFromRuim) { + // In CDMA in case on NV, the ss.mOperatorAlphaLong is set later with the + // ERI text, so here it is ignored what is coming from the modem. + newSS.setOperatorName(null, opNames[1], opNames[2]); + } else { + newSS.setOperatorName(opNames[0], opNames[1], opNames[2]); + } + } else { + if (DBG) log("EVENT_POLL_STATE_OPERATOR_CDMA: error parsing opNames"); + } + break; + default: + loge("handlePollStateResultMessage: RIL response handle in wrong phone!" + + " Expected CDMA RIL request and get GSM RIL request."); + break; + } + } + + /** + * Handle the result of one of the pollState() - related requests + */ + @Override + protected void handlePollStateResult(int what, AsyncResult ar) { + // Ignore stale requests from last poll. + if (ar.userObj != pollingContext) return; + + if (ar.exception != null) { + CommandException.Error err=null; + + if (ar.exception instanceof CommandException) { + err = ((CommandException)(ar.exception)).getCommandError(); + } + + if (err == CommandException.Error.RADIO_NOT_AVAILABLE) { + // Radio has crashed or turned off. + cancelPollState(); + return; + } + + if (!cm.getRadioState().isOn()) { + // Radio has crashed or turned off. + cancelPollState(); + return; + } + + if (err != CommandException.Error.OP_NOT_ALLOWED_BEFORE_REG_NW) { + loge("handlePollStateResult: RIL returned an error where it must succeed" + + ar.exception); + } + } else try { + handlePollStateResultMessage(what, ar); + } catch (RuntimeException ex) { + loge("handlePollStateResult: Exception while polling service state. " + + "Probably malformed RIL response." + ex); + } + + pollingContext[0]--; + + if (pollingContext[0] == 0) { + boolean namMatch = false; + if (!isSidsAllZeros() && isHomeSid(newSS.getSystemId())) { + namMatch = true; + } + + // Setting SS Roaming (general) + if (isSubscriptionFromRuim) { + newSS.setRoaming(isRoamingBetweenOperators(mCdmaRoaming, newSS)); + } else { + newSS.setRoaming(mCdmaRoaming); + } + + // Setting SS CdmaRoamingIndicator and CdmaDefaultRoamingIndicator + newSS.setCdmaDefaultRoamingIndicator(mDefaultRoamingIndicator); + newSS.setCdmaRoamingIndicator(mRoamingIndicator); + boolean isPrlLoaded = true; + if (TextUtils.isEmpty(mPrlVersion)) { + isPrlLoaded = false; + } + if (!isPrlLoaded) { + newSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_OFF); + } else if (!isSidsAllZeros()) { + if (!namMatch && !mIsInPrl) { + // Use default + newSS.setCdmaRoamingIndicator(mDefaultRoamingIndicator); + } else if (namMatch && !mIsInPrl) { + newSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_FLASH); + } else if (!namMatch && mIsInPrl) { + // Use the one from PRL/ERI + newSS.setCdmaRoamingIndicator(mRoamingIndicator); + } else { + // It means namMatch && mIsInPrl + if ((mRoamingIndicator <= 2)) { + newSS.setCdmaRoamingIndicator(EriInfo.ROAMING_INDICATOR_OFF); + } else { + // Use the one from PRL/ERI + newSS.setCdmaRoamingIndicator(mRoamingIndicator); + } + } + } + + int roamingIndicator = newSS.getCdmaRoamingIndicator(); + newSS.setCdmaEriIconIndex(phone.mEriManager.getCdmaEriIconIndex(roamingIndicator, + mDefaultRoamingIndicator)); + newSS.setCdmaEriIconMode(phone.mEriManager.getCdmaEriIconMode(roamingIndicator, + mDefaultRoamingIndicator)); + + // NOTE: Some operator may require overriding mCdmaRoaming + // (set by the modem), depending on the mRoamingIndicator. + + if (DBG) { + log("Set CDMA Roaming Indicator to: " + newSS.getCdmaRoamingIndicator() + + ". mCdmaRoaming = " + mCdmaRoaming + ", isPrlLoaded = " + isPrlLoaded + + ". namMatch = " + namMatch + " , mIsInPrl = " + mIsInPrl + + ", mRoamingIndicator = " + mRoamingIndicator + + ", mDefaultRoamingIndicator= " + mDefaultRoamingIndicator); + } + pollStateDone(); + } + + } + + protected void setSignalStrengthDefaultValues() { + mSignalStrength = new SignalStrength(99, -1, -1, -1, -1, -1, -1, false); + } + + /** + * A complete "service state" from our perspective is + * composed of a handful of separate requests to the radio. + * + * We make all of these requests at once, but then abandon them + * and start over again if the radio notifies us that some + * event has changed + */ + protected void + pollState() { + pollingContext = new int[1]; + pollingContext[0] = 0; + + switch (cm.getRadioState()) { + case RADIO_UNAVAILABLE: + newSS.setStateOutOfService(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + + pollStateDone(); + break; + + case RADIO_OFF: + newSS.setStateOff(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + + pollStateDone(); + break; + + default: + // Issue all poll-related commands at once, then count + // down the responses which are allowed to arrive + // out-of-order. + + pollingContext[0]++; + // RIL_REQUEST_OPERATOR is necessary for CDMA + cm.getOperator( + obtainMessage(EVENT_POLL_STATE_OPERATOR_CDMA, pollingContext)); + + pollingContext[0]++; + // RIL_REQUEST_VOICE_REGISTRATION_STATE is necessary for CDMA + cm.getVoiceRegistrationState( + obtainMessage(EVENT_POLL_STATE_REGISTRATION_CDMA, pollingContext)); + + break; + } + } + + protected void fixTimeZone(String isoCountryCode) { + TimeZone zone = null; + // If the offset is (0, false) and the time zone property + // is set, use the time zone property rather than GMT. + String zoneName = SystemProperties.get(TIMEZONE_PROPERTY); + if (DBG) { + log("fixTimeZone zoneName='" + zoneName + + "' mZoneOffset=" + mZoneOffset + " mZoneDst=" + mZoneDst + + " iso-cc='" + isoCountryCode + + "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode)); + } + if ((mZoneOffset == 0) && (mZoneDst == false) && (zoneName != null) + && (zoneName.length() > 0) + && (Arrays.binarySearch(GMT_COUNTRY_CODES, isoCountryCode) < 0)) { + // For NITZ string without time zone, + // need adjust time to reflect default time zone setting + zone = TimeZone.getDefault(); + if (mNeedFixZone) { + long ctm = System.currentTimeMillis(); + long tzOffset = zone.getOffset(ctm); + if (DBG) { + log("fixTimeZone: tzOffset=" + tzOffset + + " ltod=" + TimeUtils.logTimeOfDay(ctm)); + } + if (getAutoTime()) { + long adj = ctm - tzOffset; + if (DBG) log("fixTimeZone: adj ltod=" + TimeUtils.logTimeOfDay(adj)); + setAndBroadcastNetworkSetTime(adj); + } else { + // Adjust the saved NITZ time to account for tzOffset. + mSavedTime = mSavedTime - tzOffset; + if (DBG) log("fixTimeZone: adj mSavedTime=" + mSavedTime); + } + } + if (DBG) log("fixTimeZone: using default TimeZone"); + } else if (isoCountryCode.equals("")) { + // Country code not found. This is likely a test network. + // Get a TimeZone based only on the NITZ parameters (best guess). + zone = getNitzTimeZone(mZoneOffset, mZoneDst, mZoneTime); + if (DBG) log("fixTimeZone: using NITZ TimeZone"); + } else { + zone = TimeUtils.getTimeZone(mZoneOffset, mZoneDst, mZoneTime, isoCountryCode); + if (DBG) log("fixTimeZone: using getTimeZone(off, dst, time, iso)"); + } + + mNeedFixZone = false; + + if (zone != null) { + log("fixTimeZone: zone != null zone.getID=" + zone.getID()); + if (getAutoTimeZone()) { + setAndBroadcastNetworkSetTimeZone(zone.getID()); + } else { + log("fixTimeZone: skip changing zone as getAutoTimeZone was false"); + } + saveNitzTimeZone(zone.getID()); + } else { + log("fixTimeZone: zone == null, do nothing for zone"); + } + } + + protected void pollStateDone() { + if (DBG) log("pollStateDone: oldSS=[" + ss + "] newSS=[" + newSS + "]"); + + boolean hasRegistered = + ss.getState() != ServiceState.STATE_IN_SERVICE + && newSS.getState() == ServiceState.STATE_IN_SERVICE; + + boolean hasDeregistered = + ss.getState() == ServiceState.STATE_IN_SERVICE + && newSS.getState() != ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionAttached = + mDataConnectionState != ServiceState.STATE_IN_SERVICE + && mNewDataConnectionState == ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionDetached = + mDataConnectionState == ServiceState.STATE_IN_SERVICE + && mNewDataConnectionState != ServiceState.STATE_IN_SERVICE; + + boolean hasCdmaDataConnectionChanged = + mDataConnectionState != mNewDataConnectionState; + + boolean hasRadioTechnologyChanged = mRilRadioTechnology != mNewRilRadioTechnology; + + boolean hasChanged = !newSS.equals(ss); + + boolean hasRoamingOn = !ss.getRoaming() && newSS.getRoaming(); + + boolean hasRoamingOff = ss.getRoaming() && !newSS.getRoaming(); + + boolean hasLocationChanged = !newCellLoc.equals(cellLoc); + + // Add an event log when connection state changes + if (ss.getState() != newSS.getState() || + mDataConnectionState != mNewDataConnectionState) { + EventLog.writeEvent(EventLogTags.CDMA_SERVICE_STATE_CHANGE, + ss.getState(), mDataConnectionState, + newSS.getState(), mNewDataConnectionState); + } + + ServiceState tss; + tss = ss; + ss = newSS; + newSS = tss; + // clean slate for next time + newSS.setStateOutOfService(); + + CdmaCellLocation tcl = cellLoc; + cellLoc = newCellLoc; + newCellLoc = tcl; + + mDataConnectionState = mNewDataConnectionState; + mRilRadioTechnology = mNewRilRadioTechnology; + // this new state has been applied - forget it until we get a new new state + mNewRilRadioTechnology = 0; + + newSS.setStateOutOfService(); // clean slate for next time + + if (hasRadioTechnologyChanged) { + phone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, + ServiceState.rilRadioTechnologyToString(mRilRadioTechnology)); + } + + if (hasRegistered) { + mNetworkAttachedRegistrants.notifyRegistrants(); + } + + if (hasChanged) { + if ((cm.getRadioState().isOn()) && (!isSubscriptionFromRuim)) { + String eriText; + // Now the CDMAPhone sees the new ServiceState so it can get the new ERI text + if (ss.getState() == ServiceState.STATE_IN_SERVICE) { + eriText = phone.getCdmaEriText(); + } else { + // Note that ServiceState.STATE_OUT_OF_SERVICE is valid used for + // mRegistrationState 0,2,3 and 4 + eriText = phone.getContext().getText( + com.android.internal.R.string.roamingTextSearching).toString(); + } + ss.setOperatorAlphaLong(eriText); + } + + String operatorNumeric; + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ALPHA, + ss.getOperatorAlphaLong()); + + String prevOperatorNumeric = + SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, ""); + operatorNumeric = ss.getOperatorNumeric(); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, operatorNumeric); + + if (operatorNumeric == null) { + if (DBG) log("operatorNumeric is null"); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, ""); + mGotCountryCode = false; + } else { + String isoCountryCode = ""; + String mcc = operatorNumeric.substring(0, 3); + try{ + isoCountryCode = MccTable.countryCodeForMcc(Integer.parseInt( + operatorNumeric.substring(0,3))); + } catch ( NumberFormatException ex){ + loge("pollStateDone: countryCodeForMcc error" + ex); + } catch ( StringIndexOutOfBoundsException ex) { + loge("pollStateDone: countryCodeForMcc error" + ex); + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, + isoCountryCode); + mGotCountryCode = true; + + if (shouldFixTimeZoneNow(phone, operatorNumeric, prevOperatorNumeric, + mNeedFixZone)) { + fixTimeZone(isoCountryCode); + } + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISROAMING, + ss.getRoaming() ? "true" : "false"); + + updateSpnDisplay(); + phone.notifyServiceStateChanged(ss); + } + + if (hasCdmaDataConnectionAttached) { + mAttachedRegistrants.notifyRegistrants(); + } + + if (hasCdmaDataConnectionDetached) { + mDetachedRegistrants.notifyRegistrants(); + } + + if (hasCdmaDataConnectionChanged || hasRadioTechnologyChanged) { + phone.notifyDataConnection(null); + } + + if (hasRoamingOn) { + mRoamingOnRegistrants.notifyRegistrants(); + } + + if (hasRoamingOff) { + mRoamingOffRegistrants.notifyRegistrants(); + } + + if (hasLocationChanged) { + phone.notifyLocationChanged(); + } + } + + /** + * Returns a TimeZone object based only on parameters from the NITZ string. + */ + private TimeZone getNitzTimeZone(int offset, boolean dst, long when) { + TimeZone guess = findTimeZone(offset, dst, when); + if (guess == null) { + // Couldn't find a proper timezone. Perhaps the DST data is wrong. + guess = findTimeZone(offset, !dst, when); + } + if (DBG) log("getNitzTimeZone returning " + (guess == null ? guess : guess.getID())); + return guess; + } + + private TimeZone findTimeZone(int offset, boolean dst, long when) { + int rawOffset = offset; + if (dst) { + rawOffset -= 3600000; + } + String[] zones = TimeZone.getAvailableIDs(rawOffset); + TimeZone guess = null; + Date d = new Date(when); + for (String zone : zones) { + TimeZone tz = TimeZone.getTimeZone(zone); + if (tz.getOffset(when) == offset && + tz.inDaylightTime(d) == dst) { + guess = tz; + break; + } + } + + return guess; + } + + /** + * TODO: This code is exactly the same as in GsmServiceStateTracker + * and has a TODO to not poll signal strength if screen is off. + * This code should probably be hoisted to the base class so + * the fix, when added, works for both. + */ + private void + queueNextSignalStrengthPoll() { + if (dontPollSignalStrength) { + // The radio is telling us about signal strength changes + // we don't have to ask it + return; + } + + Message msg; + + msg = obtainMessage(); + msg.what = EVENT_POLL_SIGNAL_STRENGTH; + + // TODO Don't poll signal strength if screen is off + sendMessageDelayed(msg, POLL_PERIOD_MILLIS); + } + + /** + * send signal-strength-changed notification if changed + * Called both for solicited and unsolicited signal strength updates + */ + protected void + onSignalStrengthResult(AsyncResult ar) { + SignalStrength oldSignalStrength = mSignalStrength; + + if (ar.exception != null) { + // Most likely radio is resetting/disconnected change to default values. + setSignalStrengthDefaultValues(); + } else { + int[] ints = (int[])ar.result; + int offset = 2; + int cdmaDbm = (ints[offset] > 0) ? -ints[offset] : -120; + int cdmaEcio = (ints[offset+1] > 0) ? -ints[offset+1] : -160; + int evdoRssi = (ints[offset+2] > 0) ? -ints[offset+2] : -120; + int evdoEcio = (ints[offset+3] > 0) ? -ints[offset+3] : -1; + int evdoSnr = ((ints[offset+4] > 0) && (ints[offset+4] <= 8)) ? ints[offset+4] : -1; + + //log(String.format("onSignalStrengthResult cdmaDbm=%d cdmaEcio=%d evdoRssi=%d evdoEcio=%d evdoSnr=%d", + // cdmaDbm, cdmaEcio, evdoRssi, evdoEcio, evdoSnr)); + mSignalStrength = new SignalStrength(99, -1, cdmaDbm, cdmaEcio, + evdoRssi, evdoEcio, evdoSnr, false); + } + + try { + phone.notifySignalStrength(); + } catch (NullPointerException ex) { + loge("onSignalStrengthResult() Phone already destroyed: " + ex + + "SignalStrength not notified"); + } + } + + + protected int radioTechnologyToDataServiceState(int code) { + int retVal = ServiceState.STATE_OUT_OF_SERVICE; + switch(code) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + break; + case 6: // RADIO_TECHNOLOGY_1xRTT + case 7: // RADIO_TECHNOLOGY_EVDO_0 + case 8: // RADIO_TECHNOLOGY_EVDO_A + case 12: // RADIO_TECHNOLOGY_EVDO_B + case 13: // RADIO_TECHNOLOGY_EHRPD + retVal = ServiceState.STATE_IN_SERVICE; + break; + default: + loge("radioTechnologyToDataServiceState: Wrong radioTechnology code."); + break; + } + return(retVal); + } + + /** code is registration state 0-5 from TS 27.007 7.2 */ + protected int + regCodeToServiceState(int code) { + switch (code) { + case 0: // Not searching and not registered + return ServiceState.STATE_OUT_OF_SERVICE; + case 1: + return ServiceState.STATE_IN_SERVICE; + case 2: // 2 is "searching", fall through + case 3: // 3 is "registration denied", fall through + case 4: // 4 is "unknown", not valid in current baseband + return ServiceState.STATE_OUT_OF_SERVICE; + case 5:// 5 is "Registered, roaming" + return ServiceState.STATE_IN_SERVICE; + + default: + loge("regCodeToServiceState: unexpected service state " + code); + return ServiceState.STATE_OUT_OF_SERVICE; + } + } + + public int getCurrentDataConnectionState() { + return mDataConnectionState; + } + + /** + * code is registration state 0-5 from TS 27.007 7.2 + * returns true if registered roam, false otherwise + */ + private boolean + regCodeIsRoaming (int code) { + // 5 is "in service -- roam" + return 5 == code; + } + + /** + * Determine whether a roaming indicator is in the carrier-specified list of ERIs for + * home system + * + * @param roamInd roaming indicator in String + * @return true if the roamInd is in the carrier-specified list of ERIs for home network + */ + private boolean isRoamIndForHomeSystem(String roamInd) { + // retrieve the carrier-specified list of ERIs for home system + String homeRoamIndicators = SystemProperties.get("ro.cdma.homesystem"); + + if (!TextUtils.isEmpty(homeRoamIndicators)) { + // searches through the comma-separated list for a match, + // return true if one is found. + for (String homeRoamInd : homeRoamIndicators.split(",")) { + if (homeRoamInd.equals(roamInd)) { + return true; + } + } + // no matches found against the list! + return false; + } + + // no system property found for the roaming indicators for home system + return false; + } + + /** + * Set roaming state when cdmaRoaming is true and ons is different from spn + * @param cdmaRoaming TS 27.007 7.2 CREG registered roaming + * @param s ServiceState hold current ons + * @return true for roaming state set + */ + private + boolean isRoamingBetweenOperators(boolean cdmaRoaming, ServiceState s) { + String spn = SystemProperties.get(TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA, "empty"); + + // NOTE: in case of RUIM we should completely ignore the ERI data file and + // mOperatorAlphaLong is set from RIL_REQUEST_OPERATOR response 0 (alpha ONS) + String onsl = s.getOperatorAlphaLong(); + String onss = s.getOperatorAlphaShort(); + + boolean equalsOnsl = onsl != null && spn.equals(onsl); + boolean equalsOnss = onss != null && spn.equals(onss); + + return cdmaRoaming && !(equalsOnsl || equalsOnss); + } + + + /** + * nitzReceiveTime is time_t that the NITZ time was posted + */ + + private + void setTimeFromNITZString (String nitz, long nitzReceiveTime) + { + // "yy/mm/dd,hh:mm:ss(+/-)tz" + // tz is in number of quarter-hours + + long start = SystemClock.elapsedRealtime(); + if (DBG) { + log("NITZ: " + nitz + "," + nitzReceiveTime + + " start=" + start + " delay=" + (start - nitzReceiveTime)); + } + + try { + /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone + * offset as well (which we won't worry about until later) */ + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + + c.clear(); + c.set(Calendar.DST_OFFSET, 0); + + String[] nitzSubs = nitz.split("[/:,+-]"); + + int year = 2000 + Integer.parseInt(nitzSubs[0]); + c.set(Calendar.YEAR, year); + + // month is 0 based! + int month = Integer.parseInt(nitzSubs[1]) - 1; + c.set(Calendar.MONTH, month); + + int date = Integer.parseInt(nitzSubs[2]); + c.set(Calendar.DATE, date); + + int hour = Integer.parseInt(nitzSubs[3]); + c.set(Calendar.HOUR, hour); + + int minute = Integer.parseInt(nitzSubs[4]); + c.set(Calendar.MINUTE, minute); + + int second = Integer.parseInt(nitzSubs[5]); + c.set(Calendar.SECOND, second); + + boolean sign = (nitz.indexOf('-') == -1); + + int tzOffset = Integer.parseInt(nitzSubs[6]); + + int dst = (nitzSubs.length >= 8 ) ? Integer.parseInt(nitzSubs[7]) + : 0; + + // The zone offset received from NITZ is for current local time, + // so DST correction is already applied. Don't add it again. + // + // tzOffset += dst * 4; + // + // We could unapply it if we wanted the raw offset. + + tzOffset = (sign ? 1 : -1) * tzOffset * 15 * 60 * 1000; + + TimeZone zone = null; + + // As a special extension, the Android emulator appends the name of + // the host computer's timezone to the nitz string. this is zoneinfo + // timezone name of the form Area!Location or Area!Location!SubLocation + // so we need to convert the ! into / + if (nitzSubs.length >= 9) { + String tzname = nitzSubs[8].replace('!','/'); + zone = TimeZone.getTimeZone( tzname ); + } + + String iso = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY); + + if (zone == null) { + if (mGotCountryCode) { + if (iso != null && iso.length() > 0) { + zone = TimeUtils.getTimeZone(tzOffset, dst != 0, + c.getTimeInMillis(), + iso); + } else { + // We don't have a valid iso country code. This is + // most likely because we're on a test network that's + // using a bogus MCC (eg, "001"), so get a TimeZone + // based only on the NITZ parameters. + zone = getNitzTimeZone(tzOffset, (dst != 0), c.getTimeInMillis()); + } + } + } + + if ((zone == null) || (mZoneOffset != tzOffset) || (mZoneDst != (dst != 0))){ + // We got the time before the country or the zone has changed + // so we don't know how to identify the DST rules yet. Save + // the information and hope to fix it up later. + + mNeedFixZone = true; + mZoneOffset = tzOffset; + mZoneDst = dst != 0; + mZoneTime = c.getTimeInMillis(); + } + if (DBG) { + log("NITZ: tzOffset=" + tzOffset + " dst=" + dst + " zone=" + zone.getID() + + " iso=" + iso + " mGotCountryCode=" + mGotCountryCode + + " mNeedFixZone=" + mNeedFixZone); + } + + if (zone != null) { + if (getAutoTimeZone()) { + setAndBroadcastNetworkSetTimeZone(zone.getID()); + } + saveNitzTimeZone(zone.getID()); + } + + String ignore = SystemProperties.get("gsm.ignore-nitz"); + if (ignore != null && ignore.equals("yes")) { + if (DBG) log("NITZ: Not setting clock because gsm.ignore-nitz is set"); + return; + } + + try { + mWakeLock.acquire(); + + /** + * Correct the NITZ time by how long its taken to get here. + */ + long millisSinceNitzReceived + = SystemClock.elapsedRealtime() - nitzReceiveTime; + + if (millisSinceNitzReceived < 0) { + // Sanity check: something is wrong + if (DBG) { + log("NITZ: not setting time, clock has rolled " + + "backwards since NITZ time was received, " + + nitz); + } + return; + } + + if (millisSinceNitzReceived > Integer.MAX_VALUE) { + // If the time is this far off, something is wrong > 24 days! + if (DBG) { + log("NITZ: not setting time, processing has taken " + + (millisSinceNitzReceived / (1000 * 60 * 60 * 24)) + + " days"); + } + return; + } + + // Note: with range checks above, cast to int is safe + c.add(Calendar.MILLISECOND, (int)millisSinceNitzReceived); + + if (getAutoTime()) { + /** + * Update system time automatically + */ + long gained = c.getTimeInMillis() - System.currentTimeMillis(); + long timeSinceLastUpdate = SystemClock.elapsedRealtime() - mSavedAtTime; + int nitzUpdateSpacing = Settings.Secure.getInt(cr, + Settings.Secure.NITZ_UPDATE_SPACING, mNitzUpdateSpacing); + int nitzUpdateDiff = Settings.Secure.getInt(cr, + Settings.Secure.NITZ_UPDATE_DIFF, mNitzUpdateDiff); + + if ((mSavedAtTime == 0) || (timeSinceLastUpdate > nitzUpdateSpacing) + || (Math.abs(gained) > nitzUpdateDiff)) { + if (DBG) { + log("NITZ: Auto updating time of day to " + c.getTime() + + " NITZ receive delay=" + millisSinceNitzReceived + + "ms gained=" + gained + "ms from " + nitz); + } + + setAndBroadcastNetworkSetTime(c.getTimeInMillis()); + } else { + if (DBG) { + log("NITZ: ignore, a previous update was " + + timeSinceLastUpdate + "ms ago and gained=" + gained + "ms"); + } + return; + } + } + + /** + * Update properties and save the time we did the update + */ + if (DBG) log("NITZ: update nitz time property"); + SystemProperties.set("gsm.nitz.time", String.valueOf(c.getTimeInMillis())); + mSavedTime = c.getTimeInMillis(); + mSavedAtTime = SystemClock.elapsedRealtime(); + } finally { + long end = SystemClock.elapsedRealtime(); + if (DBG) log("NITZ: end=" + end + " dur=" + (end - start)); + mWakeLock.release(); + } + } catch (RuntimeException ex) { + loge("NITZ: Parsing NITZ time " + nitz + " ex=" + ex); + } + } + + private boolean getAutoTime() { + try { + return Settings.System.getInt(cr, Settings.System.AUTO_TIME) > 0; + } catch (SettingNotFoundException snfe) { + return true; + } + } + + private boolean getAutoTimeZone() { + try { + return Settings.System.getInt(cr, Settings.System.AUTO_TIME_ZONE) > 0; + } catch (SettingNotFoundException snfe) { + return true; + } + } + + private void saveNitzTimeZone(String zoneId) { + mSavedTimeZone = zoneId; + } + + /** + * Set the timezone and send out a sticky broadcast so the system can + * determine if the timezone was set by the carrier. + * + * @param zoneId timezone set by carrier + */ + private void setAndBroadcastNetworkSetTimeZone(String zoneId) { + if (DBG) log("setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId); + AlarmManager alarm = + (AlarmManager) phone.getContext().getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone(zoneId); + Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time-zone", zoneId); + phone.getContext().sendStickyBroadcast(intent); + } + + /** + * Set the time and Send out a sticky broadcast so the system can determine + * if the time was set by the carrier. + * + * @param time time set by network + */ + private void setAndBroadcastNetworkSetTime(long time) { + if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms"); + SystemClock.setCurrentTimeMillis(time); + Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time", time); + phone.getContext().sendStickyBroadcast(intent); + } + + private void revertToNitzTime() { + if (Settings.System.getInt(cr, Settings.System.AUTO_TIME, 0) == 0) { + return; + } + if (DBG) { + log("revertToNitzTime: mSavedTime=" + mSavedTime + " mSavedAtTime=" + mSavedAtTime); + } + if (mSavedTime != 0 && mSavedAtTime != 0) { + setAndBroadcastNetworkSetTime(mSavedTime + + (SystemClock.elapsedRealtime() - mSavedAtTime)); + } + } + + private void revertToNitzTimeZone() { + if (Settings.System.getInt(phone.getContext().getContentResolver(), + Settings.System.AUTO_TIME_ZONE, 0) == 0) { + return; + } + if (DBG) log("revertToNitzTimeZone: tz='" + mSavedTimeZone); + if (mSavedTimeZone != null) { + setAndBroadcastNetworkSetTimeZone(mSavedTimeZone); + } + } + + protected boolean isSidsAllZeros() { + if (mHomeSystemId != null) { + for (int i=0; i < mHomeSystemId.length; i++) { + if (mHomeSystemId[i] != 0) { + return false; + } + } + } + return true; + } + + /** + * Check whether a specified system ID that matches one of the home system IDs. + */ + private boolean isHomeSid(int sid) { + if (mHomeSystemId != null) { + for (int i=0; i < mHomeSystemId.length; i++) { + if (sid == mHomeSystemId[i]) { + return true; + } + } + } + return false; + } + + /** + * @return true if phone is camping on a technology + * that could support voice and data simultaneously. + */ + public boolean isConcurrentVoiceAndDataAllowed() { + // Note: it needs to be confirmed which CDMA network types + // can support voice and data calls concurrently. + // For the time-being, the return value will be false. + return false; + } + + public String getMdnNumber() { + return mMdn; + } + + public String getCdmaMin() { + return mMin; + } + + /** Returns null if NV is not yet ready */ + public String getPrlVersion() { + return mPrlVersion; + } + + /** + * Returns IMSI as MCC + MNC + MIN + */ + String getImsi() { + // TODO: When RUIM is enabled, IMSI will come from RUIM not build-time props. + String operatorNumeric = SystemProperties.get( + TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC, ""); + + if (!TextUtils.isEmpty(operatorNumeric) && getCdmaMin() != null) { + return (operatorNumeric + getCdmaMin()); + } else { + return null; + } + } + + /** + * Check if subscription data has been assigned to mMin + * + * return true if MIN info is ready; false otherwise. + */ + public boolean isMinInfoReady() { + return mIsMinInfoReady; + } + + /** + * Returns OTASP_UNKNOWN, OTASP_NEEDED or OTASP_NOT_NEEDED + */ + int getOtasp() { + int provisioningState; + if (mMin == null || (mMin.length() < 6)) { + if (DBG) log("getOtasp: bad mMin='" + mMin + "'"); + provisioningState = OTASP_UNKNOWN; + } else { + if ((mMin.equals(UNACTIVATED_MIN_VALUE) + || mMin.substring(0,6).equals(UNACTIVATED_MIN2_VALUE)) + || SystemProperties.getBoolean("test_cdma_setup", false)) { + provisioningState = OTASP_NEEDED; + } else { + provisioningState = OTASP_NOT_NEEDED; + } + } + if (DBG) log("getOtasp: state=" + provisioningState); + return provisioningState; + } + + @Override + protected void hangupAndPowerOff() { + // hang up all active voice calls + phone.mCT.ringingCall.hangupIfAlive(); + phone.mCT.backgroundCall.hangupIfAlive(); + phone.mCT.foregroundCall.hangupIfAlive(); + cm.setRadioPower(false, null); + } + + protected void parseSidNid (String sidStr, String nidStr) { + if (sidStr != null) { + String[] sid = sidStr.split(","); + mHomeSystemId = new int[sid.length]; + for (int i = 0; i < sid.length; i++) { + try { + mHomeSystemId[i] = Integer.parseInt(sid[i]); + } catch (NumberFormatException ex) { + loge("error parsing system id: " + ex); + } + } + } + if (DBG) log("CDMA_SUBSCRIPTION: SID=" + sidStr); + + if (nidStr != null) { + String[] nid = nidStr.split(","); + mHomeNetworkId = new int[nid.length]; + for (int i = 0; i < nid.length; i++) { + try { + mHomeNetworkId[i] = Integer.parseInt(nid[i]); + } catch (NumberFormatException ex) { + loge("CDMA_SUBSCRIPTION: error parsing network id: " + ex); + } + } + } + if (DBG) log("CDMA_SUBSCRIPTION: NID=" + nidStr); + } + + protected void updateOtaspState() { + int otaspMode = getOtasp(); + int oldOtaspMode = mCurrentOtaspMode; + mCurrentOtaspMode = otaspMode; + + // Notify apps subscription info is ready + if (cdmaForSubscriptionInfoReadyRegistrants != null) { + if (DBG) log("CDMA_SUBSCRIPTION: call notifyRegistrants()"); + cdmaForSubscriptionInfoReadyRegistrants.notifyRegistrants(); + } + if (oldOtaspMode != mCurrentOtaspMode) { + if (DBG) { + log("CDMA_SUBSCRIPTION: call notifyOtaspChanged old otaspMode=" + + oldOtaspMode + " new otaspMode=" + mCurrentOtaspMode); + } + phone.notifyOtaspChanged(mCurrentOtaspMode); + } + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[CdmaSST] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[CdmaSST] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("CdmaServiceStateTracker extends:"); + super.dump(fd, pw, args); + pw.println(" phone=" + phone); + pw.println(" cellLoc=" + cellLoc); + pw.println(" newCellLoc=" + newCellLoc); + pw.println(" mCurrentOtaspMode=" + mCurrentOtaspMode); + pw.println(" mCdmaRoaming=" + mCdmaRoaming); + pw.println(" mRoamingIndicator=" + mRoamingIndicator); + pw.println(" mIsInPrl=" + mIsInPrl); + pw.println(" mDefaultRoamingIndicator=" + mDefaultRoamingIndicator); + pw.println(" mDataConnectionState=" + mDataConnectionState); + pw.println(" mNewDataConnectionState=" + mNewDataConnectionState); + pw.println(" mRegistrationState=" + mRegistrationState); + pw.println(" mNeedFixZone=" + mNeedFixZone); + pw.println(" mZoneOffset=" + mZoneOffset); + pw.println(" mZoneDst=" + mZoneDst); + pw.println(" mZoneTime=" + mZoneTime); + pw.println(" mGotCountryCode=" + mGotCountryCode); + pw.println(" mSavedTimeZone=" + mSavedTimeZone); + pw.println(" mSavedTime=" + mSavedTime); + pw.println(" mSavedAtTime=" + mSavedAtTime); + pw.println(" mNeedToRegForRuimLoaded=" + mNeedToRegForRuimLoaded); + pw.println(" mWakeLock=" + mWakeLock); + pw.println(" mCurPlmn=" + mCurPlmn); + pw.println(" mMdn=" + mMdn); + pw.println(" mHomeSystemId=" + mHomeSystemId); + pw.println(" mHomeNetworkId=" + mHomeNetworkId); + pw.println(" mMin=" + mMin); + pw.println(" mPrlVersion=" + mPrlVersion); + pw.println(" mIsMinInfoReady=" + mIsMinInfoReady); + pw.println(" isEriTextLoaded=" + isEriTextLoaded); + pw.println(" isSubscriptionFromRuim=" + isSubscriptionFromRuim); + pw.println(" mCdmaSSM=" + mCdmaSSM); + pw.println(" mRegistrationDeniedReason=" + mRegistrationDeniedReason); + pw.println(" currentCarrier=" + currentCarrier); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java b/src/java/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java new file mode 100644 index 0000000..80af9d4 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/CdmaSubscriptionSourceManager.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2011 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.internal.telephony.cdma; + +import java.util.concurrent.atomic.AtomicInteger; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.RILConstants; + +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.provider.Settings; +import android.util.Log; + +/** + * Class that handles the CDMA subscription source changed events from RIL + */ +public class CdmaSubscriptionSourceManager extends Handler { + static final String LOG_TAG = "CDMA"; + private static final int EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED = 1; + private static final int EVENT_GET_CDMA_SUBSCRIPTION_SOURCE = 2; + private static final int EVENT_RADIO_ON = 3; + + public static final int SUBSCRIPTION_SOURCE_UNKNOWN = -1; + public static final int SUBSCRIPTION_FROM_RUIM = 0; /* CDMA subscription from RUIM */ + public static final int SUBSCRIPTION_FROM_NV = 1; /* CDMA subscription from NV */ + public static final int PREFERRED_CDMA_SUBSCRIPTION = SUBSCRIPTION_FROM_NV; + + private static CdmaSubscriptionSourceManager sInstance; + private static final Object sReferenceCountMonitor = new Object(); + private static int sReferenceCount = 0; + + // ***** Instance Variables + private CommandsInterface mCM; + private Context mContext; + private RegistrantList mCdmaSubscriptionSourceChangedRegistrants = new RegistrantList(); + + // Type of CDMA subscription source + private AtomicInteger mCdmaSubscriptionSource = new AtomicInteger(SUBSCRIPTION_FROM_NV); + + // Constructor + private CdmaSubscriptionSourceManager(Context context, CommandsInterface ci) { + mContext = context; + mCM = ci; + mCM.registerForCdmaSubscriptionChanged(this, EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED, null); + mCM.registerForOn(this, EVENT_RADIO_ON, null); + int subscriptionSource = getDefaultCdmaSubscriptionSource(); + mCdmaSubscriptionSource.set(subscriptionSource); + } + + /** + * This function creates a single instance of this class + * + * @return object of type CdmaSubscriptionSourceManager + */ + public static CdmaSubscriptionSourceManager getInstance(Context context, + CommandsInterface ci, Handler h, int what, Object obj) { + synchronized (sReferenceCountMonitor) { + if (null == sInstance) { + sInstance = new CdmaSubscriptionSourceManager(context, ci); + } + sInstance.sReferenceCount++; + } + sInstance.registerForCdmaSubscriptionSourceChanged(h, what, obj); + return sInstance; + } + + /** + * Unregisters for the registered event with RIL + */ + public void dispose(Handler h) { + mCdmaSubscriptionSourceChangedRegistrants.remove(h); + synchronized (sReferenceCountMonitor) { + sReferenceCount--; + if (sReferenceCount <= 0) { + mCM.unregisterForCdmaSubscriptionChanged(this); + mCM.unregisterForOn(this); + sInstance = null; + } + } + } + + /* + * (non-Javadoc) + * @see android.os.Handler#handleMessage(android.os.Message) + */ + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + switch (msg.what) { + case EVENT_CDMA_SUBSCRIPTION_SOURCE_CHANGED: + case EVENT_GET_CDMA_SUBSCRIPTION_SOURCE: + { + log("CDMA_SUBSCRIPTION_SOURCE event = " + msg.what); + ar = (AsyncResult) msg.obj; + handleGetCdmaSubscriptionSource(ar); + } + break; + case EVENT_RADIO_ON: { + mCM.getCdmaSubscriptionSource(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_SOURCE)); + } + break; + default: + super.handleMessage(msg); + } + } + + /** + * Returns the current CDMA subscription source value + * @return CDMA subscription source value + */ + public int getCdmaSubscriptionSource() { + return mCdmaSubscriptionSource.get(); + } + + /** + * Gets the default CDMA subscription source + * + * @return Default CDMA subscription source from Settings DB if present. + */ + private int getDefaultCdmaSubscriptionSource() { + // Get the default value from the Settings + int subscriptionSource = Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.CDMA_SUBSCRIPTION_MODE, PREFERRED_CDMA_SUBSCRIPTION); + return subscriptionSource; + } + + /** + * Clients automatically register for CDMA subscription source changed event + * when they get an instance of this object. + */ + private void registerForCdmaSubscriptionSourceChanged(Handler h, int what, Object obj) { + Registrant r = new Registrant (h, what, obj); + mCdmaSubscriptionSourceChangedRegistrants.add(r); + } + + /** + * Handles the call to get the subscription source + * + * @param ar AsyncResult object that contains the result of get CDMA + * subscription source call + */ + private void handleGetCdmaSubscriptionSource(AsyncResult ar) { + if ((ar.exception == null) && (ar.result != null)) { + int newSubscriptionSource = ((int[]) ar.result)[0]; + + if (newSubscriptionSource != mCdmaSubscriptionSource.get()) { + log("Subscription Source Changed : " + mCdmaSubscriptionSource + " >> " + + newSubscriptionSource); + mCdmaSubscriptionSource.set(newSubscriptionSource); + + // Notify registrants of the new CDMA subscription source + mCdmaSubscriptionSourceChangedRegistrants.notifyRegistrants(new AsyncResult(null, + null, null)); + } + } else { + // GET_CDMA_SUBSCRIPTION is returning Failure. Probably + // because modem created GSM Phone. If modem created + // GSMPhone, then PhoneProxy will trigger a change in + // Phone objects and this object will be destroyed. + logw("Unable to get CDMA Subscription Source, Exception: " + ar.exception + + ", result: " + ar.result); + } + } + + private void log(String s) { + Log.d(LOG_TAG, "[CdmaSSM] " + s); + } + + private void loge(String s) { + Log.e(LOG_TAG, "[CdmaSSM] " + s); + } + + private void logw(String s) { + Log.w(LOG_TAG, "[CdmaSSM] " + s); + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/EriInfo.java b/src/java/com/android/internal/telephony/cdma/EriInfo.java new file mode 100644 index 0000000..3e5d37e --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/EriInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2009 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.internal.telephony.cdma; + +public final class EriInfo { + + public static final int ROAMING_INDICATOR_ON = 0; + public static final int ROAMING_INDICATOR_OFF = 1; + public static final int ROAMING_INDICATOR_FLASH = 2; + + public static final int ROAMING_ICON_MODE_NORMAL = 0; + public static final int ROAMING_ICON_MODE_FLASH = 1; + + public int mRoamingIndicator; + public int mIconIndex; + public int mIconMode; + public String mEriText; + public int mCallPromptId; + public int mAlertId; + + public EriInfo (int roamingIndicator, int iconIndex, int iconMode, String eriText, + int callPromptId, int alertId) { + + this.mRoamingIndicator = roamingIndicator; + this.mIconIndex = iconIndex; + this.mIconMode = iconMode; + this.mEriText = eriText; + this.mCallPromptId = callPromptId; + this.mAlertId = alertId; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/EriManager.java b/src/java/com/android/internal/telephony/cdma/EriManager.java new file mode 100644 index 0000000..1bcc90a --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/EriManager.java @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2009 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.internal.telephony.cdma; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.os.Message; +import android.util.Log; +import android.util.Xml; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.util.XmlUtils; + + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; + +/** + * EriManager loads the ERI file definitions and manages the CDMA roaming information. + * + */ +public final class EriManager { + + class EriFile { + + public int mVersionNumber; // File version number + public int mNumberOfEriEntries; // Number of entries + public int mEriFileType; // Eri Phase 0/1 + //public int mNumberOfIconImages; // reserved for future use + //public int mIconImageType; // reserved for future use + public String[] mCallPromptId; // reserved for future use + public HashMap<Integer, EriInfo> mRoamIndTable; // Roaming Indicator Table + + public EriFile() { + this.mVersionNumber = -1; + this.mNumberOfEriEntries = 0; + this.mEriFileType = -1; + this.mCallPromptId = new String[] { "", "", "" }; + this.mRoamIndTable = new HashMap<Integer, EriInfo>(); + } + } + + class EriDisplayInformation { + public int mEriIconIndex; + public int mEriIconMode; + public String mEriIconText; + + public EriDisplayInformation(int eriIconIndex, int eriIconMode, String eriIconText) { + mEriIconIndex = eriIconIndex; + mEriIconMode = eriIconMode; + mEriIconText = eriIconText; + } + +// public void setParameters(int eriIconIndex, int eriIconMode, String eriIconText){ +// this.mEriIconIndex = eriIconIndex; +// this.mEriIconMode = eriIconMode; +// this.mEriIconText = eriIconText; +// } + + @Override + public String toString() { + return "EriDisplayInformation: {" + " IconIndex: " + mEriIconIndex + " EriIconMode: " + + mEriIconMode + " EriIconText: " + mEriIconText + " }"; + } + } + + private static final String LOG_TAG = "CDMA"; + private static final boolean DBG = true; + private static final boolean VDBG = false; + + public static final int ERI_FROM_XML = 0; + public static final int ERI_FROM_FILE_SYSTEM = 1; + public static final int ERI_FROM_MODEM = 2; + + private PhoneBase mPhone; + private Context mContext; + private int mEriFileSource = ERI_FROM_XML; + private boolean isEriFileLoaded; + private EriFile mEriFile; + + public EriManager(PhoneBase phone, Context context, int eriFileSource) { + this.mPhone = phone; + this.mContext = context; + this.mEriFileSource = eriFileSource; + this.mEriFile = new EriFile(); + } + + public void dispose() { + mEriFile = new EriFile(); + isEriFileLoaded = false; + } + + + public void loadEriFile() { + switch (mEriFileSource) { + case ERI_FROM_MODEM: + loadEriFileFromModem(); + break; + + case ERI_FROM_FILE_SYSTEM: + loadEriFileFromFileSystem(); + break; + + case ERI_FROM_XML: + default: + loadEriFileFromXml(); + break; + } + } + + /** + * Load the ERI file from the MODEM through chipset specific RIL_REQUEST_OEM_HOOK + * + * In this case the ERI file can be updated from the Phone Support Tool available + * from the Chipset vendor + */ + private void loadEriFileFromModem() { + // NOT IMPLEMENTED, Chipset vendor/Operator specific + } + + /** + * Load the ERI file from a File System file + * + * In this case the a Phone Support Tool to update the ERI file must be provided + * to the Operator + */ + private void loadEriFileFromFileSystem() { + // NOT IMPLEMENTED, Chipset vendor/Operator specific + } + + /** + * Load the ERI file from the application framework resources encoded in XML + * + */ + private void loadEriFileFromXml() { + XmlPullParser parser = null; + FileInputStream stream = null; + Resources r = mContext.getResources(); + + try { + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: check for alternate file"); + stream = new FileInputStream( + r.getString(com.android.internal.R.string.alternate_eri_file)); + parser = Xml.newPullParser(); + parser.setInput(stream, null); + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: opened alternate file"); + } catch (FileNotFoundException e) { + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: no alternate file"); + parser = null; + } catch (XmlPullParserException e) { + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: no parser for alternate file"); + parser = null; + } + + if (parser == null) { + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: open normal file"); + parser = r.getXml(com.android.internal.R.xml.eri); + } + + try { + XmlUtils.beginDocument(parser, "EriFile"); + mEriFile.mVersionNumber = Integer.parseInt( + parser.getAttributeValue(null, "VersionNumber")); + mEriFile.mNumberOfEriEntries = Integer.parseInt( + parser.getAttributeValue(null, "NumberOfEriEntries")); + mEriFile.mEriFileType = Integer.parseInt( + parser.getAttributeValue(null, "EriFileType")); + + int parsedEriEntries = 0; + while(true) { + XmlUtils.nextElement(parser); + String name = parser.getName(); + if (name == null) { + if (parsedEriEntries != mEriFile.mNumberOfEriEntries) + Log.e(LOG_TAG, "Error Parsing ERI file: " + mEriFile.mNumberOfEriEntries + + " defined, " + parsedEriEntries + " parsed!"); + break; + } else if (name.equals("CallPromptId")) { + int id = Integer.parseInt(parser.getAttributeValue(null, "Id")); + String text = parser.getAttributeValue(null, "CallPromptText"); + if (id >= 0 && id <= 2) { + mEriFile.mCallPromptId[id] = text; + } else { + Log.e(LOG_TAG, "Error Parsing ERI file: found" + id + " CallPromptId"); + } + + } else if (name.equals("EriInfo")) { + int roamingIndicator = Integer.parseInt( + parser.getAttributeValue(null, "RoamingIndicator")); + int iconIndex = Integer.parseInt(parser.getAttributeValue(null, "IconIndex")); + int iconMode = Integer.parseInt(parser.getAttributeValue(null, "IconMode")); + String eriText = parser.getAttributeValue(null, "EriText"); + int callPromptId = Integer.parseInt( + parser.getAttributeValue(null, "CallPromptId")); + int alertId = Integer.parseInt(parser.getAttributeValue(null, "AlertId")); + parsedEriEntries++; + mEriFile.mRoamIndTable.put(roamingIndicator, new EriInfo (roamingIndicator, + iconIndex, iconMode, eriText, callPromptId, alertId)); + } + } + + if (DBG) Log.d(LOG_TAG, "loadEriFileFromXml: eri parsing successful, file loaded"); + isEriFileLoaded = true; + + } catch (Exception e) { + Log.e(LOG_TAG, "Got exception while loading ERI file.", e); + } finally { + if (parser instanceof XmlResourceParser) { + ((XmlResourceParser)parser).close(); + } + try { + if (stream != null) { + stream.close(); + } + } catch (IOException e) { + // Ignore + } + } + } + + /** + * Returns the version of the ERI file + * + */ + public int getEriFileVersion() { + return mEriFile.mVersionNumber; + } + + /** + * Returns the number of ERI entries parsed + * + */ + public int getEriNumberOfEntries() { + return mEriFile.mNumberOfEriEntries; + } + + /** + * Returns the ERI file type value ( 0 for Phase 0, 1 for Phase 1) + * + */ + public int getEriFileType() { + return mEriFile.mEriFileType; + } + + /** + * Returns if the ERI file has been loaded + * + */ + public boolean isEriFileLoaded() { + return isEriFileLoaded; + } + + /** + * Returns the EriInfo record associated with roamingIndicator + * or null if the entry is not found + */ + private EriInfo getEriInfo(int roamingIndicator) { + if (mEriFile.mRoamIndTable.containsKey(roamingIndicator)) { + return mEriFile.mRoamIndTable.get(roamingIndicator); + } else { + return null; + } + } + + private EriDisplayInformation getEriDisplayInformation(int roamInd, int defRoamInd){ + EriDisplayInformation ret; + + // Carrier can use eri.xml to customize any built-in roaming display indications + if (isEriFileLoaded) { + EriInfo eriInfo = getEriInfo(roamInd); + if (eriInfo != null) { + if (VDBG) Log.v(LOG_TAG, "ERI roamInd " + roamInd + " found in ERI file"); + ret = new EriDisplayInformation( + eriInfo.mIconIndex, + eriInfo.mIconMode, + eriInfo.mEriText); + return ret; + } + } + + switch (roamInd) { + // Handling the standard roaming indicator (non-ERI) + case EriInfo.ROAMING_INDICATOR_ON: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_ON, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText0).toString()); + break; + + case EriInfo.ROAMING_INDICATOR_OFF: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_OFF, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText1).toString()); + break; + + case EriInfo.ROAMING_INDICATOR_FLASH: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_FLASH, + EriInfo.ROAMING_ICON_MODE_FLASH, + mContext.getText(com.android.internal.R.string.roamingText2).toString()); + break; + + + // Handling the standard ERI + case 3: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText3).toString()); + break; + + case 4: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText4).toString()); + break; + + case 5: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText5).toString()); + break; + + case 6: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText6).toString()); + break; + + case 7: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText7).toString()); + break; + + case 8: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText8).toString()); + break; + + case 9: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText9).toString()); + break; + + case 10: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText10).toString()); + break; + + case 11: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText11).toString()); + break; + + case 12: + ret = new EriDisplayInformation( + roamInd, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal.R.string.roamingText12).toString()); + break; + + // Handling the non standard Enhanced Roaming Indicator (roamInd > 63) + default: + if (!isEriFileLoaded) { + // ERI file NOT loaded + if (DBG) Log.d(LOG_TAG, "ERI File not loaded"); + if(defRoamInd > 2) { + if (VDBG) Log.v(LOG_TAG, "ERI defRoamInd > 2 ...flashing"); + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_FLASH, + EriInfo.ROAMING_ICON_MODE_FLASH, + mContext.getText(com.android.internal + .R.string.roamingText2).toString()); + } else { + if (VDBG) Log.v(LOG_TAG, "ERI defRoamInd <= 2"); + switch (defRoamInd) { + case EriInfo.ROAMING_INDICATOR_ON: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_ON, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal + .R.string.roamingText0).toString()); + break; + + case EriInfo.ROAMING_INDICATOR_OFF: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_OFF, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal + .R.string.roamingText1).toString()); + break; + + case EriInfo.ROAMING_INDICATOR_FLASH: + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_FLASH, + EriInfo.ROAMING_ICON_MODE_FLASH, + mContext.getText(com.android.internal + .R.string.roamingText2).toString()); + break; + + default: + ret = new EriDisplayInformation(-1, -1, "ERI text"); + } + } + } else { + // ERI file loaded + EriInfo eriInfo = getEriInfo(roamInd); + EriInfo defEriInfo = getEriInfo(defRoamInd); + if (eriInfo == null) { + if (VDBG) { + Log.v(LOG_TAG, "ERI roamInd " + roamInd + + " not found in ERI file ...using defRoamInd " + defRoamInd); + } + if(defEriInfo == null) { + Log.e(LOG_TAG, "ERI defRoamInd " + defRoamInd + + " not found in ERI file ...on"); + ret = new EriDisplayInformation( + EriInfo.ROAMING_INDICATOR_ON, + EriInfo.ROAMING_ICON_MODE_NORMAL, + mContext.getText(com.android.internal + .R.string.roamingText0).toString()); + + } else { + if (VDBG) { + Log.v(LOG_TAG, "ERI defRoamInd " + defRoamInd + " found in ERI file"); + } + ret = new EriDisplayInformation( + defEriInfo.mIconIndex, + defEriInfo.mIconMode, + defEriInfo.mEriText); + } + } else { + if (VDBG) Log.v(LOG_TAG, "ERI roamInd " + roamInd + " found in ERI file"); + ret = new EriDisplayInformation( + eriInfo.mIconIndex, + eriInfo.mIconMode, + eriInfo.mEriText); + } + } + break; + } + if (VDBG) Log.v(LOG_TAG, "Displaying ERI " + ret.toString()); + return ret; + } + + public int getCdmaEriIconIndex(int roamInd, int defRoamInd){ + return getEriDisplayInformation(roamInd, defRoamInd).mEriIconIndex; + } + + public int getCdmaEriIconMode(int roamInd, int defRoamInd){ + return getEriDisplayInformation(roamInd, defRoamInd).mEriIconMode; + } + + public String getCdmaEriText(int roamInd, int defRoamInd){ + return getEriDisplayInformation(roamInd, defRoamInd).mEriIconText; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/RuimFileHandler.java b/src/java/com/android/internal/telephony/cdma/RuimFileHandler.java new file mode 100644 index 0000000..f440935 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/RuimFileHandler.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import android.os.*; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccException; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccFileTypeMismatch; +import com.android.internal.telephony.IccIoResult; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneProxy; + +import java.util.ArrayList; + +/** + * {@hide} + */ +public final class RuimFileHandler extends IccFileHandler { + static final String LOG_TAG = "CDMA"; + + //***** Instance Variables + + //***** Constructor + public RuimFileHandler(IccCard card, String aid, CommandsInterface ci) { + super(card, aid, ci); + } + + protected void finalize() { + Log.d(LOG_TAG, "RuimFileHandler finalized"); + } + + //***** Overridden from IccFileHandler + + @Override + public void loadEFImgTransparent(int fileid, int highOffset, int lowOffset, + int length, Message onLoaded) { + Message response = obtainMessage(EVENT_READ_ICON_DONE, fileid, 0, + onLoaded); + + mCi.iccIOForApp(COMMAND_GET_RESPONSE, fileid, "img", 0, 0, + GET_RESPONSE_EF_IMG_SIZE_BYTES, null, null, + mAid, response); + } + + @Override + public void handleMessage(Message msg) { + + super.handleMessage(msg); + } + + protected String getEFPath(int efid) { + switch(efid) { + case EF_SMS: + case EF_CST: + case EF_RUIM_SPN: + return MF_SIM + DF_CDMA; + } + return getCommonIccEFPath(efid); + } + + protected void logd(String msg) { + Log.d(LOG_TAG, "[RuimFileHandler] " + msg); + } + + protected void loge(String msg) { + Log.e(LOG_TAG, "[RuimFileHandler] " + msg); + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/RuimPhoneBookInterfaceManager.java b/src/java/com/android/internal/telephony/cdma/RuimPhoneBookInterfaceManager.java new file mode 100644 index 0000000..04ee2dd --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/RuimPhoneBookInterfaceManager.java @@ -0,0 +1,79 @@ +/* +** Copyright 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 com.android.internal.telephony.cdma; + +import java.util.concurrent.atomic.AtomicBoolean; + +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.IccPhoneBookInterfaceManager; + +/** + * RuimPhoneBookInterfaceManager to provide an inter-process communication to + * access ADN-like SIM records. + */ + + +public class RuimPhoneBookInterfaceManager extends IccPhoneBookInterfaceManager { + static final String LOG_TAG = "CDMA"; + + public RuimPhoneBookInterfaceManager(CDMAPhone phone) { + super(phone); + adnCache = phone.mIccRecords.getAdnCache(); + //NOTE service "simphonebook" added by IccSmsInterfaceManagerProxy + } + + public void dispose() { + super.dispose(); + } + + protected void finalize() { + try { + super.finalize(); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Error while finalizing:", throwable); + } + if(DBG) Log.d(LOG_TAG, "RuimPhoneBookInterfaceManager finalized"); + } + + public int[] getAdnRecordsSize(int efid) { + if (DBG) logd("getAdnRecordsSize: efid=" + efid); + synchronized(mLock) { + checkThread(); + recordSize = new int[3]; + + //Using mBaseHandler, no difference in EVENT_GET_SIZE_DONE handling + AtomicBoolean status = new AtomicBoolean(false); + Message response = mBaseHandler.obtainMessage(EVENT_GET_SIZE_DONE, status); + + phone.getIccFileHandler().getEFLinearRecordSize(efid, response); + waitForResult(status); + } + + return recordSize; + } + + protected void logd(String msg) { + Log.d(LOG_TAG, "[RuimPbInterfaceManager] " + msg); + } + + protected void loge(String msg) { + Log.e(LOG_TAG, "[RuimPbInterfaceManager] " + msg); + } +} + diff --git a/src/java/com/android/internal/telephony/cdma/RuimRecords.java b/src/java/com/android/internal/telephony/cdma/RuimRecords.java new file mode 100755 index 0000000..e257fb6 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/RuimRecords.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ISO_COUNTRY; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC; +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.telephony.AdnRecord; +import com.android.internal.telephony.AdnRecordCache; +import com.android.internal.telephony.AdnRecordLoader; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccRefreshResponse; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.MccTable; + +// can't be used since VoiceMailConstants is not public +//import com.android.internal.telephony.gsm.VoiceMailConstants; +import com.android.internal.telephony.IccException; +import com.android.internal.telephony.IccRecords; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.PhoneProxy; + + +/** + * {@hide} + */ +public final class RuimRecords extends IccRecords { + static final String LOG_TAG = "CDMA"; + + private static final boolean DBG = true; + private boolean m_ota_commited=false; + + // ***** Instance Variables + + private String mImsi; + private String mMyMobileNumber; + private String mMin2Min1; + + private String mPrlVersion; + + // ***** Event Constants + + private static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = 2; + private static final int EVENT_GET_IMSI_DONE = 3; + private static final int EVENT_GET_DEVICE_IDENTITY_DONE = 4; + private static final int EVENT_GET_ICCID_DONE = 5; + private static final int EVENT_GET_CDMA_SUBSCRIPTION_DONE = 10; + private static final int EVENT_UPDATE_DONE = 14; + private static final int EVENT_GET_SST_DONE = 17; + private static final int EVENT_GET_ALL_SMS_DONE = 18; + private static final int EVENT_MARK_SMS_READ_DONE = 19; + + private static final int EVENT_SMS_ON_RUIM = 21; + private static final int EVENT_GET_SMS_DONE = 22; + + private static final int EVENT_RUIM_REFRESH = 31; + + + public RuimRecords(IccCard card, Context c, CommandsInterface ci) { + super(card, c, ci); + + adnCache = new AdnRecordCache(mFh); + + recordsRequested = false; // No load request is made till SIM ready + + // recordsToLoad is set to 0 because no requests are made yet + recordsToLoad = 0; + + mCi.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + // NOTE the EVENT_SMS_ON_RUIM is not registered + mCi.registerForIccRefresh(this, EVENT_RUIM_REFRESH, null); + + // Start off by setting empty state + onRadioOffOrNotAvailable(); + + } + + @Override + public void dispose() { + if (DBG) log("Disposing RuimRecords " + this); + //Unregister for all events + mCi.unregisterForOffOrNotAvailable( this); + mCi.unregisterForIccRefresh(this); + super.dispose(); + } + + @Override + protected void finalize() { + if(DBG) log("RuimRecords finalized"); + } + + @Override + protected void onRadioOffOrNotAvailable() { + countVoiceMessages = 0; + mncLength = UNINITIALIZED; + iccid = null; + + adnCache.reset(); + + // Don't clean up PROPERTY_ICC_OPERATOR_ISO_COUNTRY and + // PROPERTY_ICC_OPERATOR_NUMERIC here. Since not all CDMA + // devices have RUIM, these properties should keep the original + // values, e.g. build time settings, when there is no RUIM but + // set new values when RUIM is available and loaded. + + // recordsRequested is set to false indicating that the SIM + // read requests made so far are not valid. This is set to + // true only when fresh set of read requests are made. + recordsRequested = false; + } + + public String getMdnNumber() { + return mMyMobileNumber; + } + + public String getCdmaMin() { + return mMin2Min1; + } + + /** Returns null if RUIM is not yet ready */ + public String getPrlVersion() { + return mPrlVersion; + } + + @Override + public void setVoiceMailNumber(String alphaTag, String voiceNumber, Message onComplete){ + // In CDMA this is Operator/OEM dependent + AsyncResult.forMessage((onComplete)).exception = + new IccException("setVoiceMailNumber not implemented"); + onComplete.sendToTarget(); + loge("method setVoiceMailNumber is not implemented"); + } + + /** + * Called by CCAT Service when REFRESH is received. + * @param fileChanged indicates whether any files changed + * @param fileList if non-null, a list of EF files that changed + */ + @Override + public void onRefresh(boolean fileChanged, int[] fileList) { + if (fileChanged) { + // A future optimization would be to inspect fileList and + // only reload those files that we care about. For now, + // just re-fetch all RUIM records that we cache. + fetchRuimRecords(); + } + } + + /** + * Returns the 5 or 6 digit MCC/MNC of the operator that + * provided the RUIM card. Returns null of RUIM is not yet ready + */ + public String getRUIMOperatorNumeric() { + if (mImsi == null) { + return null; + } + + if (mncLength != UNINITIALIZED && mncLength != UNKNOWN) { + // Length = length of MCC + length of MNC + // length of mcc = 3 (3GPP2 C.S0005 - Section 2.3) + return mImsi.substring(0, 3 + mncLength); + } + + // Guess the MNC length based on the MCC if we don't + // have a valid value in ef[ad] + + int mcc = Integer.parseInt(mImsi.substring(0,3)); + return mImsi.substring(0, 3 + MccTable.smallestDigitsMccForMnc(mcc)); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + byte data[]; + + boolean isRecordLoadResponse = false; + + if (mDestroyed) { + loge("Received message " + msg + + "[" + msg.what + "] while being destroyed. Ignoring."); + return; + } + + try { switch (msg.what) { + case EVENT_RADIO_OFF_OR_NOT_AVAILABLE: + onRadioOffOrNotAvailable(); + break; + + case EVENT_GET_DEVICE_IDENTITY_DONE: + log("Event EVENT_GET_DEVICE_IDENTITY_DONE Received"); + break; + + /* IO events */ + case EVENT_GET_IMSI_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + if (ar.exception != null) { + loge("Exception querying IMSI, Exception:" + ar.exception); + break; + } + + mImsi = (String) ar.result; + + // IMSI (MCC+MNC+MSIN) is at least 6 digits, but not more + // than 15 (and usually 15). + if (mImsi != null && (mImsi.length() < 6 || mImsi.length() > 15)) { + loge("invalid IMSI " + mImsi); + mImsi = null; + } + + log("IMSI: " + mImsi.substring(0, 6) + "xxxxxxxxx"); + + String operatorNumeric = getRUIMOperatorNumeric(); + if (operatorNumeric != null) { + if(operatorNumeric.length() <= 6){ + MccTable.updateMccMncConfiguration(mContext, operatorNumeric); + } + } + break; + + case EVENT_GET_CDMA_SUBSCRIPTION_DONE: + ar = (AsyncResult)msg.obj; + String localTemp[] = (String[])ar.result; + if (ar.exception != null) { + break; + } + + mMyMobileNumber = localTemp[0]; + mMin2Min1 = localTemp[3]; + mPrlVersion = localTemp[4]; + + log("MDN: " + mMyMobileNumber + " MIN: " + mMin2Min1); + + break; + + case EVENT_GET_ICCID_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + iccid = IccUtils.bcdToString(data, 0, data.length); + + log("iccid: " + iccid); + + break; + + case EVENT_UPDATE_DONE: + ar = (AsyncResult)msg.obj; + if (ar.exception != null) { + Log.i(LOG_TAG, "RuimRecords update failed", ar.exception); + } + break; + + case EVENT_GET_ALL_SMS_DONE: + case EVENT_MARK_SMS_READ_DONE: + case EVENT_SMS_ON_RUIM: + case EVENT_GET_SMS_DONE: + Log.w(LOG_TAG, "Event not supported: " + msg.what); + break; + + // TODO: probably EF_CST should be read instead + case EVENT_GET_SST_DONE: + log("Event EVENT_GET_SST_DONE Received"); + break; + + case EVENT_RUIM_REFRESH: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + handleRuimRefresh((IccRefreshResponse)ar.result); + } + break; + + }}catch (RuntimeException exc) { + // I don't want these exceptions to be fatal + Log.w(LOG_TAG, "Exception parsing RUIM record", exc); + } finally { + // Count up record load responses even if they are fails + if (isRecordLoadResponse) { + onRecordLoaded(); + } + } + } + + @Override + protected void onRecordLoaded() { + // One record loaded successfully or failed, In either case + // we need to update the recordsToLoad count + recordsToLoad -= 1; + if (DBG) log("RuimRecords:onRecordLoaded " + recordsToLoad + " requested: " + recordsRequested); + + if (recordsToLoad == 0 && recordsRequested == true) { + onAllRecordsLoaded(); + } else if (recordsToLoad < 0) { + loge("RuimRecords: recordsToLoad <0, programmer error suspected"); + recordsToLoad = 0; + } + } + + @Override + protected void onAllRecordsLoaded() { + // Further records that can be inserted are Operator/OEM dependent + + String operator = getRUIMOperatorNumeric(); + log("RuimRecords: onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" + + operator + "'"); + SystemProperties.set(PROPERTY_ICC_OPERATOR_NUMERIC, operator); + + if (mImsi != null) { + SystemProperties.set(PROPERTY_ICC_OPERATOR_ISO_COUNTRY, + MccTable.countryCodeForMcc(Integer.parseInt(mImsi.substring(0,3)))); + } + recordsLoadedRegistrants.notifyRegistrants( + new AsyncResult(null, null, null)); + mParentCard.broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_LOADED, null); + } + + @Override + public void onReady() { + /* broadcast intent ICC_READY here so that we can make sure + READY is sent before IMSI ready + */ + + mParentCard.broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_READY, null); + + fetchRuimRecords(); + + mCi.getCDMASubscription(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_DONE)); + } + + + private void fetchRuimRecords() { + recordsRequested = true; + + Log.v(LOG_TAG, "RuimRecords:fetchRuimRecords " + recordsToLoad); + + mCi.getIMSI(obtainMessage(EVENT_GET_IMSI_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_ICCID, + obtainMessage(EVENT_GET_ICCID_DONE)); + recordsToLoad++; + + log("RuimRecords:fetchRuimRecords " + recordsToLoad + " requested: " + recordsRequested); + // Further records that can be inserted are Operator/OEM dependent + } + + /** + * {@inheritDoc} + * + * No Display rule for RUIMs yet. + */ + @Override + public int getDisplayRule(String plmn) { + // TODO together with spn + return 0; + } + + @Override + public void setVoiceMessageWaiting(int line, int countWaiting) { + if (line != 1) { + // only profile 1 is supported + return; + } + + // range check + if (countWaiting < 0) { + countWaiting = -1; + } else if (countWaiting > 0xff) { + // C.S0015-B v2, 4.5.12 + // range: 0-99 + countWaiting = 0xff; + } + countVoiceMessages = countWaiting; + + mRecordsEventsRegistrants.notifyResult(EVENT_MWI); + } + + private void handleRuimRefresh(IccRefreshResponse refreshResponse) { + if (refreshResponse == null) { + if (DBG) log("handleRuimRefresh received without input"); + return; + } + + if (refreshResponse.aid != null && + !refreshResponse.aid.equals(mParentCard.getAid())) { + // This is for different app. Ignore. + return; + } + + switch (refreshResponse.refreshResult) { + case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE: + if (DBG) log("handleRuimRefresh with SIM_REFRESH_FILE_UPDATED"); + adnCache.reset(); + fetchRuimRecords(); + break; + case IccRefreshResponse.REFRESH_RESULT_INIT: + if (DBG) log("handleRuimRefresh with SIM_REFRESH_INIT"); + // need to reload all files (that we care about) + fetchRuimRecords(); + break; + case IccRefreshResponse.REFRESH_RESULT_RESET: + if (DBG) log("handleRuimRefresh with SIM_REFRESH_RESET"); + mCi.setRadioPower(false, null); + /* Note: no need to call setRadioPower(true). Assuming the desired + * radio power state is still ON (as tracked by ServiceStateTracker), + * ServiceStateTracker will call setRadioPower when it receives the + * RADIO_STATE_CHANGED notification for the power off. And if the + * desired power state has changed in the interim, we don't want to + * override it with an unconditional power on. + */ + break; + default: + // unknown refresh operation + if (DBG) log("handleRuimRefresh with unknown operation"); + break; + } + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[RuimRecords] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[RuimRecords] " + s); + } +} diff --git a/src/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java b/src/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java new file mode 100644 index 0000000..9cd059d --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccSmsInterfaceManager; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.PhoneProxy; +import com.android.internal.telephony.SMSDispatcher; +import com.android.internal.telephony.SmsRawData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static android.telephony.SmsManager.STATUS_ON_ICC_FREE; + +/** + * RuimSmsInterfaceManager to provide an inter-process communication to + * access Sms in Ruim. + */ +public class RuimSmsInterfaceManager extends IccSmsInterfaceManager { + static final String LOG_TAG = "CDMA"; + static final boolean DBG = true; + + private final Object mLock = new Object(); + private boolean mSuccess; + private List<SmsRawData> mSms; + + private static final int EVENT_LOAD_DONE = 1; + private static final int EVENT_UPDATE_DONE = 2; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_UPDATE_DONE: + ar = (AsyncResult) msg.obj; + synchronized (mLock) { + mSuccess = (ar.exception == null); + mLock.notifyAll(); + } + break; + case EVENT_LOAD_DONE: + ar = (AsyncResult)msg.obj; + synchronized (mLock) { + if (ar.exception == null) { + mSms = buildValidRawData((ArrayList<byte[]>) ar.result); + } else { + if(DBG) log("Cannot load Sms records"); + if (mSms != null) + mSms.clear(); + } + mLock.notifyAll(); + } + break; + } + } + }; + + public RuimSmsInterfaceManager(CDMAPhone phone, SMSDispatcher dispatcher) { + super(phone); + mDispatcher = dispatcher; + } + + public void dispose() { + } + + protected void finalize() { + try { + super.finalize(); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Error while finalizing:", throwable); + } + if(DBG) Log.d(LOG_TAG, "RuimSmsInterfaceManager finalized"); + } + + /** + * Update the specified message on the RUIM. + * + * @param index record index of message to update + * @param status new message status (STATUS_ON_ICC_READ, + * STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT, + * STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE) + * @param pdu the raw PDU to store + * @return success or not + * + */ + public boolean + updateMessageOnIccEf(int index, int status, byte[] pdu) { + if (DBG) log("updateMessageOnIccEf: index=" + index + + " status=" + status + " ==> " + + "("+ pdu + ")"); + enforceReceiveAndSend("Updating message on RUIM"); + synchronized(mLock) { + mSuccess = false; + Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE); + + if (status == STATUS_ON_ICC_FREE) { + // Special case FREE: call deleteSmsOnRuim instead of + // manipulating the RUIM record + mPhone.mCM.deleteSmsOnRuim(index, response); + } else { + byte[] record = makeSmsRecordData(status, pdu); + mPhone.getIccFileHandler().updateEFLinearFixed( + IccConstants.EF_SMS, index, record, null, response); + } + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to update by index"); + } + } + return mSuccess; + } + + /** + * Copy a raw SMS PDU to the RUIM. + * + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD, + * STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT) + * @return success or not + * + */ + public boolean copyMessageToIccEf(int status, byte[] pdu, byte[] smsc) { + //NOTE smsc not used in RUIM + if (DBG) log("copyMessageToIccEf: status=" + status + " ==> " + + "pdu=("+ Arrays.toString(pdu) + ")"); + enforceReceiveAndSend("Copying message to RUIM"); + synchronized(mLock) { + mSuccess = false; + Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE); + + mPhone.mCM.writeSmsToRuim(status, IccUtils.bytesToHexString(pdu), + response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to update by index"); + } + } + return mSuccess; + } + + /** + * Retrieves all messages currently stored on RUIM. + */ + public List<SmsRawData> getAllMessagesFromIccEf() { + if (DBG) log("getAllMessagesFromEF"); + + Context context = mPhone.getContext(); + + context.enforceCallingPermission( + "android.permission.RECEIVE_SMS", + "Reading messages from RUIM"); + synchronized(mLock) { + Message response = mHandler.obtainMessage(EVENT_LOAD_DONE); + mPhone.getIccFileHandler().loadEFLinearFixedAll(IccConstants.EF_SMS, response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to load from the RUIM"); + } + } + return mSms; + } + + public boolean enableCellBroadcast(int messageIdentifier) { + // Not implemented + Log.e(LOG_TAG, "Error! Not implemented for CDMA."); + return false; + } + + public boolean disableCellBroadcast(int messageIdentifier) { + // Not implemented + Log.e(LOG_TAG, "Error! Not implemented for CDMA."); + return false; + } + + public boolean enableCellBroadcastRange(int startMessageId, int endMessageId) { + // Not implemented + Log.e(LOG_TAG, "Error! Not implemented for CDMA."); + return false; + } + + public boolean disableCellBroadcastRange(int startMessageId, int endMessageId) { + // Not implemented + Log.e(LOG_TAG, "Error! Not implemented for CDMA."); + return false; + } + + protected void log(String msg) { + Log.d(LOG_TAG, "[RuimSmsInterfaceManager] " + msg); + } +} + diff --git a/src/java/com/android/internal/telephony/cdma/SignalToneUtil.java b/src/java/com/android/internal/telephony/cdma/SignalToneUtil.java new file mode 100644 index 0000000..a149e72 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/SignalToneUtil.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2009 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.internal.telephony.cdma; + +import java.util.HashMap; +import java.util.HashSet; +import android.util.Log; +import android.media.ToneGenerator; + +public class SignalToneUtil { + /** A marker that isn't a valid TONE */ + public static final int CDMA_INVALID_TONE = -1; + + // public final int int IS95_CONST_IR_SIGNAL_TYPE_TYPE; + static public final int IS95_CONST_IR_SIGNAL_TONE = 0; + static public final int IS95_CONST_IR_SIGNAL_ISDN = 1; + static public final int IS95_CONST_IR_SIGNAL_IS54B = 2; + static public final int IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT = 4; + + // public final int int IS95_CONST_IR_ALERT_PITCH_TYPE; + static public final int IS95_CONST_IR_ALERT_MED = 0; + static public final int IS95_CONST_IR_ALERT_HIGH = 1; + static public final int IS95_CONST_IR_ALERT_LOW = 2; + + // Based on 3GPP2 C.S0005-E, seciton 3.7.5.5 Signal, + // set TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN to 0 to avoid + // the alert pitch to be involved in hash calculation for + // signal type other than IS54B. + static public final int TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN = 0; + + // public final int int IS95_CONST_IR_SIGNAL_TYPE; + static public final int IS95_CONST_IR_SIG_ISDN_NORMAL = 0; + static public final int IS95_CONST_IR_SIG_ISDN_INTGRP = 1; + static public final int IS95_CONST_IR_SIG_ISDN_SP_PRI = 2; + static public final int IS95_CONST_IR_SIG_ISDN_PAT_3 = 3; + static public final int IS95_CONST_IR_SIG_ISDN_PING = 4; + static public final int IS95_CONST_IR_SIG_ISDN_PAT_5 = 5; + static public final int IS95_CONST_IR_SIG_ISDN_PAT_6 = 6; + static public final int IS95_CONST_IR_SIG_ISDN_PAT_7 = 7; + static public final int IS95_CONST_IR_SIG_ISDN_OFF = 15; + static public final int IS95_CONST_IR_SIG_TONE_DIAL = 0; + static public final int IS95_CONST_IR_SIG_TONE_RING = 1; + static public final int IS95_CONST_IR_SIG_TONE_INT = 2; + static public final int IS95_CONST_IR_SIG_TONE_ABB_INT = 3; + static public final int IS95_CONST_IR_SIG_TONE_REORDER = 4; + static public final int IS95_CONST_IR_SIG_TONE_ABB_RE = 5; + static public final int IS95_CONST_IR_SIG_TONE_BUSY = 6; + static public final int IS95_CONST_IR_SIG_TONE_CONFIRM = 7; + static public final int IS95_CONST_IR_SIG_TONE_ANSWER = 8; + static public final int IS95_CONST_IR_SIG_TONE_CALL_W = 9; + static public final int IS95_CONST_IR_SIG_TONE_PIP = 10; + static public final int IS95_CONST_IR_SIG_TONE_NO_TONE = 63; + static public final int IS95_CONST_IR_SIG_IS54B_NO_TONE = 0; + static public final int IS95_CONST_IR_SIG_IS54B_L = 1; + static public final int IS95_CONST_IR_SIG_IS54B_SS = 2; + static public final int IS95_CONST_IR_SIG_IS54B_SSL = 3; + static public final int IS95_CONST_IR_SIG_IS54B_SS_2 = 4; + static public final int IS95_CONST_IR_SIG_IS54B_SLS = 5; + static public final int IS95_CONST_IR_SIG_IS54B_S_X4 = 6; + static public final int IS95_CONST_IR_SIG_IS54B_PBX_L = 7; + static public final int IS95_CONST_IR_SIG_IS54B_PBX_SS = 8; + static public final int IS95_CONST_IR_SIG_IS54B_PBX_SSL = 9; + static public final int IS95_CONST_IR_SIG_IS54B_PBX_SLS = 10; + static public final int IS95_CONST_IR_SIG_IS54B_PBX_S_X4 = 11; + static public final int IS95_CONST_IR_SIG_TONE_ABBR_ALRT = 0; + + // Hashmap to map signalInfo To AudioTone + static private HashMap<Integer, Integer> hm = new HashMap<Integer, Integer>(); + + private static Integer signalParamHash(int signalType, int alertPitch, int signal) { + if ((signalType < 0) || (signalType > 256) || (alertPitch > 256) || + (alertPitch < 0) || (signal > 256) || (signal < 0)) { + return new Integer(CDMA_INVALID_TONE); + } + // Based on 3GPP2 C.S0005-E, seciton 3.7.5.5 Signal, + // the alert pitch field is ignored by the mobile station unless + // SIGNAL_TYPE is '10',IS-54B Alerting. + // Set alert pitch to TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN + // so the alert pitch is not involved in hash calculation + // when signal type is not IS-54B. + if (signalType != IS95_CONST_IR_SIGNAL_IS54B) { + alertPitch = TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN; + } + return new Integer(signalType * 256 * 256 + alertPitch * 256 + signal); + } + + public static int getAudioToneFromSignalInfo(int signalType, int alertPitch, int signal) { + Integer result = hm.get(signalParamHash(signalType, alertPitch, signal)); + if (result == null) { + return CDMA_INVALID_TONE; + } + return result; + } + + static { + + /* SIGNAL_TYPE_ISDN */ + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_NORMAL), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_NORMAL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_INTGRP), + ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_INTERGROUP); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_SP_PRI), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_SP_PRI); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_PAT_3), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT3); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_PING), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PING_RING); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_PAT_5), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT5); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_PAT_6), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT6); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_PAT_7), ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PAT7); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_ISDN, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_ISDN_OFF), ToneGenerator.TONE_CDMA_SIGNAL_OFF); + + /* SIGNAL_TYPE_TONE */ + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_DIAL), ToneGenerator.TONE_CDMA_DIAL_TONE_LITE); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_RING), ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_INT), ToneGenerator.TONE_SUP_INTERCEPT); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_ABB_INT), ToneGenerator.TONE_SUP_INTERCEPT_ABBREV); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_REORDER), ToneGenerator.TONE_CDMA_REORDER); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_ABB_RE), ToneGenerator.TONE_CDMA_ABBR_REORDER); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_BUSY), ToneGenerator.TONE_CDMA_NETWORK_BUSY); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_CONFIRM), ToneGenerator.TONE_SUP_CONFIRM); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_ANSWER), ToneGenerator.TONE_CDMA_ANSWER); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_CALL_W), ToneGenerator.TONE_CDMA_NETWORK_CALLWAITING); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_PIP), ToneGenerator.TONE_CDMA_PIP); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_TONE, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_TONE_NO_TONE), ToneGenerator.TONE_CDMA_SIGNAL_OFF); + + /* SIGNAL_TYPE_IS54B */ + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_HIGH_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_MED_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_L), ToneGenerator.TONE_CDMA_LOW_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_HIGH_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_MED_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_SS), ToneGenerator.TONE_CDMA_LOW_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_HIGH_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_MED_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_SSL), ToneGenerator.TONE_CDMA_LOW_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_HIGH_SS_2); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_MED_SS_2); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_SS_2), ToneGenerator.TONE_CDMA_LOW_SS_2); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_HIGH_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_MED_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_SLS), ToneGenerator.TONE_CDMA_LOW_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_HIGH_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_MED_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_S_X4), ToneGenerator.TONE_CDMA_LOW_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_HIGH_PBX_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_MED_PBX_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_PBX_L), ToneGenerator.TONE_CDMA_LOW_PBX_L); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_HIGH_PBX_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_MED_PBX_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_PBX_SS), ToneGenerator.TONE_CDMA_LOW_PBX_SS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_HIGH_PBX_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_MED_PBX_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_PBX_SSL), ToneGenerator.TONE_CDMA_LOW_PBX_SSL); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_HIGH_PBX_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_MED_PBX_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_PBX_SLS), ToneGenerator.TONE_CDMA_LOW_PBX_SLS); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_HIGH, + IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_HIGH_PBX_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_MED, + IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_MED_PBX_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, IS95_CONST_IR_ALERT_LOW, + IS95_CONST_IR_SIG_IS54B_PBX_S_X4), ToneGenerator.TONE_CDMA_LOW_PBX_S_X4); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_IS54B, TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, + IS95_CONST_IR_SIG_IS54B_NO_TONE), ToneGenerator.TONE_CDMA_SIGNAL_OFF); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT, + TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, IS95_CONST_IR_SIG_TONE_ABBR_ALRT), + ToneGenerator.TONE_CDMA_ABBR_ALERT); + + hm.put(signalParamHash(IS95_CONST_IR_SIGNAL_USR_DEFD_ALERT, + TAPIAMSSCDMA_SIGNAL_PITCH_UNKNOWN, IS95_CONST_IR_SIG_TONE_NO_TONE), + ToneGenerator.TONE_CDMA_ABBR_ALERT); + + } + + // suppress default constructor for noninstantiability + private SignalToneUtil() { + } +} diff --git a/src/java/com/android/internal/telephony/cdma/SmsMessage.java b/src/java/com/android/internal/telephony/cdma/SmsMessage.java new file mode 100644 index 0000000..6254a50 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/SmsMessage.java @@ -0,0 +1,998 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma; + +import android.os.Parcel; +import android.os.SystemProperties; +import android.telephony.PhoneNumberUtils; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.cdma.sms.BearerData; +import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.BitwiseInputStream; +import com.android.internal.util.HexDump; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.List; + +/** + * TODO(cleanup): these constants are disturbing... are they not just + * different interpretations on one number? And if we did not have + * terrible class name overlap, they would not need to be directly + * imported like this. The class in this file could just as well be + * named CdmaSmsMessage, could it not? + */ + +/** + * TODO(cleanup): internally returning null in many places makes + * debugging very hard (among many other reasons) and should be made + * more meaningful (replaced with exceptions for example). Null + * returns should only occur at the very outside of the module/class + * scope. + */ + +/** + * A Short Message Service message. + * + */ +public class SmsMessage extends SmsMessageBase { + static final String LOG_TAG = "CDMA"; + static private final String LOGGABLE_TAG = "CDMA:SMS"; + + private final static byte TELESERVICE_IDENTIFIER = 0x00; + private final static byte SERVICE_CATEGORY = 0x01; + private final static byte ORIGINATING_ADDRESS = 0x02; + private final static byte ORIGINATING_SUB_ADDRESS = 0x03; + private final static byte DESTINATION_ADDRESS = 0x04; + private final static byte DESTINATION_SUB_ADDRESS = 0x05; + private final static byte BEARER_REPLY_OPTION = 0x06; + private final static byte CAUSE_CODES = 0x07; + private final static byte BEARER_DATA = 0x08; + + /** + * Status of a previously submitted SMS. + * This field applies to SMS Delivery Acknowledge messages. 0 indicates success; + * Here, the error class is defined by the bits from 9-8, the status code by the bits from 7-0. + * See C.S0015-B, v2.0, 4.5.21 for a detailed description of possible values. + */ + private int status; + + /** Specifies if a return of an acknowledgment is requested for send SMS */ + private static final int RETURN_NO_ACK = 0; + private static final int RETURN_ACK = 1; + + private SmsEnvelope mEnvelope; + private BearerData mBearerData; + + public static class SubmitPdu extends SubmitPduBase { + } + + /** + * Create an SmsMessage from a raw PDU. + * Note: In CDMA the PDU is just a byte representation of the received Sms. + */ + public static SmsMessage createFromPdu(byte[] pdu) { + SmsMessage msg = new SmsMessage(); + + try { + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * Create a "raw" CDMA SmsMessage from a Parcel that was forged in ril.cpp. + * Note: Only primitive fields are set. + */ + public static SmsMessage newFromParcel(Parcel p) { + // Note: Parcel.readByte actually reads one Int and masks to byte + SmsMessage msg = new SmsMessage(); + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + CdmaSmsSubaddress subaddr = new CdmaSmsSubaddress(); + byte[] data; + byte count; + int countInt; + int addressDigitMode; + + //currently not supported by the modem-lib: env.mMessageType + env.teleService = p.readInt(); //p_cur->uTeleserviceID + + if (0 != p.readByte()) { //p_cur->bIsServicePresent + env.messageType = SmsEnvelope.MESSAGE_TYPE_BROADCAST; + } + else { + if (SmsEnvelope.TELESERVICE_NOT_SET == env.teleService) { + // assume type ACK + env.messageType = SmsEnvelope.MESSAGE_TYPE_ACKNOWLEDGE; + } else { + env.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT; + } + } + env.serviceCategory = p.readInt(); //p_cur->uServicecategory + + // address + addressDigitMode = p.readInt(); + addr.digitMode = (byte) (0xFF & addressDigitMode); //p_cur->sAddress.digit_mode + addr.numberMode = (byte) (0xFF & p.readInt()); //p_cur->sAddress.number_mode + addr.ton = p.readInt(); //p_cur->sAddress.number_type + addr.numberPlan = (byte) (0xFF & p.readInt()); //p_cur->sAddress.number_plan + count = p.readByte(); //p_cur->sAddress.number_of_digits + addr.numberOfDigits = count; + data = new byte[count]; + //p_cur->sAddress.digits[digitCount] + for (int index=0; index < count; index++) { + data[index] = p.readByte(); + + // convert the value if it is 4-bit DTMF to 8 bit + if (addressDigitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) { + data[index] = msg.convertDtmfToAscii(data[index]); + } + } + + addr.origBytes = data; + + subaddr.type = p.readInt(); // p_cur->sSubAddress.subaddressType + subaddr.odd = p.readByte(); // p_cur->sSubAddress.odd + count = p.readByte(); // p_cur->sSubAddress.number_of_digits + + if (count < 0) { + count = 0; + } + + // p_cur->sSubAddress.digits[digitCount] : + + data = new byte[count]; + + for (int index = 0; index < count; ++index) { + data[index] = p.readByte(); + } + + subaddr.origBytes = data; + + /* currently not supported by the modem-lib: + env.bearerReply + env.replySeqNo + env.errorClass + env.causeCode + */ + + // bearer data + countInt = p.readInt(); //p_cur->uBearerDataLen + if (countInt < 0) { + countInt = 0; + } + + data = new byte[countInt]; + for (int index=0; index < countInt; index++) { + data[index] = p.readByte(); + } + // BD gets further decoded when accessed in SMSDispatcher + env.bearerData = data; + + // link the the filled objects to the SMS + env.origAddress = addr; + env.origSubaddress = subaddr; + msg.originatingAddress = addr; + msg.mEnvelope = env; + + // create byte stream representation for transportation through the layers. + msg.createPdu(); + + return msg; + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by RuimSmsInterfaceManager.getAllMessagesFromIcc + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + try { + SmsMessage msg = new SmsMessage(); + + msg.indexOnIcc = index; + + // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT, + // or STORED_UNSENT + // See 3GPP2 C.S0023 3.4.27 + if ((data[0] & 1) == 0) { + Log.w(LOG_TAG, "SMS parsing failed: Trying to parse a free record"); + return null; + } else { + msg.statusOnIcc = data[0] & 0x07; + } + + // Second byte is the MSG_LEN, length of the message + // See 3GPP2 C.S0023 3.4.27 + int size = data[1]; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[size]; + System.arraycopy(data, 2, pdu, 0, size); + // the message has to be parsed before it can be displayed + // see gsm.SmsMessage + msg.parsePduFromEfRecord(pdu); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public static int getTPLayerLengthForPDU(String pdu) { + Log.w(LOG_TAG, "getTPLayerLengthForPDU: is not supported in CDMA mode."); + return 0; + } + + /** + * TODO(cleanup): why do getSubmitPdu methods take an scAddr input + * and do nothing with it? GSM allows us to specify a SC (eg, + * when responding to an SMS that explicitly requests the response + * is sent to a specific SC), or pass null to use the default + * value. Is there no similar notion in CDMA? Or do we just not + * have it hooked up? + */ + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddr Service Centre address. Null means use default. + * @param destAddr Address of the recipient. + * @param message String representation of the message payload. + * @param statusReportRequested Indicates whether a report is requested for this message. + * @param smsHeader Array containing the data for the User Data Header, preceded + * by the Element Identifiers. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, String message, + boolean statusReportRequested, SmsHeader smsHeader) { + + /** + * TODO(cleanup): Do we really want silent failure like this? + * Would it not be much more reasonable to make sure we don't + * call this function if we really want nothing done? + */ + if (message == null || destAddr == null) { + return null; + } + + UserData uData = new UserData(); + uData.payloadStr = message; + uData.userDataHeader = smsHeader; + return privateGetSubmitPdu(destAddr, statusReportRequested, uData); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address and port. + * + * @param scAddr Service Centre address. null == use default + * @param destAddr the address of the destination for the message + * @param destPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddr, String destAddr, int destPort, + byte[] data, boolean statusReportRequested) { + + /** + * TODO(cleanup): this is not a general-purpose SMS creation + * method, but rather something specialized to messages + * containing OCTET encoded (meaning non-human-readable) user + * data. The name should reflect that, and not just overload. + */ + + SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs(); + portAddrs.destPort = destPort; + portAddrs.origPort = 0; + portAddrs.areEightBits = false; + + SmsHeader smsHeader = new SmsHeader(); + smsHeader.portAddrs = portAddrs; + + UserData uData = new UserData(); + uData.userDataHeader = smsHeader; + uData.msgEncoding = UserData.ENCODING_OCTET; + uData.msgEncodingSet = true; + uData.payload = data; + + return privateGetSubmitPdu(destAddr, statusReportRequested, uData); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port + * + * @param destAddr the address of the destination for the message + * @param userData the data for the message + * @param statusReportRequested Indicates whether a report is requested for this message. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String destAddr, UserData userData, + boolean statusReportRequested) { + return privateGetSubmitPdu(destAddr, statusReportRequested, userData); + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public int getProtocolIdentifier() { + Log.w(LOG_TAG, "getProtocolIdentifier: is not supported in CDMA mode."); + // (3GPP TS 23.040): "no interworking, but SME to SME protocol": + return 0; + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public boolean isReplace() { + Log.w(LOG_TAG, "isReplace: is not supported in CDMA mode."); + return false; + } + + /** + * {@inheritDoc} + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public boolean isCphsMwiMessage() { + Log.w(LOG_TAG, "isCphsMwiMessage: is not supported in CDMA mode."); + return false; + } + + /** + * {@inheritDoc} + */ + public boolean isMWIClearMessage() { + return ((mBearerData != null) && (mBearerData.numberOfMessages == 0)); + } + + /** + * {@inheritDoc} + */ + public boolean isMWISetMessage() { + return ((mBearerData != null) && (mBearerData.numberOfMessages > 0)); + } + + /** + * {@inheritDoc} + */ + public boolean isMwiDontStore() { + return ((mBearerData != null) && + (mBearerData.numberOfMessages > 0) && + (mBearerData.userData == null)); + } + + /** + * Returns the status for a previously submitted message. + * For not interfering with status codes from GSM, this status code is + * shifted to the bits 31-16. + */ + public int getStatus() { + return (status << 16); + } + + /** Return true iff the bearer data message type is DELIVERY_ACK. */ + public boolean isStatusReportMessage() { + return (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK); + } + + /** + * Note: This function is a GSM specific functionality which is not supported in CDMA mode. + */ + public boolean isReplyPathPresent() { + Log.w(LOG_TAG, "isReplyPathPresent: is not supported in CDMA mode."); + return false; + } + + /** + * Calculate the number of septets needed to encode the message. + * + * @param messageBody the message to encode + * @param use7bitOnly ignore (but still count) illegal characters if true + * @return TextEncodingDetails + */ + public static TextEncodingDetails calculateLength(CharSequence messageBody, + boolean use7bitOnly) { + return BearerData.calcTextEncodingDetails(messageBody, use7bitOnly); + } + + /** + * Returns the teleservice type of the message. + * @return the teleservice: + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_NOT_SET}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WMT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WEMT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_VMN}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#TELESERVICE_WAP} + */ + /* package */ int getTeleService() { + return mEnvelope.teleService; + } + + /** + * Returns the message type of the message. + * @return the message type: + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_POINT_TO_POINT}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_BROADCAST}, + * {@link com.android.internal.telephony.cdma.sms.SmsEnvelope#MESSAGE_TYPE_ACKNOWLEDGE}, + */ + /* package */ int getMessageType() { + return mEnvelope.messageType; + } + + /** + * Decodes pdu to an empty SMS object. + * In the CDMA case the pdu is just an internal byte stream representation + * of the SMS Java-object. + * @see #createPdu() + */ + private void parsePdu(byte[] pdu) { + ByteArrayInputStream bais = new ByteArrayInputStream(pdu); + DataInputStream dis = new DataInputStream(bais); + byte length; + int bearerDataLength; + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + + try { + env.messageType = dis.readInt(); + env.teleService = dis.readInt(); + env.serviceCategory = dis.readInt(); + + addr.digitMode = dis.readByte(); + addr.numberMode = dis.readByte(); + addr.ton = dis.readByte(); + addr.numberPlan = dis.readByte(); + + length = dis.readByte(); + addr.numberOfDigits = length; + addr.origBytes = new byte[length]; + dis.read(addr.origBytes, 0, length); // digits + + env.bearerReply = dis.readInt(); + // CauseCode values: + env.replySeqNo = dis.readByte(); + env.errorClass = dis.readByte(); + env.causeCode = dis.readByte(); + + //encoded BearerData: + bearerDataLength = dis.readInt(); + env.bearerData = new byte[bearerDataLength]; + dis.read(env.bearerData, 0, bearerDataLength); + dis.close(); + } catch (Exception ex) { + Log.e(LOG_TAG, "createFromPdu: conversion from byte array to object failed: " + ex); + } + + // link the filled objects to this SMS + originatingAddress = addr; + env.origAddress = addr; + mEnvelope = env; + mPdu = pdu; + + parseSms(); + } + + /** + * Decodes 3GPP2 sms stored in CSIM/RUIM cards As per 3GPP2 C.S0015-0 + */ + private void parsePduFromEfRecord(byte[] pdu) { + ByteArrayInputStream bais = new ByteArrayInputStream(pdu); + DataInputStream dis = new DataInputStream(bais); + SmsEnvelope env = new SmsEnvelope(); + CdmaSmsAddress addr = new CdmaSmsAddress(); + CdmaSmsSubaddress subAddr = new CdmaSmsSubaddress(); + + try { + env.messageType = dis.readByte(); + + while (dis.available() > 0) { + int parameterId = dis.readByte(); + int parameterLen = dis.readByte(); + byte[] parameterData = new byte[parameterLen]; + + switch (parameterId) { + case TELESERVICE_IDENTIFIER: + /* + * 16 bit parameter that identifies which upper layer + * service access point is sending or should receive + * this message + */ + env.teleService = dis.readUnsignedShort(); + Log.i(LOG_TAG, "teleservice = " + env.teleService); + break; + case SERVICE_CATEGORY: + /* + * 16 bit parameter that identifies type of service as + * in 3GPP2 C.S0015-0 Table 3.4.3.2-1 + */ + env.serviceCategory = dis.readUnsignedShort(); + break; + case ORIGINATING_ADDRESS: + case DESTINATION_ADDRESS: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream addrBis = new BitwiseInputStream(parameterData); + addr.digitMode = addrBis.read(1); + addr.numberMode = addrBis.read(1); + int numberType = 0; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + numberType = addrBis.read(3); + addr.ton = numberType; + + if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK) + addr.numberPlan = addrBis.read(4); + } + + addr.numberOfDigits = addrBis.read(8); + + byte[] data = new byte[addr.numberOfDigits]; + byte b = 0x00; + + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF) { + /* As per 3GPP2 C.S0005-0 Table 2.7.1.3.2.4-4 */ + for (int index = 0; index < addr.numberOfDigits; index++) { + b = (byte) (0xF & addrBis.read(4)); + // convert the value if it is 4-bit DTMF to 8 + // bit + data[index] = convertDtmfToAscii(b); + } + } else if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK) { + for (int index = 0; index < addr.numberOfDigits; index++) { + b = (byte) (0xFF & addrBis.read(8)); + data[index] = b; + } + + } else if (addr.numberMode == CdmaSmsAddress.NUMBER_MODE_DATA_NETWORK) { + if (numberType == 2) + Log.e(LOG_TAG, "TODO: Originating Addr is email id"); + else + Log.e(LOG_TAG, + "TODO: Originating Addr is data network address"); + } else { + Log.e(LOG_TAG, "Originating Addr is of incorrect type"); + } + } else { + Log.e(LOG_TAG, "Incorrect Digit mode"); + } + addr.origBytes = data; + Log.i(LOG_TAG, "Originating Addr=" + addr.toString()); + break; + case ORIGINATING_SUB_ADDRESS: + case DESTINATION_SUB_ADDRESS: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream subAddrBis = new BitwiseInputStream(parameterData); + subAddr.type = subAddrBis.read(3); + subAddr.odd = subAddrBis.readByteArray(1)[0]; + int subAddrLen = subAddrBis.read(8); + byte[] subdata = new byte[subAddrLen]; + for (int index = 0; index < subAddrLen; index++) { + b = (byte) (0xFF & subAddrBis.read(4)); + // convert the value if it is 4-bit DTMF to 8 bit + subdata[index] = convertDtmfToAscii(b); + } + subAddr.origBytes = subdata; + break; + case BEARER_REPLY_OPTION: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream replyOptBis = new BitwiseInputStream(parameterData); + env.bearerReply = replyOptBis.read(6); + break; + case CAUSE_CODES: + dis.read(parameterData, 0, parameterLen); + BitwiseInputStream ccBis = new BitwiseInputStream(parameterData); + env.replySeqNo = ccBis.readByteArray(6)[0]; + env.errorClass = ccBis.readByteArray(2)[0]; + if (env.errorClass != 0x00) + env.causeCode = ccBis.readByteArray(8)[0]; + break; + case BEARER_DATA: + dis.read(parameterData, 0, parameterLen); + env.bearerData = parameterData; + break; + default: + throw new Exception("unsupported parameterId (" + parameterId + ")"); + } + } + bais.close(); + dis.close(); + } catch (Exception ex) { + Log.e(LOG_TAG, "parsePduFromEfRecord: conversion from pdu to SmsMessage failed" + ex); + } + + // link the filled objects to this SMS + originatingAddress = addr; + env.origAddress = addr; + env.origSubaddress = subAddr; + mEnvelope = env; + mPdu = pdu; + + parseSms(); + } + + /** + * Parses a SMS message from its BearerData stream. (mobile-terminated only) + */ + protected void parseSms() { + // Message Waiting Info Record defined in 3GPP2 C.S-0005, 3.7.5.6 + // It contains only an 8-bit number with the number of messages waiting + if (mEnvelope.teleService == SmsEnvelope.TELESERVICE_MWI) { + mBearerData = new BearerData(); + if (mEnvelope.bearerData != null) { + mBearerData.numberOfMessages = 0x000000FF & mEnvelope.bearerData[0]; + } + if (false) { + Log.d(LOG_TAG, "parseSms: get MWI " + + Integer.toString(mBearerData.numberOfMessages)); + } + return; + } + mBearerData = BearerData.decode(mEnvelope.bearerData); + if (Log.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Log.d(LOG_TAG, "MT raw BearerData = '" + + HexDump.toHexString(mEnvelope.bearerData) + "'"); + Log.d(LOG_TAG, "MT (decoded) BearerData = " + mBearerData); + } + messageRef = mBearerData.messageId; + if (mBearerData.userData != null) { + userData = mBearerData.userData.payload; + userDataHeader = mBearerData.userData.userDataHeader; + messageBody = mBearerData.userData.payloadStr; + } + + if (originatingAddress != null) { + originatingAddress.address = new String(originatingAddress.origBytes); + if (false) Log.v(LOG_TAG, "SMS originating address: " + + originatingAddress.address); + } + + if (mBearerData.msgCenterTimeStamp != null) { + scTimeMillis = mBearerData.msgCenterTimeStamp.toMillis(true); + } + + if (false) Log.d(LOG_TAG, "SMS SC timestamp: " + scTimeMillis); + + // Message Type (See 3GPP2 C.S0015-B, v2, 4.5.1) + if (mBearerData.messageType == BearerData.MESSAGE_TYPE_DELIVERY_ACK) { + // The BearerData MsgStatus subparameter should only be + // included for DELIVERY_ACK messages. If it occurred for + // other messages, it would be unclear what the status + // being reported refers to. The MsgStatus subparameter + // is primarily useful to indicate error conditions -- a + // message without this subparameter is assumed to + // indicate successful delivery (status == 0). + if (! mBearerData.messageStatusSet) { + Log.d(LOG_TAG, "DELIVERY_ACK message without msgStatus (" + + (userData == null ? "also missing" : "does have") + + " userData)."); + status = 0; + } else { + status = mBearerData.errorClass << 8; + status |= mBearerData.messageStatus; + } + } else if (mBearerData.messageType != BearerData.MESSAGE_TYPE_DELIVER) { + throw new RuntimeException("Unsupported message type: " + mBearerData.messageType); + } + + if (messageBody != null) { + if (false) Log.v(LOG_TAG, "SMS message body: '" + messageBody + "'"); + parseMessageBody(); + } else if ((userData != null) && (false)) { + Log.v(LOG_TAG, "SMS payload: '" + IccUtils.bytesToHexString(userData) + "'"); + } + } + + /** + * Parses a broadcast SMS, possibly containing a CMAS alert. + */ + SmsCbMessage parseBroadcastSms() { + BearerData bData = BearerData.decode(mEnvelope.bearerData, mEnvelope.serviceCategory); + if (bData == null) { + Log.w(LOG_TAG, "BearerData.decode() returned null"); + return null; + } + + if (Log.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Log.d(LOG_TAG, "MT raw BearerData = " + HexDump.toHexString(mEnvelope.bearerData)); + } + + String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC); + SmsCbLocation location = new SmsCbLocation(plmn); + + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2, + SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, bData.messageId, location, + mEnvelope.serviceCategory, bData.getLanguage(), bData.userData.payloadStr, + bData.priority, null, bData.cmasWarningInfo); + } + + /** + * {@inheritDoc} + */ + @Override + public SmsConstants.MessageClass getMessageClass() { + if (BearerData.DISPLAY_MODE_IMMEDIATE == mBearerData.displayMode ) { + return SmsConstants.MessageClass.CLASS_0; + } else { + return SmsConstants.MessageClass.UNKNOWN; + } + } + + /** + * Calculate the next message id, starting at 1 and iteratively + * incrementing within the range 1..65535 remembering the state + * via a persistent system property. (See C.S0015-B, v2.0, + * 4.3.1.5) Since this routine is expected to be accessed via via + * binder-call, and hence should be thread-safe, it has been + * synchronized. + */ + private synchronized static int getNextMessageId() { + // Testing and dialog with partners has indicated that + // msgId==0 is (sometimes?) treated specially by lower levels. + // Specifically, the ID is not preserved for delivery ACKs. + // Hence, avoid 0 -- constraining the range to 1..65535. + int msgId = SystemProperties.getInt(TelephonyProperties.PROPERTY_CDMA_MSG_ID, 1); + String nextMsgId = Integer.toString((msgId % 0xFFFF) + 1); + SystemProperties.set(TelephonyProperties.PROPERTY_CDMA_MSG_ID, nextMsgId); + if (Log.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Log.d(LOG_TAG, "next " + TelephonyProperties.PROPERTY_CDMA_MSG_ID + " = " + nextMsgId); + Log.d(LOG_TAG, "readback gets " + + SystemProperties.get(TelephonyProperties.PROPERTY_CDMA_MSG_ID)); + } + return msgId; + } + + /** + * Creates BearerData and Envelope from parameters for a Submit SMS. + * @return byte stream for SubmitPdu. + */ + private static SubmitPdu privateGetSubmitPdu(String destAddrStr, boolean statusReportRequested, + UserData userData) { + + /** + * TODO(cleanup): give this function a more meaningful name. + */ + + /** + * TODO(cleanup): Make returning null from the getSubmitPdu + * variations meaningful -- clean up the error feedback + * mechanism, and avoid null pointer exceptions. + */ + + /** + * North America Plus Code : + * Convert + code to 011 and dial out for international SMS + */ + CdmaSmsAddress destAddr = CdmaSmsAddress.parse( + PhoneNumberUtils.cdmaCheckAndProcessPlusCode(destAddrStr)); + if (destAddr == null) return null; + + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_SUBMIT; + + bearerData.messageId = getNextMessageId(); + + bearerData.deliveryAckReq = statusReportRequested; + bearerData.userAckReq = false; + bearerData.readAckReq = false; + bearerData.reportReq = false; + + bearerData.userData = userData; + + byte[] encodedBearerData = BearerData.encode(bearerData); + if (Log.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) { + Log.d(LOG_TAG, "MO (encoded) BearerData = " + bearerData); + Log.d(LOG_TAG, "MO raw BearerData = '" + HexDump.toHexString(encodedBearerData) + "'"); + } + if (encodedBearerData == null) return null; + + int teleservice = bearerData.hasUserDataHeader ? + SmsEnvelope.TELESERVICE_WEMT : SmsEnvelope.TELESERVICE_WMT; + + SmsEnvelope envelope = new SmsEnvelope(); + envelope.messageType = SmsEnvelope.MESSAGE_TYPE_POINT_TO_POINT; + envelope.teleService = teleservice; + envelope.destAddress = destAddr; + envelope.bearerReply = RETURN_ACK; + envelope.bearerData = encodedBearerData; + + /** + * TODO(cleanup): envelope looks to be a pointless class, get + * rid of it. Also -- most of the envelope fields set here + * are ignored, why? + */ + + try { + /** + * TODO(cleanup): reference a spec and get rid of the ugly comments + */ + ByteArrayOutputStream baos = new ByteArrayOutputStream(100); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(envelope.teleService); + dos.writeInt(0); //servicePresent + dos.writeInt(0); //serviceCategory + dos.write(destAddr.digitMode); + dos.write(destAddr.numberMode); + dos.write(destAddr.ton); // number_type + dos.write(destAddr.numberPlan); + dos.write(destAddr.numberOfDigits); + dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits + // Subaddress is not supported. + dos.write(0); //subaddressType + dos.write(0); //subaddr_odd + dos.write(0); //subaddr_nbr_of_digits + dos.write(encodedBearerData.length); + dos.write(encodedBearerData, 0, encodedBearerData.length); + dos.close(); + + SubmitPdu pdu = new SubmitPdu(); + pdu.encodedMessage = baos.toByteArray(); + pdu.encodedScAddress = null; + return pdu; + } catch(IOException ex) { + Log.e(LOG_TAG, "creating SubmitPdu failed: " + ex); + } + return null; + } + + /** + * Creates byte array (pseudo pdu) from SMS object. + * Note: Do not call this method more than once per object! + */ + private void createPdu() { + SmsEnvelope env = mEnvelope; + CdmaSmsAddress addr = env.origAddress; + ByteArrayOutputStream baos = new ByteArrayOutputStream(100); + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(baos)); + + try { + dos.writeInt(env.messageType); + dos.writeInt(env.teleService); + dos.writeInt(env.serviceCategory); + + dos.writeByte(addr.digitMode); + dos.writeByte(addr.numberMode); + dos.writeByte(addr.ton); + dos.writeByte(addr.numberPlan); + dos.writeByte(addr.numberOfDigits); + dos.write(addr.origBytes, 0, addr.origBytes.length); // digits + + dos.writeInt(env.bearerReply); + // CauseCode values: + dos.writeByte(env.replySeqNo); + dos.writeByte(env.errorClass); + dos.writeByte(env.causeCode); + //encoded BearerData: + dos.writeInt(env.bearerData.length); + dos.write(env.bearerData, 0, env.bearerData.length); + dos.close(); + + /** + * TODO(cleanup) -- The mPdu field is managed in + * a fragile manner, and it would be much nicer if + * accessing the serialized representation used a less + * fragile mechanism. Maybe the getPdu method could + * generate a representation if there was not yet one? + */ + + mPdu = baos.toByteArray(); + } catch (IOException ex) { + Log.e(LOG_TAG, "createPdu: conversion from object to byte array failed: " + ex); + } + } + + /** + * Converts a 4-Bit DTMF encoded symbol from the calling address number to ASCII character + */ + private byte convertDtmfToAscii(byte dtmfDigit) { + byte asciiDigit; + + switch (dtmfDigit) { + case 0: asciiDigit = 68; break; // 'D' + case 1: asciiDigit = 49; break; // '1' + case 2: asciiDigit = 50; break; // '2' + case 3: asciiDigit = 51; break; // '3' + case 4: asciiDigit = 52; break; // '4' + case 5: asciiDigit = 53; break; // '5' + case 6: asciiDigit = 54; break; // '6' + case 7: asciiDigit = 55; break; // '7' + case 8: asciiDigit = 56; break; // '8' + case 9: asciiDigit = 57; break; // '9' + case 10: asciiDigit = 48; break; // '0' + case 11: asciiDigit = 42; break; // '*' + case 12: asciiDigit = 35; break; // '#' + case 13: asciiDigit = 65; break; // 'A' + case 14: asciiDigit = 66; break; // 'B' + case 15: asciiDigit = 67; break; // 'C' + default: + asciiDigit = 32; // Invalid DTMF code + break; + } + + return asciiDigit; + } + + /** This function shall be called to get the number of voicemails. + * @hide + */ + /*package*/ int getNumOfVoicemails() { + return mBearerData.numberOfMessages; + } + + /** + * Returns a byte array that can be use to uniquely identify a received SMS message. + * C.S0015-B 4.3.1.6 Unique Message Identification. + * + * @return byte array uniquely identifying the message. + * @hide + */ + /* package */ byte[] getIncomingSmsFingerprint() { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + + output.write(mEnvelope.teleService); + output.write(mEnvelope.origAddress.origBytes, 0, mEnvelope.origAddress.origBytes.length); + output.write(mEnvelope.bearerData, 0, mEnvelope.bearerData.length); + output.write(mEnvelope.origSubaddress.origBytes, 0, + mEnvelope.origSubaddress.origBytes.length); + + return output.toByteArray(); + } + + /** + * Returns the list of service category program data, if present. + * @return a list of CdmaSmsCbProgramData objects, or null if not present + */ + List<CdmaSmsCbProgramData> getSmsCbProgramData() { + return mBearerData.serviceCategoryProgramData; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/TtyIntent.java b/src/java/com/android/internal/telephony/cdma/TtyIntent.java new file mode 100644 index 0000000..4907aa9 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/TtyIntent.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma; + +public class TtyIntent { + + private static final String TAG = "TtyIntent"; + + + /** Event for TTY mode change */ + + /** + * Broadcast intent action indicating that the TTY has either been + * enabled or disabled. An intent extra provides this state as a boolean, + * where {@code true} means enabled. + * @see #TTY_ENABLED + * + * {@hide} + */ + public static final String TTY_ENABLED_CHANGE_ACTION = + "com.android.internal.telephony.cdma.intent.action.TTY_ENABLED_CHANGE"; + + /** + * The lookup key for a boolean that indicates whether TTY mode is enabled or + * disabled. {@code true} means TTY mode is enabled. Retrieve it with + * {@link android.content.Intent#getBooleanExtra(String,boolean)}. + * + * {@hide} + */ + public static final String TTY_ENABLED = "ttyEnabled"; + + /** + * Broadcast intent action indicating that the TTY preferred operating mode + * has changed. An intent extra provides the new mode as an int. + * @see #TTY_PREFFERED_MODE + * + * {@hide} + */ + public static final String TTY_PREFERRED_MODE_CHANGE_ACTION = + "com.android.internal.telephony.cdma.intent.action.TTY_PREFERRED_MODE_CHANGE"; + + /** + * The lookup key for an int that indicates preferred TTY mode. + * Valid modes are: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * + * {@hide} + */ + public static final String TTY_PREFFERED_MODE = "ttyPreferredMode"; +} diff --git a/src/java/com/android/internal/telephony/cdma/package.html b/src/java/com/android/internal/telephony/cdma/package.html new file mode 100644 index 0000000..4eb1f9c --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/package.html @@ -0,0 +1,6 @@ +<HTML> +<BODY> +Provides classes to control or read data from CDMA phones. +@hide +</BODY> +</HTML> diff --git a/src/java/com/android/internal/telephony/cdma/sms/BearerData.java b/src/java/com/android/internal/telephony/cdma/sms/BearerData.java new file mode 100755 index 0000000..7e7347f --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/BearerData.java @@ -0,0 +1,1938 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.content.res.Resources; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbMessage; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.text.format.Time; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.util.BitwiseInputStream; +import com.android.internal.util.BitwiseOutputStream; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; + +/** + * An object to encode and decode CDMA SMS bearer data. + */ +public final class BearerData { + private final static String LOG_TAG = "SMS"; + + /** + * Bearer Data Subparameter Identifiers + * (See 3GPP2 C.S0015-B, v2.0, table 4.5-1) + * NOTE: Commented subparameter types are not implemented. + */ + private final static byte SUBPARAM_MESSAGE_IDENTIFIER = 0x00; + private final static byte SUBPARAM_USER_DATA = 0x01; + private final static byte SUBPARAM_USER_RESPONSE_CODE = 0x02; + private final static byte SUBPARAM_MESSAGE_CENTER_TIME_STAMP = 0x03; + private final static byte SUBPARAM_VALIDITY_PERIOD_ABSOLUTE = 0x04; + private final static byte SUBPARAM_VALIDITY_PERIOD_RELATIVE = 0x05; + private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE = 0x06; + private final static byte SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE = 0x07; + private final static byte SUBPARAM_PRIORITY_INDICATOR = 0x08; + private final static byte SUBPARAM_PRIVACY_INDICATOR = 0x09; + private final static byte SUBPARAM_REPLY_OPTION = 0x0A; + private final static byte SUBPARAM_NUMBER_OF_MESSAGES = 0x0B; + private final static byte SUBPARAM_ALERT_ON_MESSAGE_DELIVERY = 0x0C; + private final static byte SUBPARAM_LANGUAGE_INDICATOR = 0x0D; + private final static byte SUBPARAM_CALLBACK_NUMBER = 0x0E; + private final static byte SUBPARAM_MESSAGE_DISPLAY_MODE = 0x0F; + //private final static byte SUBPARAM_MULTIPLE_ENCODING_USER_DATA = 0x10; + private final static byte SUBPARAM_MESSAGE_DEPOSIT_INDEX = 0x11; + private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA = 0x12; + private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS = 0x13; + private final static byte SUBPARAM_MESSAGE_STATUS = 0x14; + //private final static byte SUBPARAM_TP_FAILURE_CAUSE = 0x15; + //private final static byte SUBPARAM_ENHANCED_VMN = 0x16; + //private final static byte SUBPARAM_ENHANCED_VMN_ACK = 0x17; + + /** + * Supported message types for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.1-1) + */ + public static final int MESSAGE_TYPE_DELIVER = 0x01; + public static final int MESSAGE_TYPE_SUBMIT = 0x02; + public static final int MESSAGE_TYPE_CANCELLATION = 0x03; + public static final int MESSAGE_TYPE_DELIVERY_ACK = 0x04; + public static final int MESSAGE_TYPE_USER_ACK = 0x05; + public static final int MESSAGE_TYPE_READ_ACK = 0x06; + public static final int MESSAGE_TYPE_DELIVER_REPORT = 0x07; + public static final int MESSAGE_TYPE_SUBMIT_REPORT = 0x08; + + public int messageType; + + /** + * 16-bit value indicating the message ID, which increments modulo 65536. + * (Special rules apply for WAP-messages.) + * (See 3GPP2 C.S0015-B, v2, 4.5.1) + */ + public int messageId; + + /** + * Supported priority modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.9-1) + */ + public static final int PRIORITY_NORMAL = 0x0; + public static final int PRIORITY_INTERACTIVE = 0x1; + public static final int PRIORITY_URGENT = 0x2; + public static final int PRIORITY_EMERGENCY = 0x3; + + public boolean priorityIndicatorSet = false; + public int priority = PRIORITY_NORMAL; + + /** + * Supported privacy modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.10-1) + */ + public static final int PRIVACY_NOT_RESTRICTED = 0x0; + public static final int PRIVACY_RESTRICTED = 0x1; + public static final int PRIVACY_CONFIDENTIAL = 0x2; + public static final int PRIVACY_SECRET = 0x3; + + public boolean privacyIndicatorSet = false; + public int privacy = PRIVACY_NOT_RESTRICTED; + + /** + * Supported alert priority modes for CDMA SMS messages + * (See 3GPP2 C.S0015-B, v2.0, table 4.5.13-1) + */ + public static final int ALERT_DEFAULT = 0x0; + public static final int ALERT_LOW_PRIO = 0x1; + public static final int ALERT_MEDIUM_PRIO = 0x2; + public static final int ALERT_HIGH_PRIO = 0x3; + + public boolean alertIndicatorSet = false; + public int alert = ALERT_DEFAULT; + + /** + * Supported display modes for CDMA SMS messages. Display mode is + * a 2-bit value used to indicate to the mobile station when to + * display the received message. (See 3GPP2 C.S0015-B, v2, + * 4.5.16) + */ + public static final int DISPLAY_MODE_IMMEDIATE = 0x0; + public static final int DISPLAY_MODE_DEFAULT = 0x1; + public static final int DISPLAY_MODE_USER = 0x2; + + public boolean displayModeSet = false; + public int displayMode = DISPLAY_MODE_DEFAULT; + + /** + * Language Indicator values. NOTE: the spec (3GPP2 C.S0015-B, + * v2, 4.5.14) is ambiguous as to the meaning of this field, as it + * refers to C.R1001-D but that reference has been crossed out. + * It would seem reasonable to assume the values from C.R1001-F + * (table 9.2-1) are to be used instead. + */ + public static final int LANGUAGE_UNKNOWN = 0x00; + public static final int LANGUAGE_ENGLISH = 0x01; + public static final int LANGUAGE_FRENCH = 0x02; + public static final int LANGUAGE_SPANISH = 0x03; + public static final int LANGUAGE_JAPANESE = 0x04; + public static final int LANGUAGE_KOREAN = 0x05; + public static final int LANGUAGE_CHINESE = 0x06; + public static final int LANGUAGE_HEBREW = 0x07; + + public boolean languageIndicatorSet = false; + public int language = LANGUAGE_UNKNOWN; + + /** + * SMS Message Status Codes. The first component of the Message + * status indicates if an error has occurred and whether the error + * is considered permanent or temporary. The second component of + * the Message status indicates the cause of the error (if any). + * (See 3GPP2 C.S0015-B, v2.0, 4.5.21) + */ + /* no-error codes */ + public static final int ERROR_NONE = 0x00; + public static final int STATUS_ACCEPTED = 0x00; + public static final int STATUS_DEPOSITED_TO_INTERNET = 0x01; + public static final int STATUS_DELIVERED = 0x02; + public static final int STATUS_CANCELLED = 0x03; + /* temporary-error and permanent-error codes */ + public static final int ERROR_TEMPORARY = 0x02; + public static final int STATUS_NETWORK_CONGESTION = 0x04; + public static final int STATUS_NETWORK_ERROR = 0x05; + public static final int STATUS_UNKNOWN_ERROR = 0x1F; + /* permanent-error codes */ + public static final int ERROR_PERMANENT = 0x03; + public static final int STATUS_CANCEL_FAILED = 0x06; + public static final int STATUS_BLOCKED_DESTINATION = 0x07; + public static final int STATUS_TEXT_TOO_LONG = 0x08; + public static final int STATUS_DUPLICATE_MESSAGE = 0x09; + public static final int STATUS_INVALID_DESTINATION = 0x0A; + public static final int STATUS_MESSAGE_EXPIRED = 0x0D; + /* undefined-status codes */ + public static final int ERROR_UNDEFINED = 0xFF; + public static final int STATUS_UNDEFINED = 0xFF; + + public boolean messageStatusSet = false; + public int errorClass = ERROR_UNDEFINED; + public int messageStatus = STATUS_UNDEFINED; + + /** + * 1-bit value that indicates whether a User Data Header (UDH) is present. + * (See 3GPP2 C.S0015-B, v2, 4.5.1) + * + * NOTE: during encoding, this value will be set based on the + * presence of a UDH in the structured data, any existing setting + * will be overwritten. + */ + public boolean hasUserDataHeader; + + /** + * provides the information for the user data + * (e.g. padding bits, user data, user data header, etc) + * (See 3GPP2 C.S.0015-B, v2, 4.5.2) + */ + public UserData userData; + + /** + * The User Response Code subparameter is used in the SMS User + * Acknowledgment Message to respond to previously received short + * messages. This message center-specific element carries the + * identifier of a predefined response. (See 3GPP2 C.S.0015-B, v2, + * 4.5.3) + */ + public boolean userResponseCodeSet = false; + public int userResponseCode; + + /** + * 6-byte-field, see 3GPP2 C.S0015-B, v2, 4.5.4 + */ + public static class TimeStamp extends Time { + + public TimeStamp() { + super(TimeZone.getDefault().getID()); // 3GPP2 timestamps use the local timezone + } + + public static TimeStamp fromByteArray(byte[] data) { + TimeStamp ts = new TimeStamp(); + // C.S0015-B v2.0, 4.5.4: range is 1996-2095 + int year = IccUtils.cdmaBcdByteToInt(data[0]); + if (year > 99 || year < 0) return null; + ts.year = year >= 96 ? year + 1900 : year + 2000; + int month = IccUtils.cdmaBcdByteToInt(data[1]); + if (month < 1 || month > 12) return null; + ts.month = month - 1; + int day = IccUtils.cdmaBcdByteToInt(data[2]); + if (day < 1 || day > 31) return null; + ts.monthDay = day; + int hour = IccUtils.cdmaBcdByteToInt(data[3]); + if (hour < 0 || hour > 23) return null; + ts.hour = hour; + int minute = IccUtils.cdmaBcdByteToInt(data[4]); + if (minute < 0 || minute > 59) return null; + ts.minute = minute; + int second = IccUtils.cdmaBcdByteToInt(data[5]); + if (second < 0 || second > 59) return null; + ts.second = second; + return ts; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TimeStamp "); + builder.append("{ year=" + year); + builder.append(", month=" + month); + builder.append(", day=" + monthDay); + builder.append(", hour=" + hour); + builder.append(", minute=" + minute); + builder.append(", second=" + second); + builder.append(" }"); + return builder.toString(); + } + } + + public TimeStamp msgCenterTimeStamp; + public TimeStamp validityPeriodAbsolute; + public TimeStamp deferredDeliveryTimeAbsolute; + + /** + * Relative time is specified as one byte, the value of which + * falls into a series of ranges, as specified below. The idea is + * that shorter time intervals allow greater precision -- the + * value means minutes from zero until the MINS_LIMIT (inclusive), + * upon which it means hours until the HOURS_LIMIT, and so + * forth. (See 3GPP2 C.S0015-B, v2, 4.5.6-1) + */ + public static final int RELATIVE_TIME_MINS_LIMIT = 143; + public static final int RELATIVE_TIME_HOURS_LIMIT = 167; + public static final int RELATIVE_TIME_DAYS_LIMIT = 196; + public static final int RELATIVE_TIME_WEEKS_LIMIT = 244; + public static final int RELATIVE_TIME_INDEFINITE = 245; + public static final int RELATIVE_TIME_NOW = 246; + public static final int RELATIVE_TIME_MOBILE_INACTIVE = 247; + public static final int RELATIVE_TIME_RESERVED = 248; + + public boolean validityPeriodRelativeSet; + public int validityPeriodRelative; + public boolean deferredDeliveryTimeRelativeSet; + public int deferredDeliveryTimeRelative; + + /** + * The Reply Option subparameter contains 1-bit values which + * indicate whether SMS acknowledgment is requested or not. (See + * 3GPP2 C.S0015-B, v2, 4.5.11) + */ + public boolean userAckReq; + public boolean deliveryAckReq; + public boolean readAckReq; + public boolean reportReq; + + /** + * The Number of Messages subparameter (8-bit value) is a decimal + * number in the 0 to 99 range representing the number of messages + * stored at the Voice Mail System. This element is used by the + * Voice Mail Notification service. (See 3GPP2 C.S0015-B, v2, + * 4.5.12) + */ + public int numberOfMessages; + + /** + * The Message Deposit Index subparameter is assigned by the + * message center as a unique index to the contents of the User + * Data subparameter in each message sent to a particular mobile + * station. The mobile station, when replying to a previously + * received short message which included a Message Deposit Index + * subparameter, may include the Message Deposit Index of the + * received message to indicate to the message center that the + * original contents of the message are to be included in the + * reply. (See 3GPP2 C.S0015-B, v2, 4.5.18) + */ + public int depositIndex; + + /** + * 4-bit or 8-bit value that indicates the number to be dialed in reply to a + * received SMS message. + * (See 3GPP2 C.S0015-B, v2, 4.5.15) + */ + public CdmaSmsAddress callbackNumber; + + /** + * CMAS warning notification information. + * @see #decodeCmasUserData(BearerData, int) + */ + public SmsCbCmasInfo cmasWarningInfo; + + /** + * The Service Category Program Data subparameter is used to enable and disable + * SMS broadcast service categories to display. If this subparameter is present, + * this field will contain a list of one or more + * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects containing the + * operation(s) to perform. + */ + public List<CdmaSmsCbProgramData> serviceCategoryProgramData; + + private static class CodingException extends Exception { + public CodingException(String s) { + super(s); + } + } + + /** + * Returns the language indicator as a two-character ISO 639 string. + * @return a two character ISO 639 language code + */ + public String getLanguage() { + return getLanguageCodeForValue(language); + } + + /** + * Converts a CDMA language indicator value to an ISO 639 two character language code. + * @param languageValue the CDMA language value to convert + * @return the two character ISO 639 language code for the specified value, or null if unknown + */ + private static String getLanguageCodeForValue(int languageValue) { + switch (languageValue) { + case LANGUAGE_ENGLISH: + return "en"; + + case LANGUAGE_FRENCH: + return "fr"; + + case LANGUAGE_SPANISH: + return "es"; + + case LANGUAGE_JAPANESE: + return "ja"; + + case LANGUAGE_KOREAN: + return "ko"; + + case LANGUAGE_CHINESE: + return "zh"; + + case LANGUAGE_HEBREW: + return "he"; + + default: + return null; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("BearerData "); + builder.append("{ messageType=" + messageType); + builder.append(", messageId=" + (int)messageId); + builder.append(", priority=" + (priorityIndicatorSet ? priority : "unset")); + builder.append(", privacy=" + (privacyIndicatorSet ? privacy : "unset")); + builder.append(", alert=" + (alertIndicatorSet ? alert : "unset")); + builder.append(", displayMode=" + (displayModeSet ? displayMode : "unset")); + builder.append(", language=" + (languageIndicatorSet ? language : "unset")); + builder.append(", errorClass=" + (messageStatusSet ? errorClass : "unset")); + builder.append(", msgStatus=" + (messageStatusSet ? messageStatus : "unset")); + builder.append(", msgCenterTimeStamp=" + + ((msgCenterTimeStamp != null) ? msgCenterTimeStamp : "unset")); + builder.append(", validityPeriodAbsolute=" + + ((validityPeriodAbsolute != null) ? validityPeriodAbsolute : "unset")); + builder.append(", validityPeriodRelative=" + + ((validityPeriodRelativeSet) ? validityPeriodRelative : "unset")); + builder.append(", deferredDeliveryTimeAbsolute=" + + ((deferredDeliveryTimeAbsolute != null) ? deferredDeliveryTimeAbsolute : "unset")); + builder.append(", deferredDeliveryTimeRelative=" + + ((deferredDeliveryTimeRelativeSet) ? deferredDeliveryTimeRelative : "unset")); + builder.append(", userAckReq=" + userAckReq); + builder.append(", deliveryAckReq=" + deliveryAckReq); + builder.append(", readAckReq=" + readAckReq); + builder.append(", reportReq=" + reportReq); + builder.append(", numberOfMessages=" + numberOfMessages); + builder.append(", callbackNumber=" + callbackNumber); + builder.append(", depositIndex=" + depositIndex); + builder.append(", hasUserDataHeader=" + hasUserDataHeader); + builder.append(", userData=" + userData); + builder.append(" }"); + return builder.toString(); + } + + private static void encodeMessageId(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 3); + outStream.write(4, bData.messageType); + outStream.write(8, bData.messageId >> 8); + outStream.write(8, bData.messageId); + outStream.write(1, bData.hasUserDataHeader ? 1 : 0); + outStream.skip(3); + } + + private static int countAsciiSeptets(CharSequence msg, boolean force) { + int msgLen = msg.length(); + if (force) return msgLen; + for (int i = 0; i < msgLen; i++) { + if (UserData.charToAscii.get(msg.charAt(i), -1) == -1) { + return -1; + } + } + return msgLen; + } + + /** + * Calculate the message text encoding length, fragmentation, and other details. + * + * @param msg message text + * @param force7BitEncoding ignore (but still count) illegal characters if true + * @return septet count, or -1 on failure + */ + public static TextEncodingDetails calcTextEncodingDetails(CharSequence msg, + boolean force7BitEncoding) { + TextEncodingDetails ted; + int septets = countAsciiSeptets(msg, force7BitEncoding); + if (septets != -1 && septets <= SmsConstants.MAX_USER_DATA_SEPTETS) { + ted = new TextEncodingDetails(); + ted.msgCount = 1; + ted.codeUnitCount = septets; + ted.codeUnitsRemaining = SmsConstants.MAX_USER_DATA_SEPTETS - septets; + ted.codeUnitSize = SmsConstants.ENCODING_7BIT; + } else { + ted = com.android.internal.telephony.gsm.SmsMessage.calculateLength( + msg, force7BitEncoding); + if (ted.msgCount == 1 && ted.codeUnitSize == SmsConstants.ENCODING_7BIT) { + // We don't support single-segment EMS, so calculate for 16-bit + // TODO: Consider supporting single-segment EMS + ted.codeUnitCount = msg.length(); + int octets = ted.codeUnitCount * 2; + if (octets > SmsConstants.MAX_USER_DATA_BYTES) { + ted.msgCount = (octets + (SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER - 1)) / + SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + ted.codeUnitsRemaining = ((ted.msgCount * + SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER) - octets) / 2; + } else { + ted.msgCount = 1; + ted.codeUnitsRemaining = (SmsConstants.MAX_USER_DATA_BYTES - octets)/2; + } + ted.codeUnitSize = SmsConstants.ENCODING_16BIT; + } + } + return ted; + } + + private static byte[] encode7bitAscii(String msg, boolean force) + throws CodingException + { + try { + BitwiseOutputStream outStream = new BitwiseOutputStream(msg.length()); + int msgLen = msg.length(); + for (int i = 0; i < msgLen; i++) { + int charCode = UserData.charToAscii.get(msg.charAt(i), -1); + if (charCode == -1) { + if (force) { + outStream.write(7, UserData.UNENCODABLE_7_BIT_CHAR); + } else { + throw new CodingException("cannot ASCII encode (" + msg.charAt(i) + ")"); + } + } else { + outStream.write(7, charCode); + } + } + return outStream.toByteArray(); + } catch (BitwiseOutputStream.AccessException ex) { + throw new CodingException("7bit ASCII encode failed: " + ex); + } + } + + private static byte[] encodeUtf16(String msg) + throws CodingException + { + try { + return msg.getBytes("utf-16be"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("UTF-16 encode failed: " + ex); + } + } + + private static class Gsm7bitCodingResult { + int septets; + byte[] data; + } + + private static Gsm7bitCodingResult encode7bitGsm(String msg, int septetOffset, boolean force) + throws CodingException + { + try { + /* + * TODO(cleanup): It would be nice if GsmAlphabet provided + * an option to produce just the data without prepending + * the septet count, as this function is really just a + * wrapper to strip that off. Not to mention that the + * septet count is generally known prior to invocation of + * the encoder. Note that it cannot be derived from the + * resulting array length, since that cannot distinguish + * if the last contains either 1 or 8 valid bits. + * + * TODO(cleanup): The BitwiseXStreams could also be + * extended with byte-wise reversed endianness read/write + * routines to allow a corresponding implementation of + * stringToGsm7BitPacked, and potentially directly support + * access to the main bitwise stream from encode/decode. + */ + byte[] fullData = GsmAlphabet.stringToGsm7BitPacked(msg, septetOffset, !force, 0, 0); + Gsm7bitCodingResult result = new Gsm7bitCodingResult(); + result.data = new byte[fullData.length - 1]; + System.arraycopy(fullData, 1, result.data, 0, fullData.length - 1); + result.septets = fullData[0] & 0x00FF; + return result; + } catch (com.android.internal.telephony.EncodeException ex) { + throw new CodingException("7bit GSM encode failed: " + ex); + } + } + + private static void encode7bitEms(UserData uData, byte[] udhData, boolean force) + throws CodingException + { + int udhBytes = udhData.length + 1; // Add length octet. + int udhSeptets = ((udhBytes * 8) + 6) / 7; + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, udhSeptets, force); + uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + uData.msgEncodingSet = true; + uData.numFields = gcr.septets; + uData.payload = gcr.data; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + } + + private static void encode16bitEms(UserData uData, byte[] udhData) + throws CodingException + { + byte[] payload = encodeUtf16(uData.payloadStr); + int udhBytes = udhData.length + 1; // Add length octet. + int udhCodeUnits = (udhBytes + 1) / 2; + int udhPadding = udhBytes % 2; + int payloadCodeUnits = payload.length / 2; + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + uData.msgEncodingSet = true; + uData.numFields = udhCodeUnits + payloadCodeUnits; + uData.payload = new byte[uData.numFields * 2]; + uData.payload[0] = (byte)udhData.length; + System.arraycopy(udhData, 0, uData.payload, 1, udhData.length); + System.arraycopy(payload, 0, uData.payload, udhBytes + udhPadding, payload.length); + } + + private static void encodeEmsUserDataPayload(UserData uData) + throws CodingException + { + byte[] headerData = SmsHeader.toByteArray(uData.userDataHeader); + if (uData.msgEncodingSet) { + if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { + encode7bitEms(uData, headerData, true); + } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { + encode16bitEms(uData, headerData); + } else { + throw new CodingException("unsupported EMS user data encoding (" + + uData.msgEncoding + ")"); + } + } else { + try { + encode7bitEms(uData, headerData, false); + } catch (CodingException ex) { + encode16bitEms(uData, headerData); + } + } + } + + private static void encodeUserDataPayload(UserData uData) + throws CodingException + { + if ((uData.payloadStr == null) && (uData.msgEncoding != UserData.ENCODING_OCTET)) { + Log.e(LOG_TAG, "user data with null payloadStr"); + uData.payloadStr = ""; + } + + if (uData.userDataHeader != null) { + encodeEmsUserDataPayload(uData); + return; + } + + if (uData.msgEncodingSet) { + if (uData.msgEncoding == UserData.ENCODING_OCTET) { + if (uData.payload == null) { + Log.e(LOG_TAG, "user data with octet encoding but null payload"); + uData.payload = new byte[0]; + uData.numFields = 0; + } else { + uData.numFields = uData.payload.length; + } + } else { + if (uData.payloadStr == null) { + Log.e(LOG_TAG, "non-octet user data with null payloadStr"); + uData.payloadStr = ""; + } + if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) { + Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, 0, true); + uData.payload = gcr.data; + uData.numFields = gcr.septets; + } else if (uData.msgEncoding == UserData.ENCODING_7BIT_ASCII) { + uData.payload = encode7bitAscii(uData.payloadStr, true); + uData.numFields = uData.payloadStr.length(); + } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) { + uData.payload = encodeUtf16(uData.payloadStr); + uData.numFields = uData.payloadStr.length(); + } else { + throw new CodingException("unsupported user data encoding (" + + uData.msgEncoding + ")"); + } + } + } else { + try { + uData.payload = encode7bitAscii(uData.payloadStr, false); + uData.msgEncoding = UserData.ENCODING_7BIT_ASCII; + } catch (CodingException ex) { + uData.payload = encodeUtf16(uData.payloadStr); + uData.msgEncoding = UserData.ENCODING_UNICODE_16; + } + uData.numFields = uData.payloadStr.length(); + uData.msgEncodingSet = true; + } + } + + private static void encodeUserData(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException, CodingException + { + /* + * TODO(cleanup): Do we really need to set userData.payload as + * a side effect of encoding? If not, we could avoid data + * copies by passing outStream directly. + */ + encodeUserDataPayload(bData.userData); + bData.hasUserDataHeader = bData.userData.userDataHeader != null; + + if (bData.userData.payload.length > SmsConstants.MAX_USER_DATA_BYTES) { + throw new CodingException("encoded user data too large (" + + bData.userData.payload.length + + " > " + SmsConstants.MAX_USER_DATA_BYTES + " bytes)"); + } + + /* + * TODO(cleanup): figure out what the right answer is WRT paddingBits field + * + * userData.paddingBits = (userData.payload.length * 8) - (userData.numFields * 7); + * userData.paddingBits = 0; // XXX this seems better, but why? + * + */ + int dataBits = (bData.userData.payload.length * 8) - bData.userData.paddingBits; + int paramBits = dataBits + 13; + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + paramBits += 8; + } + int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0); + int paddingBits = (paramBytes * 8) - paramBits; + outStream.write(8, paramBytes); + outStream.write(5, bData.userData.msgEncoding); + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + outStream.write(8, bData.userData.msgType); + } + outStream.write(8, bData.userData.numFields); + outStream.writeByteArray(dataBits, bData.userData.payload); + if (paddingBits > 0) outStream.write(paddingBits, 0); + } + + private static void encodeReplyOption(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(1, bData.userAckReq ? 1 : 0); + outStream.write(1, bData.deliveryAckReq ? 1 : 0); + outStream.write(1, bData.readAckReq ? 1 : 0); + outStream.write(1, bData.reportReq ? 1 : 0); + outStream.write(4, 0); + } + + private static byte[] encodeDtmfSmsAddress(String address) { + int digits = address.length(); + int dataBits = digits * 4; + int dataBytes = (dataBits / 8); + dataBytes += (dataBits % 8) > 0 ? 1 : 0; + byte[] rawData = new byte[dataBytes]; + for (int i = 0; i < digits; i++) { + char c = address.charAt(i); + int val = 0; + if ((c >= '1') && (c <= '9')) val = c - '0'; + else if (c == '0') val = 10; + else if (c == '*') val = 11; + else if (c == '#') val = 12; + else return null; + rawData[i / 2] |= val << (4 - ((i % 2) * 4)); + } + return rawData; + } + + /* + * TODO(cleanup): CdmaSmsAddress encoding should make use of + * CdmaSmsAddress.parse provided that DTMF encoding is unified, + * and the difference in 4-bit vs. 8-bit is resolved. + */ + + private static void encodeCdmaSmsAddress(CdmaSmsAddress addr) throws CodingException { + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + try { + addr.origBytes = addr.address.getBytes("US-ASCII"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("invalid SMS address, cannot convert to ASCII"); + } + } else { + addr.origBytes = encodeDtmfSmsAddress(addr.address); + } + } + + private static void encodeCallbackNumber(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException, CodingException + { + CdmaSmsAddress addr = bData.callbackNumber; + encodeCdmaSmsAddress(addr); + int paramBits = 9; + int dataBits = 0; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + paramBits += 7; + dataBits = addr.numberOfDigits * 8; + } else { + dataBits = addr.numberOfDigits * 4; + } + paramBits += dataBits; + int paramBytes = (paramBits / 8) + ((paramBits % 8) > 0 ? 1 : 0); + int paddingBits = (paramBytes * 8) - paramBits; + outStream.write(8, paramBytes); + outStream.write(1, addr.digitMode); + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + outStream.write(3, addr.ton); + outStream.write(4, addr.numberPlan); + } + outStream.write(8, addr.numberOfDigits); + outStream.writeByteArray(dataBits, addr.origBytes); + if (paddingBits > 0) outStream.write(paddingBits, 0); + } + + private static void encodeMsgStatus(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.errorClass); + outStream.write(6, bData.messageStatus); + } + + private static void encodeMsgCount(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.numberOfMessages); + } + + private static void encodeValidityPeriodRel(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.validityPeriodRelative); + } + + private static void encodePrivacyIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.privacy); + outStream.skip(6); + } + + private static void encodeLanguageIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(8, bData.language); + } + + private static void encodeDisplayMode(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.displayMode); + outStream.skip(6); + } + + private static void encodePriorityIndicator(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.priority); + outStream.skip(6); + } + + private static void encodeMsgDeliveryAlert(BearerData bData, BitwiseOutputStream outStream) + throws BitwiseOutputStream.AccessException + { + outStream.write(8, 1); + outStream.write(2, bData.alert); + outStream.skip(6); + } + + /** + * Create serialized representation for BearerData object. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param bData an instance of BearerData. + * + * @return byte array of raw encoded SMS bearer data. + */ + public static byte[] encode(BearerData bData) { + bData.hasUserDataHeader = ((bData.userData != null) && + (bData.userData.userDataHeader != null)); + try { + BitwiseOutputStream outStream = new BitwiseOutputStream(200); + outStream.write(8, SUBPARAM_MESSAGE_IDENTIFIER); + encodeMessageId(bData, outStream); + if (bData.userData != null) { + outStream.write(8, SUBPARAM_USER_DATA); + encodeUserData(bData, outStream); + } + if (bData.callbackNumber != null) { + outStream.write(8, SUBPARAM_CALLBACK_NUMBER); + encodeCallbackNumber(bData, outStream); + } + if (bData.userAckReq || bData.deliveryAckReq || bData.readAckReq || bData.reportReq) { + outStream.write(8, SUBPARAM_REPLY_OPTION); + encodeReplyOption(bData, outStream); + } + if (bData.numberOfMessages != 0) { + outStream.write(8, SUBPARAM_NUMBER_OF_MESSAGES); + encodeMsgCount(bData, outStream); + } + if (bData.validityPeriodRelativeSet) { + outStream.write(8, SUBPARAM_VALIDITY_PERIOD_RELATIVE); + encodeValidityPeriodRel(bData, outStream); + } + if (bData.privacyIndicatorSet) { + outStream.write(8, SUBPARAM_PRIVACY_INDICATOR); + encodePrivacyIndicator(bData, outStream); + } + if (bData.languageIndicatorSet) { + outStream.write(8, SUBPARAM_LANGUAGE_INDICATOR); + encodeLanguageIndicator(bData, outStream); + } + if (bData.displayModeSet) { + outStream.write(8, SUBPARAM_MESSAGE_DISPLAY_MODE); + encodeDisplayMode(bData, outStream); + } + if (bData.priorityIndicatorSet) { + outStream.write(8, SUBPARAM_PRIORITY_INDICATOR); + encodePriorityIndicator(bData, outStream); + } + if (bData.alertIndicatorSet) { + outStream.write(8, SUBPARAM_ALERT_ON_MESSAGE_DELIVERY); + encodeMsgDeliveryAlert(bData, outStream); + } + if (bData.messageStatusSet) { + outStream.write(8, SUBPARAM_MESSAGE_STATUS); + encodeMsgStatus(bData, outStream); + } + return outStream.toByteArray(); + } catch (BitwiseOutputStream.AccessException ex) { + Log.e(LOG_TAG, "BearerData encode failed: " + ex); + } catch (CodingException ex) { + Log.e(LOG_TAG, "BearerData encode failed: " + ex); + } + return null; + } + + private static boolean decodeMessageId(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 3 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.messageType = inStream.read(4); + bData.messageId = inStream.read(8) << 8; + bData.messageId |= inStream.read(8); + bData.hasUserDataHeader = (inStream.read(1) == 1); + inStream.skip(3); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "MESSAGE_IDENTIFIER decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeUserData(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException + { + int paramBits = inStream.read(8) * 8; + bData.userData = new UserData(); + bData.userData.msgEncoding = inStream.read(5); + bData.userData.msgEncodingSet = true; + bData.userData.msgType = 0; + int consumedBits = 5; + if ((bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) || + (bData.userData.msgEncoding == UserData.ENCODING_GSM_DCS)) { + bData.userData.msgType = inStream.read(8); + consumedBits += 8; + } + bData.userData.numFields = inStream.read(8); + consumedBits += 8; + int dataBits = paramBits - consumedBits; + bData.userData.payload = inStream.readByteArray(dataBits); + return true; + } + + private static String decodeUtf8(byte[] data, int offset, int numFields) + throws CodingException + { + if (numFields < 0 || (numFields + offset) > data.length) { + throw new CodingException("UTF-8 decode failed: offset or length out of range"); + } + try { + return new String(data, offset, numFields, "UTF-8"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("UTF-8 decode failed: " + ex); + } + } + + private static String decodeUtf16(byte[] data, int offset, int numFields) + throws CodingException + { + int byteCount = numFields * 2; + if (byteCount < 0 || (byteCount + offset) > data.length) { + throw new CodingException("UTF-16 decode failed: offset or length out of range"); + } + try { + return new String(data, offset, byteCount, "utf-16be"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("UTF-16 decode failed: " + ex); + } + } + + private static String decode7bitAscii(byte[] data, int offset, int numFields) + throws CodingException + { + try { + offset *= 8; + StringBuffer strBuf = new StringBuffer(numFields); + BitwiseInputStream inStream = new BitwiseInputStream(data); + int wantedBits = (offset * 8) + (numFields * 7); + if (inStream.available() < wantedBits) { + throw new CodingException("insufficient data (wanted " + wantedBits + + " bits, but only have " + inStream.available() + ")"); + } + inStream.skip(offset); + for (int i = 0; i < numFields; i++) { + int charCode = inStream.read(7); + if ((charCode >= UserData.ASCII_MAP_BASE_INDEX) && + (charCode <= UserData.ASCII_MAP_MAX_INDEX)) { + strBuf.append(UserData.ASCII_MAP[charCode - UserData.ASCII_MAP_BASE_INDEX]); + } else if (charCode == UserData.ASCII_NL_INDEX) { + strBuf.append('\n'); + } else if (charCode == UserData.ASCII_CR_INDEX) { + strBuf.append('\r'); + } else { + /* For other charCodes, they are unprintable, and so simply use SPACE. */ + strBuf.append(' '); + } + } + return strBuf.toString(); + } catch (BitwiseInputStream.AccessException ex) { + throw new CodingException("7bit ASCII decode failed: " + ex); + } + } + + private static String decode7bitGsm(byte[] data, int offset, int numFields) + throws CodingException + { + // Start reading from the next 7-bit aligned boundary after offset. + int offsetBits = offset * 8; + int offsetSeptets = (offsetBits + 6) / 7; + numFields -= offsetSeptets; + int paddingBits = (offsetSeptets * 7) - offsetBits; + String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits, + 0, 0); + if (result == null) { + throw new CodingException("7bit GSM decoding failed"); + } + return result; + } + + private static String decodeLatin(byte[] data, int offset, int numFields) + throws CodingException + { + if (numFields < 0 || (numFields + offset) > data.length) { + throw new CodingException("ISO-8859-1 decode failed: offset or length out of range"); + } + try { + return new String(data, offset, numFields, "ISO-8859-1"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("ISO-8859-1 decode failed: " + ex); + } + } + + private static void decodeUserDataPayload(UserData userData, boolean hasUserDataHeader) + throws CodingException + { + int offset = 0; + if (hasUserDataHeader) { + int udhLen = userData.payload[0] & 0x00FF; + offset += udhLen + 1; + byte[] headerData = new byte[udhLen]; + System.arraycopy(userData.payload, 1, headerData, 0, udhLen); + userData.userDataHeader = SmsHeader.fromByteArray(headerData); + } + switch (userData.msgEncoding) { + case UserData.ENCODING_OCTET: + /* + * Octet decoding depends on the carrier service. + */ + boolean decodingtypeUTF8 = Resources.getSystem() + .getBoolean(com.android.internal.R.bool.config_sms_utf8_support); + + // Strip off any padding bytes, meaning any differences between the length of the + // array and the target length specified by numFields. This is to avoid any + // confusion by code elsewhere that only considers the payload array length. + byte[] payload = new byte[userData.numFields]; + int copyLen = userData.numFields < userData.payload.length + ? userData.numFields : userData.payload.length; + + System.arraycopy(userData.payload, 0, payload, 0, copyLen); + userData.payload = payload; + + if (!decodingtypeUTF8) { + // There are many devices in the market that send 8bit text sms (latin encoded) as + // octet encoded. + userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields); + } else { + userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields); + } + break; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_UNICODE_16: + userData.payloadStr = decodeUtf16(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_GSM_7BIT_ALPHABET: + userData.payloadStr = decode7bitGsm(userData.payload, offset, userData.numFields); + break; + case UserData.ENCODING_LATIN: + userData.payloadStr = decodeLatin(userData.payload, offset, userData.numFields); + break; + default: + throw new CodingException("unsupported user data encoding (" + + userData.msgEncoding + ")"); + } + } + + /** + * IS-91 Voice Mail message decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * (For character encodings, see TIA/EIA/IS-91, Annex B) + * + * Protocol Summary: The user data payload may contain 3-14 + * characters. The first two characters are parsed as a number + * and indicate the number of voicemails. The third character is + * either a SPACE or '!' to indicate normal or urgent priority, + * respectively. Any following characters are treated as normal + * text user data payload. + * + * Note that the characters encoding is 6-bit packed. + */ + private static void decodeIs91VoicemailStatus(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 6; // 6-bit packed character encoding. + int numFields = bData.userData.numFields; + if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) { + throw new CodingException("IS-91 voicemail status decoding failed"); + } + try { + StringBuffer strbuf = new StringBuffer(dataLen); + while (inStream.available() >= 6) { + strbuf.append(UserData.ASCII_MAP[inStream.read(6)]); + } + String data = strbuf.toString(); + bData.numberOfMessages = Integer.parseInt(data.substring(0, 2)); + char prioCode = data.charAt(2); + if (prioCode == ' ') { + bData.priority = PRIORITY_NORMAL; + } else if (prioCode == '!') { + bData.priority = PRIORITY_URGENT; + } else { + throw new CodingException("IS-91 voicemail status decoding failed: " + + "illegal priority setting (" + prioCode + ")"); + } + bData.priorityIndicatorSet = true; + bData.userData.payloadStr = data.substring(3, numFields - 3); + } catch (java.lang.NumberFormatException ex) { + throw new CodingException("IS-91 voicemail status decoding failed: " + ex); + } catch (java.lang.IndexOutOfBoundsException ex) { + throw new CodingException("IS-91 voicemail status decoding failed: " + ex); + } + } + + /** + * IS-91 Short Message decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * (For character encodings, see TIA/EIA/IS-91, Annex B) + * + * Protocol Summary: The user data payload may contain 1-14 + * characters, which are treated as normal text user data payload. + * Note that the characters encoding is 6-bit packed. + */ + private static void decodeIs91ShortMessage(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 6; // 6-bit packed character encoding. + int numFields = bData.userData.numFields; + // dataLen may be > 14 characters due to octet padding + if ((numFields > 14) || (dataLen < numFields)) { + throw new CodingException("IS-91 short message decoding failed"); + } + StringBuffer strbuf = new StringBuffer(dataLen); + for (int i = 0; i < numFields; i++) { + strbuf.append(UserData.ASCII_MAP[inStream.read(6)]); + } + bData.userData.payloadStr = strbuf.toString(); + } + + /** + * IS-91 CLI message (callback number) decoding + * (See 3GPP2 C.S0015-A, Table 4.3.1.4.1-1) + * + * Protocol Summary: The data payload may contain 1-32 digits, + * encoded using standard 4-bit DTMF, which are treated as a + * callback number. + */ + private static void decodeIs91Cli(BearerData bData) throws CodingException { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + int dataLen = inStream.available() / 4; // 4-bit packed DTMF digit encoding. + int numFields = bData.userData.numFields; + if ((dataLen > 14) || (dataLen < 3) || (dataLen < numFields)) { + throw new CodingException("IS-91 voicemail status decoding failed"); + } + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.digitMode = CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF; + addr.origBytes = bData.userData.payload; + addr.numberOfDigits = (byte)numFields; + decodeSmsAddress(addr); + bData.callbackNumber = addr; + } + + private static void decodeIs91(BearerData bData) + throws BitwiseInputStream.AccessException, CodingException + { + switch (bData.userData.msgType) { + case UserData.IS91_MSG_TYPE_VOICEMAIL_STATUS: + decodeIs91VoicemailStatus(bData); + break; + case UserData.IS91_MSG_TYPE_CLI: + decodeIs91Cli(bData); + break; + case UserData.IS91_MSG_TYPE_SHORT_MESSAGE_FULL: + case UserData.IS91_MSG_TYPE_SHORT_MESSAGE: + decodeIs91ShortMessage(bData); + break; + default: + throw new CodingException("unsupported IS-91 message type (" + + bData.userData.msgType + ")"); + } + } + + private static boolean decodeReplyOption(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.userAckReq = (inStream.read(1) == 1); + bData.deliveryAckReq = (inStream.read(1) == 1); + bData.readAckReq = (inStream.read(1) == 1); + bData.reportReq = (inStream.read(1) == 1); + inStream.skip(4); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "REPLY_OPTION decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeMsgCount(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.numberOfMessages = IccUtils.cdmaBcdByteToInt((byte)inStream.read(8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "NUMBER_OF_MESSAGES decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeDepositIndex(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 2 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.depositIndex = (inStream.read(8) << 8) | inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "MESSAGE_DEPOSIT_INDEX decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static String decodeDtmfSmsAddress(byte[] rawData, int numFields) + throws CodingException + { + /* DTMF 4-bit digit encoding, defined in at + * 3GPP2 C.S005-D, v2.0, table 2.7.1.3.2.4-4 */ + StringBuffer strBuf = new StringBuffer(numFields); + for (int i = 0; i < numFields; i++) { + int val = 0x0F & (rawData[i / 2] >>> (4 - ((i % 2) * 4))); + if ((val >= 1) && (val <= 9)) strBuf.append(Integer.toString(val, 10)); + else if (val == 10) strBuf.append('0'); + else if (val == 11) strBuf.append('*'); + else if (val == 12) strBuf.append('#'); + else throw new CodingException("invalid SMS address DTMF code (" + val + ")"); + } + return strBuf.toString(); + } + + private static void decodeSmsAddress(CdmaSmsAddress addr) throws CodingException { + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + try { + /* As specified in 3GPP2 C.S0015-B, v2, 4.5.15 -- actually + * just 7-bit ASCII encoding, with the MSB being zero. */ + addr.address = new String(addr.origBytes, 0, addr.origBytes.length, "US-ASCII"); + } catch (java.io.UnsupportedEncodingException ex) { + throw new CodingException("invalid SMS address ASCII code"); + } + } else { + addr.address = decodeDtmfSmsAddress(addr.origBytes, addr.numberOfDigits); + } + } + + private static boolean decodeCallbackNumber(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + int paramBits = inStream.read(8) * 8; + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.digitMode = inStream.read(1); + byte fieldBits = 4; + byte consumedBits = 1; + if (addr.digitMode == CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR) { + addr.ton = inStream.read(3); + addr.numberPlan = inStream.read(4); + fieldBits = 8; + consumedBits += 7; + } + addr.numberOfDigits = inStream.read(8); + consumedBits += 8; + int remainingBits = paramBits - consumedBits; + int dataBits = addr.numberOfDigits * fieldBits; + int paddingBits = remainingBits - dataBits; + if (remainingBits < dataBits) { + throw new CodingException("CALLBACK_NUMBER subparam encoding size error (" + + "remainingBits + " + remainingBits + ", dataBits + " + + dataBits + ", paddingBits + " + paddingBits + ")"); + } + addr.origBytes = inStream.readByteArray(dataBits); + inStream.skip(paddingBits); + decodeSmsAddress(addr); + bData.callbackNumber = addr; + return true; + } + + private static boolean decodeMsgStatus(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.errorClass = inStream.read(2); + bData.messageStatus = inStream.read(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "MESSAGE_STATUS decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.messageStatusSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeMsgCenterTimeStamp(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.msgCenterTimeStamp = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "MESSAGE_CENTER_TIME_STAMP decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeValidityAbs(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.validityPeriodAbsolute = TimeStamp.fromByteArray(inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "VALIDITY_PERIOD_ABSOLUTE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeDeferredDeliveryAbs(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 6 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.deferredDeliveryTimeAbsolute = TimeStamp.fromByteArray( + inStream.readByteArray(6 * 8)); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_ABSOLUTE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + return decodeSuccess; + } + + private static boolean decodeValidityRel(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.deferredDeliveryTimeRelative = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "VALIDITY_PERIOD_RELATIVE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.deferredDeliveryTimeRelativeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeDeferredDeliveryRel(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.validityPeriodRelative = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "DEFERRED_DELIVERY_TIME_RELATIVE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.validityPeriodRelativeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodePrivacyIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.privacy = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "PRIVACY_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.privacyIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeLanguageIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.language = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "LANGUAGE_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.languageIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeDisplayMode(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.displayMode = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "DISPLAY_MODE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.displayModeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodePriorityIndicator(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.priority = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "PRIORITY_INDICATOR decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.priorityIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeMsgDeliveryAlert(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.alert = inStream.read(2); + inStream.skip(6); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "ALERT_ON_MESSAGE_DELIVERY decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.alertIndicatorSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeUserResponseCode(BearerData bData, BitwiseInputStream inStream) + throws BitwiseInputStream.AccessException, CodingException + { + final int EXPECTED_PARAM_SIZE = 1 * 8; + boolean decodeSuccess = false; + int paramBits = inStream.read(8) * 8; + if (paramBits >= EXPECTED_PARAM_SIZE) { + paramBits -= EXPECTED_PARAM_SIZE; + decodeSuccess = true; + bData.userResponseCode = inStream.read(8); + } + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "USER_RESPONSE_CODE decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ")"); + } + inStream.skip(paramBits); + bData.userResponseCodeSet = decodeSuccess; + return decodeSuccess; + } + + private static boolean decodeServiceCategoryProgramData(BearerData bData, + BitwiseInputStream inStream) throws BitwiseInputStream.AccessException, CodingException + { + if (inStream.available() < 13) { + throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only " + + inStream.available() + " bits available"); + } + + int paramBits = inStream.read(8) * 8; + int msgEncoding = inStream.read(5); + paramBits -= 5; + + if (inStream.available() < paramBits) { + throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only " + + inStream.available() + " bits available (" + paramBits + " bits expected)"); + } + + ArrayList<CdmaSmsCbProgramData> programDataList = new ArrayList<CdmaSmsCbProgramData>(); + + final int CATEGORY_FIELD_MIN_SIZE = 6 * 8; + boolean decodeSuccess = false; + while (paramBits >= CATEGORY_FIELD_MIN_SIZE) { + int operation = inStream.read(4); + int category = (inStream.read(8) << 8) | inStream.read(8); + String language = getLanguageCodeForValue(inStream.read(8)); + int maxMessages = inStream.read(8); + int alertOption = inStream.read(4); + int numFields = inStream.read(8); + paramBits -= CATEGORY_FIELD_MIN_SIZE; + + int textBits = getBitsForNumFields(msgEncoding, numFields); + if (paramBits < textBits) { + throw new CodingException("category name is " + textBits + " bits in length," + + " but there are only " + paramBits + " bits available"); + } + + UserData userData = new UserData(); + userData.msgEncoding = msgEncoding; + userData.msgEncodingSet = true; + userData.numFields = numFields; + userData.payload = inStream.readByteArray(textBits); + paramBits -= textBits; + + decodeUserDataPayload(userData, false); + String categoryName = userData.payloadStr; + CdmaSmsCbProgramData programData = new CdmaSmsCbProgramData(operation, category, + language, maxMessages, alertOption, categoryName); + programDataList.add(programData); + + decodeSuccess = true; + } + + if ((! decodeSuccess) || (paramBits > 0)) { + Log.d(LOG_TAG, "SERVICE_CATEGORY_PROGRAM_DATA decode " + + (decodeSuccess ? "succeeded" : "failed") + + " (extra bits = " + paramBits + ')'); + } + + inStream.skip(paramBits); + bData.serviceCategoryProgramData = programDataList; + return decodeSuccess; + } + + private static int serviceCategoryToCmasMessageClass(int serviceCategory) { + switch (serviceCategory) { + case SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT: + return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY: + return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE: + return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + + default: + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Calculates the number of bits to read for the specified number of encoded characters. + * @param msgEncoding the message encoding to use + * @param numFields the number of characters to read. For Shift-JIS and Korean encodings, + * this is the number of bytes to read. + * @return the number of bits to read from the stream + * @throws CodingException if the specified encoding is not supported + */ + private static int getBitsForNumFields(int msgEncoding, int numFields) + throws CodingException { + switch (msgEncoding) { + case UserData.ENCODING_OCTET: + case UserData.ENCODING_SHIFT_JIS: + case UserData.ENCODING_KOREAN: + case UserData.ENCODING_LATIN: + case UserData.ENCODING_LATIN_HEBREW: + return numFields * 8; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + case UserData.ENCODING_GSM_7BIT_ALPHABET: + return numFields * 7; + + case UserData.ENCODING_UNICODE_16: + return numFields * 16; + + default: + throw new CodingException("unsupported message encoding (" + msgEncoding + ')'); + } + } + + /** + * CMAS message decoding. + * (See TIA-1149-0-1, CMAS over CDMA) + * + * @param serviceCategory is the service category from the SMS envelope + */ + private static void decodeCmasUserData(BearerData bData, int serviceCategory) + throws BitwiseInputStream.AccessException, CodingException { + BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload); + if (inStream.available() < 8) { + throw new CodingException("emergency CB with no CMAE_protocol_version"); + } + int protocolVersion = inStream.read(8); + if (protocolVersion != 0) { + throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion); + } + + int messageClass = serviceCategoryToCmasMessageClass(serviceCategory); + int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + + while (inStream.available() >= 16) { + int recordType = inStream.read(8); + int recordLen = inStream.read(8); + switch (recordType) { + case 0: // Type 0 elements (Alert text) + UserData alertUserData = new UserData(); + alertUserData.msgEncoding = inStream.read(5); + alertUserData.msgEncodingSet = true; + alertUserData.msgType = 0; + + int numFields; // number of chars to decode + switch (alertUserData.msgEncoding) { + case UserData.ENCODING_OCTET: + case UserData.ENCODING_LATIN: + numFields = recordLen - 1; // subtract 1 byte for encoding + break; + + case UserData.ENCODING_IA5: + case UserData.ENCODING_7BIT_ASCII: + case UserData.ENCODING_GSM_7BIT_ALPHABET: + numFields = ((recordLen * 8) - 5) / 7; // subtract 5 bits for encoding + break; + + case UserData.ENCODING_UNICODE_16: + numFields = (recordLen - 1) / 2; + break; + + default: + numFields = 0; // unsupported encoding + } + + alertUserData.numFields = numFields; + alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5); + decodeUserDataPayload(alertUserData, false); + bData.userData = alertUserData; + break; + + case 1: // Type 1 elements + category = inStream.read(8); + responseType = inStream.read(8); + severity = inStream.read(4); + urgency = inStream.read(4); + certainty = inStream.read(4); + inStream.skip(recordLen * 8 - 28); + break; + + default: + Log.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType); + inStream.skip(recordLen * 8); + break; + } + } + + bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity, + urgency, certainty); + } + + /** + * Create BearerData object from serialized representation. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param smsData byte array of raw encoded SMS bearer data. + * @return an instance of BearerData. + */ + public static BearerData decode(byte[] smsData) { + return decode(smsData, 0); + } + + private static boolean isCmasAlertCategory(int category) { + return category >= SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT + && category <= SmsEnvelope.SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE; + } + + /** + * Create BearerData object from serialized representation. + * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details) + * + * @param smsData byte array of raw encoded SMS bearer data. + * @param serviceCategory the envelope service category (for CMAS alert handling) + * @return an instance of BearerData. + */ + public static BearerData decode(byte[] smsData, int serviceCategory) { + try { + BitwiseInputStream inStream = new BitwiseInputStream(smsData); + BearerData bData = new BearerData(); + int foundSubparamMask = 0; + while (inStream.available() > 0) { + int subparamId = inStream.read(8); + int subparamIdBit = 1 << subparamId; + if ((foundSubparamMask & subparamIdBit) != 0) { + throw new CodingException("illegal duplicate subparameter (" + + subparamId + ")"); + } + boolean decodeSuccess; + switch (subparamId) { + case SUBPARAM_MESSAGE_IDENTIFIER: + decodeSuccess = decodeMessageId(bData, inStream); + break; + case SUBPARAM_USER_DATA: + decodeSuccess = decodeUserData(bData, inStream); + break; + case SUBPARAM_USER_RESPONSE_CODE: + decodeSuccess = decodeUserResponseCode(bData, inStream); + break; + case SUBPARAM_REPLY_OPTION: + decodeSuccess = decodeReplyOption(bData, inStream); + break; + case SUBPARAM_NUMBER_OF_MESSAGES: + decodeSuccess = decodeMsgCount(bData, inStream); + break; + case SUBPARAM_CALLBACK_NUMBER: + decodeSuccess = decodeCallbackNumber(bData, inStream); + break; + case SUBPARAM_MESSAGE_STATUS: + decodeSuccess = decodeMsgStatus(bData, inStream); + break; + case SUBPARAM_MESSAGE_CENTER_TIME_STAMP: + decodeSuccess = decodeMsgCenterTimeStamp(bData, inStream); + break; + case SUBPARAM_VALIDITY_PERIOD_ABSOLUTE: + decodeSuccess = decodeValidityAbs(bData, inStream); + break; + case SUBPARAM_VALIDITY_PERIOD_RELATIVE: + decodeSuccess = decodeValidityRel(bData, inStream); + break; + case SUBPARAM_DEFERRED_DELIVERY_TIME_ABSOLUTE: + decodeSuccess = decodeDeferredDeliveryAbs(bData, inStream); + break; + case SUBPARAM_DEFERRED_DELIVERY_TIME_RELATIVE: + decodeSuccess = decodeDeferredDeliveryRel(bData, inStream); + break; + case SUBPARAM_PRIVACY_INDICATOR: + decodeSuccess = decodePrivacyIndicator(bData, inStream); + break; + case SUBPARAM_LANGUAGE_INDICATOR: + decodeSuccess = decodeLanguageIndicator(bData, inStream); + break; + case SUBPARAM_MESSAGE_DISPLAY_MODE: + decodeSuccess = decodeDisplayMode(bData, inStream); + break; + case SUBPARAM_PRIORITY_INDICATOR: + decodeSuccess = decodePriorityIndicator(bData, inStream); + break; + case SUBPARAM_ALERT_ON_MESSAGE_DELIVERY: + decodeSuccess = decodeMsgDeliveryAlert(bData, inStream); + break; + case SUBPARAM_MESSAGE_DEPOSIT_INDEX: + decodeSuccess = decodeDepositIndex(bData, inStream); + break; + case SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA: + decodeSuccess = decodeServiceCategoryProgramData(bData, inStream); + break; + default: + throw new CodingException("unsupported bearer data subparameter (" + + subparamId + ")"); + } + if (decodeSuccess) foundSubparamMask |= subparamIdBit; + } + if ((foundSubparamMask & (1 << SUBPARAM_MESSAGE_IDENTIFIER)) == 0) { + throw new CodingException("missing MESSAGE_IDENTIFIER subparam"); + } + if (bData.userData != null) { + if (isCmasAlertCategory(serviceCategory)) { + decodeCmasUserData(bData, serviceCategory); + } else if (bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) { + if ((foundSubparamMask ^ + (1 << SUBPARAM_MESSAGE_IDENTIFIER) ^ + (1 << SUBPARAM_USER_DATA)) + != 0) { + Log.e(LOG_TAG, "IS-91 must occur without extra subparams (" + + foundSubparamMask + ")"); + } + decodeIs91(bData); + } else { + decodeUserDataPayload(bData.userData, bData.hasUserDataHeader); + } + } + return bData; + } catch (BitwiseInputStream.AccessException ex) { + Log.e(LOG_TAG, "BearerData decode failed: " + ex); + } catch (CodingException ex) { + Log.e(LOG_TAG, "BearerData decode failed: " + ex); + } + return null; + } +} diff --git a/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java b/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java new file mode 100644 index 0000000..5f2e561 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsAddress.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.util.SparseBooleanArray; + +import com.android.internal.telephony.SmsAddress; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.HexDump; + +public class CdmaSmsAddress extends SmsAddress { + + /** + * Digit Mode Indicator is a 1-bit value that indicates whether + * the address digits are 4-bit DTMF codes or 8-bit codes. (See + * 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + static public final int DIGIT_MODE_4BIT_DTMF = 0x00; + static public final int DIGIT_MODE_8BIT_CHAR = 0x01; + + public int digitMode; + + /** + * Number Mode Indicator is 1-bit value that indicates whether the + * address type is a data network address or not. (See 3GPP2 + * C.S0015-B, v2, 3.4.3.3) + */ + static public final int NUMBER_MODE_NOT_DATA_NETWORK = 0x00; + static public final int NUMBER_MODE_DATA_NETWORK = 0x01; + + public int numberMode; + + /** + * Number Types for data networks. + * (See 3GPP2 C.S005-D, table2.7.1.3.2.4-2 for complete table) + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3 for data network subset) + * NOTE: value is stored in the parent class ton field. + */ + static public final int TON_UNKNOWN = 0x00; + static public final int TON_INTERNATIONAL_OR_IP = 0x01; + static public final int TON_NATIONAL_OR_EMAIL = 0x02; + static public final int TON_NETWORK = 0x03; + static public final int TON_SUBSCRIBER = 0x04; + static public final int TON_ALPHANUMERIC = 0x05; + static public final int TON_ABBREVIATED = 0x06; + static public final int TON_RESERVED = 0x07; + + /** + * Maximum lengths for fields as defined in ril_cdma_sms.h. + */ + static public final int SMS_ADDRESS_MAX = 36; + static public final int SMS_SUBADDRESS_MAX = 36; + + /** + * This field shall be set to the number of address digits + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public int numberOfDigits; + + /** + * Numbering Plan identification is a 0 or 4-bit value that + * indicates which numbering plan identification is set. (See + * 3GPP2, C.S0015-B, v2, 3.4.3.3 and C.S005-D, table2.7.1.3.2.4-3) + */ + static public final int NUMBERING_PLAN_UNKNOWN = 0x0; + static public final int NUMBERING_PLAN_ISDN_TELEPHONY = 0x1; + //static protected final int NUMBERING_PLAN_DATA = 0x3; + //static protected final int NUMBERING_PLAN_TELEX = 0x4; + //static protected final int NUMBERING_PLAN_PRIVATE = 0x9; + + public int numberPlan; + + /** + * NOTE: the parsed string address and the raw byte array values + * are stored in the parent class address and origBytes fields, + * respectively. + */ + + public CdmaSmsAddress(){ + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CdmaSmsAddress "); + builder.append("{ digitMode=" + digitMode); + builder.append(", numberMode=" + numberMode); + builder.append(", numberPlan=" + numberPlan); + builder.append(", numberOfDigits=" + numberOfDigits); + builder.append(", ton=" + ton); + builder.append(", address=\"" + address + "\""); + builder.append(", origBytes=" + HexDump.toHexString(origBytes)); + builder.append(" }"); + return builder.toString(); + } + + /* + * TODO(cleanup): Refactor the parsing for addresses to better + * share code and logic with GSM. Also, gather all DTMF/BCD + * processing code in one place. + */ + + private static byte[] parseToDtmf(String address) { + int digits = address.length(); + byte[] result = new byte[digits]; + for (int i = 0; i < digits; i++) { + char c = address.charAt(i); + int val = 0; + if ((c >= '1') && (c <= '9')) val = c - '0'; + else if (c == '0') val = 10; + else if (c == '*') val = 11; + else if (c == '#') val = 12; + else return null; + result[i] = (byte)val; + } + return result; + } + + private static final char[] numericCharsDialable = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#' + }; + + private static final char[] numericCharsSugar = { + '(', ')', ' ', '-', '+', '.', '/', '\\' + }; + + private static final SparseBooleanArray numericCharDialableMap = new SparseBooleanArray ( + numericCharsDialable.length + numericCharsSugar.length); + static { + for (int i = 0; i < numericCharsDialable.length; i++) { + numericCharDialableMap.put(numericCharsDialable[i], true); + } + for (int i = 0; i < numericCharsSugar.length; i++) { + numericCharDialableMap.put(numericCharsSugar[i], false); + } + } + + /** + * Given a numeric address string, return the string without + * syntactic sugar, meaning parens, spaces, hyphens/minuses, or + * plus signs. If the input string contains non-numeric + * non-punctuation characters, return null. + */ + private static String filterNumericSugar(String address) { + StringBuilder builder = new StringBuilder(); + int len = address.length(); + for (int i = 0; i < len; i++) { + char c = address.charAt(i); + int mapIndex = numericCharDialableMap.indexOfKey(c); + if (mapIndex < 0) return null; + if (! numericCharDialableMap.valueAt(mapIndex)) continue; + builder.append(c); + } + return builder.toString(); + } + + /** + * Given a string, return the string without whitespace, + * including CR/LF. + */ + private static String filterWhitespace(String address) { + StringBuilder builder = new StringBuilder(); + int len = address.length(); + for (int i = 0; i < len; i++) { + char c = address.charAt(i); + if ((c == ' ') || (c == '\r') || (c == '\n') || (c == '\t')) continue; + builder.append(c); + } + return builder.toString(); + } + + /** + * Given a string, create a corresponding CdmaSmsAddress object. + * + * The result will be null if the input string is not + * representable using printable ASCII. + * + * For numeric addresses, the string is cleaned up by removing + * common punctuation. For alpha addresses, the string is cleaned + * up by removing whitespace. + */ + public static CdmaSmsAddress parse(String address) { + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.address = address; + addr.ton = CdmaSmsAddress.TON_UNKNOWN; + byte[] origBytes = null; + String filteredAddr = filterNumericSugar(address); + if (filteredAddr != null) { + origBytes = parseToDtmf(filteredAddr); + } + if (origBytes != null) { + addr.digitMode = DIGIT_MODE_4BIT_DTMF; + addr.numberMode = NUMBER_MODE_NOT_DATA_NETWORK; + if (address.indexOf('+') != -1) { + addr.ton = TON_INTERNATIONAL_OR_IP; + } + } else { + filteredAddr = filterWhitespace(address); + origBytes = UserData.stringToAscii(filteredAddr); + if (origBytes == null) { + return null; + } + addr.digitMode = DIGIT_MODE_8BIT_CHAR; + addr.numberMode = NUMBER_MODE_DATA_NETWORK; + if (address.indexOf('@') != -1) { + addr.ton = TON_NATIONAL_OR_EMAIL; + } + } + addr.origBytes = origBytes; + addr.numberOfDigits = origBytes.length; + return addr; + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java b/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java new file mode 100644 index 0000000..f9cebf5 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/CdmaSmsSubaddress.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 The Android Open Source Project. All rights reserved. + * Copyright (C) 2010 Code Aurora Forum. All rights reserved. + * + * 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.internal.telephony.cdma.sms; + +public class CdmaSmsSubaddress { + public int type; + + public byte odd; + + public byte[] origBytes; +} + diff --git a/src/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java b/src/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java new file mode 100644 index 0000000..f73df56 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + + +import com.android.internal.telephony.cdma.sms.CdmaSmsSubaddress; + +public final class SmsEnvelope { + /** + * Message Types + * (See 3GPP2 C.S0015-B 3.4.1) + */ + static public final int MESSAGE_TYPE_POINT_TO_POINT = 0x00; + static public final int MESSAGE_TYPE_BROADCAST = 0x01; + static public final int MESSAGE_TYPE_ACKNOWLEDGE = 0x02; + + /** + * Supported Teleservices + * (See 3GPP2 N.S0005 and TIA-41) + */ + static public final int TELESERVICE_NOT_SET = 0x0000; + static public final int TELESERVICE_WMT = 0x1002; + static public final int TELESERVICE_VMN = 0x1003; + static public final int TELESERVICE_WAP = 0x1004; + static public final int TELESERVICE_WEMT = 0x1005; + static public final int TELESERVICE_SCPT = 0x1006; + + /** + * The following are defined as extensions to the standard teleservices + */ + // Voice mail notification through Message Waiting Indication in CDMA mode or Analog mode. + // Defined in 3GPP2 C.S-0005, 3.7.5.6, an Info Record containing an 8-bit number with the + // number of messages waiting, it's used by some CDMA carriers for a voice mail count. + static public final int TELESERVICE_MWI = 0x40000; + + // Service Categories for Cell Broadcast, see 3GPP2 C.R1001 table 9.3.1-1 + // static final int SERVICE_CATEGORY_EMERGENCY = 0x0001; + //... + + // CMAS alert service category assignments, see 3GPP2 C.R1001 table 9.3.3-1 + public static final int SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT = 0x1000; + public static final int SERVICE_CATEGORY_CMAS_EXTREME_THREAT = 0x1001; + public static final int SERVICE_CATEGORY_CMAS_SEVERE_THREAT = 0x1002; + public static final int SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY = 0x1003; + public static final int SERVICE_CATEGORY_CMAS_TEST_MESSAGE = 0x1004; + public static final int SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE = 0x10ff; + + /** + * Provides the type of a SMS message like point to point, broadcast or acknowledge + */ + public int messageType; + + /** + * The 16-bit Teleservice parameter identifies which upper layer service access point is sending + * or receiving the message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.1) + */ + public int teleService = TELESERVICE_NOT_SET; + + /** + * The 16-bit service category parameter identifies the type of service provided + * by the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.2) + */ + public int serviceCategory; + + /** + * The origination address identifies the originator of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public CdmaSmsAddress origAddress; + + /** + * The destination address identifies the target of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.3) + */ + public CdmaSmsAddress destAddress; + + /** + * The origination subaddress identifies the originator of the SMS message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.4) + */ + public CdmaSmsSubaddress origSubaddress; + + /** + * The 6-bit bearer reply parameter is used to request the return of a + * SMS Acknowledge Message. + * (See 3GPP2 C.S0015-B, v2, 3.4.3.5) + */ + public int bearerReply; + + /** + * Cause Code values: + * The cause code parameters are an indication whether an SMS error has occurred and if so, + * whether the condition is considered temporary or permanent. + * ReplySeqNo 6-bit value, + * ErrorClass 2-bit value, + * CauseCode 0-bit or 8-bit value + * (See 3GPP2 C.S0015-B, v2, 3.4.3.6) + */ + public byte replySeqNo; + public byte errorClass; + public byte causeCode; + + /** + * encoded bearer data + * (See 3GPP2 C.S0015-B, v2, 3.4.3.7) + */ + public byte[] bearerData; + + public SmsEnvelope() { + // nothing to see here + } + +} + diff --git a/src/java/com/android/internal/telephony/cdma/sms/UserData.java b/src/java/com/android/internal/telephony/cdma/sms/UserData.java new file mode 100644 index 0000000..599c2b3 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/UserData.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 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 com.android.internal.telephony.cdma.sms; + +import android.util.SparseIntArray; + +import com.android.internal.telephony.SmsHeader; +import com.android.internal.util.HexDump; + +public class UserData { + + /** + * User data encoding types. + * (See 3GPP2 C.R1001-F, v1.0, table 9.1-1) + */ + public static final int ENCODING_OCTET = 0x00; + public static final int ENCODING_IS91_EXTENDED_PROTOCOL = 0x01; + public static final int ENCODING_7BIT_ASCII = 0x02; + public static final int ENCODING_IA5 = 0x03; + public static final int ENCODING_UNICODE_16 = 0x04; + public static final int ENCODING_SHIFT_JIS = 0x05; + public static final int ENCODING_KOREAN = 0x06; + public static final int ENCODING_LATIN_HEBREW = 0x07; + public static final int ENCODING_LATIN = 0x08; + public static final int ENCODING_GSM_7BIT_ALPHABET = 0x09; + public static final int ENCODING_GSM_DCS = 0x0A; + + /** + * IS-91 message types. + * (See TIA/EIS/IS-91-A-ENGL 1999, table 3.7.1.1-3) + */ + public static final int IS91_MSG_TYPE_VOICEMAIL_STATUS = 0x82; + public static final int IS91_MSG_TYPE_SHORT_MESSAGE_FULL = 0x83; + public static final int IS91_MSG_TYPE_CLI = 0x84; + public static final int IS91_MSG_TYPE_SHORT_MESSAGE = 0x85; + + /** + * US ASCII character mapping table. + * + * This table contains only the printable ASCII characters, with a + * 0x20 offset, meaning that the ASCII SPACE character is at index + * 0, with the resulting code of 0x20. + * + * Note this mapping is also equivalent to that used by both the + * IA5 and the IS-91 encodings. For the former this is defined + * using CCITT Rec. T.50 Tables 1 and 3. For the latter IS 637 B, + * Table 4.3.1.4.1-1 -- and note the encoding uses only 6 bits, + * and hence only maps entries up to the '_' character. + * + */ + public static final char[] ASCII_MAP = { + ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', + '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~'}; + + /** + * Character to use when forced to encode otherwise unencodable + * characters, meaning those not in the respective ASCII or GSM + * 7-bit encoding tables. Current choice is SPACE, which is 0x20 + * in both the GSM-7bit and ASCII-7bit encodings. + */ + static final byte UNENCODABLE_7_BIT_CHAR = 0x20; + + /** + * Only elements between these indices in the ASCII table are printable. + */ + public static final int PRINTABLE_ASCII_MIN_INDEX = 0x20; + public static final int ASCII_NL_INDEX = 0x0A; + public static final int ASCII_CR_INDEX = 0x0D; + public static final SparseIntArray charToAscii = new SparseIntArray(); + static { + for (int i = 0; i < ASCII_MAP.length; i++) { + charToAscii.put(ASCII_MAP[i], PRINTABLE_ASCII_MIN_INDEX + i); + } + charToAscii.put('\n', ASCII_NL_INDEX); + charToAscii.put('\r', ASCII_CR_INDEX); + } + + /* + * TODO(cleanup): Move this very generic functionality somewhere + * more general. + */ + /** + * Given a string generate a corresponding ASCII-encoded byte + * array, but limited to printable characters. If the input + * contains unprintable characters, return null. + */ + public static byte[] stringToAscii(String str) { + int len = str.length(); + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) { + int charCode = charToAscii.get(str.charAt(i), -1); + if (charCode == -1) return null; + result[i] = (byte)charCode; + } + return result; + } + + /** + * Mapping for ASCII values less than 32 are flow control signals + * and not used here. + */ + public static final int ASCII_MAP_BASE_INDEX = 0x20; + public static final int ASCII_MAP_MAX_INDEX = ASCII_MAP_BASE_INDEX + ASCII_MAP.length - 1; + + /** + * Contains the data header of the user data + */ + public SmsHeader userDataHeader; + + /** + * Contains the data encoding type for the SMS message + */ + public int msgEncoding; + public boolean msgEncodingSet = false; + + public int msgType; + + /** + * Number of invalid bits in the last byte of data. + */ + public int paddingBits; + + public int numFields; + + /** + * Contains the user data of a SMS message + * (See 3GPP2 C.S0015-B, v2, 4.5.2) + */ + public byte[] payload; + public String payloadStr; + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("UserData "); + builder.append("{ msgEncoding=" + (msgEncodingSet ? msgEncoding : "unset")); + builder.append(", msgType=" + msgType); + builder.append(", paddingBits=" + paddingBits); + builder.append(", numFields=" + numFields); + builder.append(", userDataHeader=" + userDataHeader); + builder.append(", payload='" + HexDump.toHexString(payload) + "'"); + builder.append(", payloadStr='" + payloadStr + "'"); + builder.append(" }"); + return builder.toString(); + } + +} diff --git a/src/java/com/android/internal/telephony/cdma/sms/package.html b/src/java/com/android/internal/telephony/cdma/sms/package.html new file mode 100644 index 0000000..b2bc736 --- /dev/null +++ b/src/java/com/android/internal/telephony/cdma/sms/package.html @@ -0,0 +1,6 @@ +<HTML> +<BODY> +Provides CDMA-specific features for text/data/PDU SMS messages +@hide +</BODY> +</HTML> diff --git a/src/java/com/android/internal/telephony/gsm/CallFailCause.java b/src/java/com/android/internal/telephony/gsm/CallFailCause.java new file mode 100644 index 0000000..af2ad48 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/CallFailCause.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +/** + * Call fail causes from TS 24.008 . + * These are mostly the cause codes we need to distinguish for the UI. + * See 22.001 Annex F.4 for mapping of cause codes to local tones. + * + * {@hide} + * + */ +public interface CallFailCause { + // Unassigned/Unobtainable number + static final int UNOBTAINABLE_NUMBER = 1; + + static final int NORMAL_CLEARING = 16; + // Busy Tone + static final int USER_BUSY = 17; + + // No Tone + static final int NUMBER_CHANGED = 22; + static final int STATUS_ENQUIRY = 30; + static final int NORMAL_UNSPECIFIED = 31; + + // Congestion Tone + static final int NO_CIRCUIT_AVAIL = 34; + static final int TEMPORARY_FAILURE = 41; + static final int SWITCHING_CONGESTION = 42; + static final int CHANNEL_NOT_AVAIL = 44; + static final int QOS_NOT_AVAIL = 49; + static final int BEARER_NOT_AVAIL = 58; + + // others + static final int ACM_LIMIT_EXCEEDED = 68; + static final int CALL_BARRED = 240; + static final int FDN_BLOCKED = 241; + static final int ERROR_UNSPECIFIED = 0xffff; +} diff --git a/src/java/com/android/internal/telephony/gsm/GSMPhone.java b/src/java/com/android/internal/telephony/gsm/GSMPhone.java new file mode 100644 index 0000000..8c5368e --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GSMPhone.java @@ -0,0 +1,1508 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.SQLException; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemProperties; +import android.preference.PreferenceManager; +import android.provider.Telephony; +import android.telephony.CellLocation; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import com.android.internal.telephony.CallTracker; +import android.text.TextUtils; +import android.util.Log; + +import static com.android.internal.telephony.CommandsInterface.CF_ACTION_DISABLE; +import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ENABLE; +import static com.android.internal.telephony.CommandsInterface.CF_ACTION_ERASURE; +import static com.android.internal.telephony.CommandsInterface.CF_ACTION_REGISTRATION; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_ALL_CONDITIONAL; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_NO_REPLY; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_NOT_REACHABLE; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_BUSY; +import static com.android.internal.telephony.CommandsInterface.CF_REASON_UNCONDITIONAL; +import static com.android.internal.telephony.CommandsInterface.SERVICE_CLASS_VOICE; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_BASEBAND_VERSION; + +import com.android.internal.telephony.cat.CatService; +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallForwardInfo; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccPhoneBookInterfaceManager; +import com.android.internal.telephony.IccSmsInterfaceManager; +import com.android.internal.telephony.MmiCode; +import com.android.internal.telephony.OperatorInfo; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneNotifier; +import com.android.internal.telephony.PhoneProxy; +import com.android.internal.telephony.PhoneSubInfo; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.UUSInfo; +import com.android.internal.telephony.test.SimulatedRadioControl; +import com.android.internal.telephony.uicc.UiccController; +import com.android.internal.telephony.IccVmNotSupportedException; +import com.android.internal.telephony.ServiceStateTracker; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +/** + * {@hide} + */ +public class GSMPhone extends PhoneBase { + // NOTE that LOG_TAG here is "GSM", which means that log messages + // from this file will go into the radio log rather than the main + // log. (Use "adb logcat -b radio" to see them.) + static final String LOG_TAG = "GSM"; + private static final boolean LOCAL_DEBUG = true; + private static final boolean VDBG = false; /* STOP SHIP if true */ + + // Key used to read/write current ciphering state + public static final String CIPHERING_KEY = "ciphering_key"; + // Key used to read/write voice mail number + public static final String VM_NUMBER = "vm_number_key"; + // Key used to read/write the SIM IMSI used for storing the voice mail + public static final String VM_SIM_IMSI = "vm_sim_imsi_key"; + + // Instance Variables + GsmCallTracker mCT; + GsmServiceStateTracker mSST; + ArrayList <GsmMmiCode> mPendingMMIs = new ArrayList<GsmMmiCode>(); + SimPhoneBookInterfaceManager mSimPhoneBookIntManager; + SimSmsInterfaceManager mSimSmsIntManager; + PhoneSubInfo mSubInfo; + + + Registrant mPostDialHandler; + + /** List of Registrants to receive Supplementary Service Notifications. */ + RegistrantList mSsnRegistrants = new RegistrantList(); + + Thread debugPortThread; + ServerSocket debugSocket; + + private String mImei; + private String mImeiSv; + private String mVmNumber; + + + // Constructors + + public + GSMPhone (Context context, CommandsInterface ci, PhoneNotifier notifier) { + this(context,ci,notifier, false); + } + + public + GSMPhone (Context context, CommandsInterface ci, PhoneNotifier notifier, boolean unitTestMode) { + super(notifier, context, ci, unitTestMode); + + if (ci instanceof SimulatedRadioControl) { + mSimulatedRadioControl = (SimulatedRadioControl) ci; + } + + mCM.setPhoneType(PhoneConstants.PHONE_TYPE_GSM); + mIccCard.set(UiccController.getInstance(this).getIccCard()); + mIccRecords = mIccCard.get().getIccRecords(); + mCT = new GsmCallTracker(this); + mSST = new GsmServiceStateTracker (this); + mSMS = new GsmSMSDispatcher(this, mSmsStorageMonitor, mSmsUsageMonitor); + mDataConnectionTracker = new GsmDataConnectionTracker (this); + if (!unitTestMode) { + mSimPhoneBookIntManager = new SimPhoneBookInterfaceManager(this); + mSimSmsIntManager = new SimSmsInterfaceManager(this, mSMS); + mSubInfo = new PhoneSubInfo(this); + } + + mCM.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null); + registerForSimRecordEvents(); + mCM.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + mCM.registerForOn(this, EVENT_RADIO_ON, null); + mCM.setOnUSSD(this, EVENT_USSD, null); + mCM.setOnSuppServiceNotification(this, EVENT_SSN, null); + mSST.registerForNetworkAttached(this, EVENT_REGISTERED_TO_NETWORK, null); + + if (false) { + try { + //debugSocket = new LocalServerSocket("com.android.internal.telephony.debug"); + debugSocket = new ServerSocket(); + debugSocket.setReuseAddress(true); + debugSocket.bind (new InetSocketAddress("127.0.0.1", 6666)); + + debugPortThread + = new Thread( + new Runnable() { + public void run() { + for(;;) { + try { + Socket sock; + sock = debugSocket.accept(); + Log.i(LOG_TAG, "New connection; resetting radio"); + mCM.resetRadio(null); + sock.close(); + } catch (IOException ex) { + Log.w(LOG_TAG, + "Exception accepting socket", ex); + } + } + } + }, + "GSMPhone debug"); + + debugPortThread.start(); + + } catch (IOException ex) { + Log.w(LOG_TAG, "Failure to open com.android.internal.telephony.debug socket", ex); + } + } + + //Change the system property + SystemProperties.set(TelephonyProperties.CURRENT_ACTIVE_PHONE, + new Integer(PhoneConstants.PHONE_TYPE_GSM).toString()); + } + + @Override + public void dispose() { + synchronized(PhoneProxy.lockForRadioTechnologyChange) { + super.dispose(); + + //Unregister from all former registered events + mCM.unregisterForAvailable(this); //EVENT_RADIO_AVAILABLE + unregisterForSimRecordEvents(); + mCM.unregisterForOffOrNotAvailable(this); //EVENT_RADIO_OFF_OR_NOT_AVAILABLE + mCM.unregisterForOn(this); //EVENT_RADIO_ON + mSST.unregisterForNetworkAttached(this); //EVENT_REGISTERED_TO_NETWORK + mCM.unSetOnUSSD(this); + mCM.unSetOnSuppServiceNotification(this); + + mPendingMMIs.clear(); + + //Force all referenced classes to unregister their former registered events + mCT.dispose(); + mDataConnectionTracker.dispose(); + mSST.dispose(); + mSimPhoneBookIntManager.dispose(); + mSimSmsIntManager.dispose(); + mSubInfo.dispose(); + } + } + + @Override + public void removeReferences() { + Log.d(LOG_TAG, "removeReferences"); + mSimulatedRadioControl = null; + mSimPhoneBookIntManager = null; + mSimSmsIntManager = null; + mSubInfo = null; + mCT = null; + mSST = null; + super.removeReferences(); + } + + protected void finalize() { + if(LOCAL_DEBUG) Log.d(LOG_TAG, "GSMPhone finalized"); + } + + + public ServiceState + getServiceState() { + return mSST.ss; + } + + public CellLocation getCellLocation() { + return mSST.cellLoc; + } + + public PhoneConstants.State getState() { + return mCT.state; + } + + public String getPhoneName() { + return "GSM"; + } + + public int getPhoneType() { + return PhoneConstants.PHONE_TYPE_GSM; + } + + public SignalStrength getSignalStrength() { + return mSST.mSignalStrength; + } + + public CallTracker getCallTracker() { + return mCT; + } + + public ServiceStateTracker getServiceStateTracker() { + return mSST; + } + + public List<? extends MmiCode> + getPendingMmiCodes() { + return mPendingMMIs; + } + + public PhoneConstants.DataState getDataConnectionState(String apnType) { + PhoneConstants.DataState ret = PhoneConstants.DataState.DISCONNECTED; + + if (mSST == null) { + // Radio Technology Change is ongoning, dispose() and removeReferences() have + // already been called + + ret = PhoneConstants.DataState.DISCONNECTED; + } else if (mSST.getCurrentGprsState() + != ServiceState.STATE_IN_SERVICE) { + // If we're out of service, open TCP sockets may still work + // but no data will flow + ret = PhoneConstants.DataState.DISCONNECTED; + } else if (mDataConnectionTracker.isApnTypeEnabled(apnType) == false || + mDataConnectionTracker.isApnTypeActive(apnType) == false) { + //TODO: isApnTypeActive() is just checking whether ApnContext holds + // Dataconnection or not. Checking each ApnState below should + // provide the same state. Calling isApnTypeActive() can be removed. + ret = PhoneConstants.DataState.DISCONNECTED; + } else { /* mSST.gprsState == ServiceState.STATE_IN_SERVICE */ + switch (mDataConnectionTracker.getState(apnType)) { + case FAILED: + case IDLE: + ret = PhoneConstants.DataState.DISCONNECTED; + break; + + case CONNECTED: + case DISCONNECTING: + if ( mCT.state != PhoneConstants.State.IDLE + && !mSST.isConcurrentVoiceAndDataAllowed()) { + ret = PhoneConstants.DataState.SUSPENDED; + } else { + ret = PhoneConstants.DataState.CONNECTED; + } + break; + + case INITING: + case CONNECTING: + case SCANNING: + ret = PhoneConstants.DataState.CONNECTING; + break; + } + } + + return ret; + } + + public DataActivityState getDataActivityState() { + DataActivityState ret = DataActivityState.NONE; + + if (mSST.getCurrentGprsState() == ServiceState.STATE_IN_SERVICE) { + switch (mDataConnectionTracker.getActivity()) { + case DATAIN: + ret = DataActivityState.DATAIN; + break; + + case DATAOUT: + ret = DataActivityState.DATAOUT; + break; + + case DATAINANDOUT: + ret = DataActivityState.DATAINANDOUT; + break; + } + } + + return ret; + } + + /** + * Notify any interested party of a Phone state change {@link PhoneConstants.State} + */ + /*package*/ void notifyPhoneStateChanged() { + mNotifier.notifyPhoneState(this); + } + + /** + * Notify registrants of a change in the call state. This notifies changes in {@link Call.State} + * Use this when changes in the precise call state are needed, else use notifyPhoneStateChanged. + */ + /*package*/ void notifyPreciseCallStateChanged() { + /* we'd love it if this was package-scoped*/ + super.notifyPreciseCallStateChangedP(); + } + + /*package*/ void + notifyNewRingingConnection(Connection c) { + /* we'd love it if this was package-scoped*/ + super.notifyNewRingingConnectionP(c); + } + + /*package*/ void + notifyDisconnect(Connection cn) { + mDisconnectRegistrants.notifyResult(cn); + } + + void notifyUnknownConnection() { + mUnknownConnectionRegistrants.notifyResult(this); + } + + void notifySuppServiceFailed(SuppService code) { + mSuppServiceFailedRegistrants.notifyResult(code); + } + + /*package*/ void + notifyServiceStateChanged(ServiceState ss) { + super.notifyServiceStateChangedP(ss); + } + + /*package*/ + void notifyLocationChanged() { + mNotifier.notifyCellLocation(this); + } + + /*package*/ void + notifySignalStrength() { + mNotifier.notifySignalStrength(this); + } + + public void + notifyCallForwardingIndicator() { + mNotifier.notifyCallForwardingChanged(this); + } + + // override for allowing access from other classes of this package + /** + * {@inheritDoc} + */ + public final void + setSystemProperty(String property, String value) { + super.setSystemProperty(property, value); + } + + public void registerForSuppServiceNotification( + Handler h, int what, Object obj) { + mSsnRegistrants.addUnique(h, what, obj); + if (mSsnRegistrants.size() == 1) mCM.setSuppServiceNotifications(true, null); + } + + public void unregisterForSuppServiceNotification(Handler h) { + mSsnRegistrants.remove(h); + if (mSsnRegistrants.size() == 0) mCM.setSuppServiceNotifications(false, null); + } + + public void + acceptCall() throws CallStateException { + mCT.acceptCall(); + } + + public void + rejectCall() throws CallStateException { + mCT.rejectCall(); + } + + public void + switchHoldingAndActive() throws CallStateException { + mCT.switchWaitingOrHoldingAndActive(); + } + + public boolean canConference() { + return mCT.canConference(); + } + + public boolean canDial() { + return mCT.canDial(); + } + + public void conference() throws CallStateException { + mCT.conference(); + } + + public void clearDisconnected() { + mCT.clearDisconnected(); + } + + public boolean canTransfer() { + return mCT.canTransfer(); + } + + public void explicitCallTransfer() throws CallStateException { + mCT.explicitCallTransfer(); + } + + public GsmCall + getForegroundCall() { + return mCT.foregroundCall; + } + + public GsmCall + getBackgroundCall() { + return mCT.backgroundCall; + } + + public GsmCall + getRingingCall() { + return mCT.ringingCall; + } + + private boolean handleCallDeflectionIncallSupplementaryService( + String dialString) throws CallStateException { + if (dialString.length() > 1) { + return false; + } + + if (getRingingCall().getState() != GsmCall.State.IDLE) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "MmiCode 0: rejectCall"); + try { + mCT.rejectCall(); + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "reject failed", e); + notifySuppServiceFailed(Phone.SuppService.REJECT); + } + } else if (getBackgroundCall().getState() != GsmCall.State.IDLE) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 0: hangupWaitingOrBackground"); + mCT.hangupWaitingOrBackground(); + } + + return true; + } + + private boolean handleCallWaitingIncallSupplementaryService( + String dialString) throws CallStateException { + int len = dialString.length(); + + if (len > 2) { + return false; + } + + GsmCall call = (GsmCall) getForegroundCall(); + + try { + if (len > 1) { + char ch = dialString.charAt(1); + int callIndex = ch - '0'; + + if (callIndex >= 1 && callIndex <= GsmCallTracker.MAX_CONNECTIONS) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 1: hangupConnectionByIndex " + + callIndex); + mCT.hangupConnectionByIndex(call, callIndex); + } + } else { + if (call.getState() != GsmCall.State.IDLE) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 1: hangup foreground"); + //mCT.hangupForegroundResumeBackground(); + mCT.hangup(call); + } else { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 1: switchWaitingOrHoldingAndActive"); + mCT.switchWaitingOrHoldingAndActive(); + } + } + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "hangup failed", e); + notifySuppServiceFailed(Phone.SuppService.HANGUP); + } + + return true; + } + + private boolean handleCallHoldIncallSupplementaryService(String dialString) + throws CallStateException { + int len = dialString.length(); + + if (len > 2) { + return false; + } + + GsmCall call = (GsmCall) getForegroundCall(); + + if (len > 1) { + try { + char ch = dialString.charAt(1); + int callIndex = ch - '0'; + GsmConnection conn = mCT.getConnectionByIndex(call, callIndex); + + // gsm index starts at 1, up to 5 connections in a call, + if (conn != null && callIndex >= 1 && callIndex <= GsmCallTracker.MAX_CONNECTIONS) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "MmiCode 2: separate call "+ + callIndex); + mCT.separate(conn); + } else { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "separate: invalid call index "+ + callIndex); + notifySuppServiceFailed(Phone.SuppService.SEPARATE); + } + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "separate failed", e); + notifySuppServiceFailed(Phone.SuppService.SEPARATE); + } + } else { + try { + if (getRingingCall().getState() != GsmCall.State.IDLE) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 2: accept ringing call"); + mCT.acceptCall(); + } else { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "MmiCode 2: switchWaitingOrHoldingAndActive"); + mCT.switchWaitingOrHoldingAndActive(); + } + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "switch failed", e); + notifySuppServiceFailed(Phone.SuppService.SWITCH); + } + } + + return true; + } + + private boolean handleMultipartyIncallSupplementaryService( + String dialString) throws CallStateException { + if (dialString.length() > 1) { + return false; + } + + if (LOCAL_DEBUG) Log.d(LOG_TAG, "MmiCode 3: merge calls"); + try { + conference(); + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "conference failed", e); + notifySuppServiceFailed(Phone.SuppService.CONFERENCE); + } + return true; + } + + private boolean handleEctIncallSupplementaryService(String dialString) + throws CallStateException { + + int len = dialString.length(); + + if (len != 1) { + return false; + } + + if (LOCAL_DEBUG) Log.d(LOG_TAG, "MmiCode 4: explicit call transfer"); + try { + explicitCallTransfer(); + } catch (CallStateException e) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "transfer failed", e); + notifySuppServiceFailed(Phone.SuppService.TRANSFER); + } + return true; + } + + private boolean handleCcbsIncallSupplementaryService(String dialString) + throws CallStateException { + if (dialString.length() > 1) { + return false; + } + + Log.i(LOG_TAG, "MmiCode 5: CCBS not supported!"); + // Treat it as an "unknown" service. + notifySuppServiceFailed(Phone.SuppService.UNKNOWN); + return true; + } + + public boolean handleInCallMmiCommands(String dialString) + throws CallStateException { + if (!isInCall()) { + return false; + } + + if (TextUtils.isEmpty(dialString)) { + return false; + } + + boolean result = false; + char ch = dialString.charAt(0); + switch (ch) { + case '0': + result = handleCallDeflectionIncallSupplementaryService( + dialString); + break; + case '1': + result = handleCallWaitingIncallSupplementaryService( + dialString); + break; + case '2': + result = handleCallHoldIncallSupplementaryService(dialString); + break; + case '3': + result = handleMultipartyIncallSupplementaryService(dialString); + break; + case '4': + result = handleEctIncallSupplementaryService(dialString); + break; + case '5': + result = handleCcbsIncallSupplementaryService(dialString); + break; + default: + break; + } + + return result; + } + + boolean isInCall() { + GsmCall.State foregroundCallState = getForegroundCall().getState(); + GsmCall.State backgroundCallState = getBackgroundCall().getState(); + GsmCall.State ringingCallState = getRingingCall().getState(); + + return (foregroundCallState.isAlive() || + backgroundCallState.isAlive() || + ringingCallState.isAlive()); + } + + public Connection + dial(String dialString) throws CallStateException { + return dial(dialString, null); + } + + public Connection + dial (String dialString, UUSInfo uusInfo) throws CallStateException { + // Need to make sure dialString gets parsed properly + String newDialString = PhoneNumberUtils.stripSeparators(dialString); + + // handle in-call MMI first if applicable + if (handleInCallMmiCommands(newDialString)) { + return null; + } + + // Only look at the Network portion for mmi + String networkPortion = PhoneNumberUtils.extractNetworkPortionAlt(newDialString); + GsmMmiCode mmi = GsmMmiCode.newFromDialString(networkPortion, this); + if (LOCAL_DEBUG) Log.d(LOG_TAG, + "dialing w/ mmi '" + mmi + "'..."); + + if (mmi == null) { + return mCT.dial(newDialString, uusInfo); + } else if (mmi.isTemporaryModeCLIR()) { + return mCT.dial(mmi.dialingNumber, mmi.getCLIRMode(), uusInfo); + } else { + mPendingMMIs.add(mmi); + mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null)); + mmi.processCode(); + + // FIXME should this return null or something else? + return null; + } + } + + public boolean handlePinMmi(String dialString) { + GsmMmiCode mmi = GsmMmiCode.newFromDialString(dialString, this); + + if (mmi != null && mmi.isPinCommand()) { + mPendingMMIs.add(mmi); + mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null)); + mmi.processCode(); + return true; + } + + return false; + } + + public void sendUssdResponse(String ussdMessge) { + GsmMmiCode mmi = GsmMmiCode.newFromUssdUserInput(ussdMessge, this); + mPendingMMIs.add(mmi); + mMmiRegistrants.notifyRegistrants(new AsyncResult(null, mmi, null)); + mmi.sendUssd(ussdMessge); + } + + public void + sendDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "sendDtmf called with invalid character '" + c + "'"); + } else { + if (mCT.state == PhoneConstants.State.OFFHOOK) { + mCM.sendDtmf(c, null); + } + } + } + + public void + startDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "startDtmf called with invalid character '" + c + "'"); + } else { + mCM.startDtmf(c, null); + } + } + + public void + stopDtmf() { + mCM.stopDtmf(null); + } + + public void + sendBurstDtmf(String dtmfString) { + Log.e(LOG_TAG, "[GSMPhone] sendBurstDtmf() is a CDMA method"); + } + + public void + setRadioPower(boolean power) { + mSST.setRadioPower(power); + } + + private void storeVoiceMailNumber(String number) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(VM_NUMBER, number); + editor.apply(); + setVmSimImsi(getSubscriberId()); + } + + public String getVoiceMailNumber() { + // Read from the SIM. If its null, try reading from the shared preference area. + String number = mIccRecords.getVoiceMailNumber(); + if (TextUtils.isEmpty(number)) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + number = sp.getString(VM_NUMBER, null); + } + return number; + } + + private String getVmSimImsi() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + return sp.getString(VM_SIM_IMSI, null); + } + + private void setVmSimImsi(String imsi) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(VM_SIM_IMSI, imsi); + editor.apply(); + } + + public String getVoiceMailAlphaTag() { + String ret; + + ret = mIccRecords.getVoiceMailAlphaTag(); + + if (ret == null || ret.length() == 0) { + return mContext.getText( + com.android.internal.R.string.defaultVoiceMailAlphaTag).toString(); + } + + return ret; + } + + public String getDeviceId() { + return mImei; + } + + public String getDeviceSvn() { + return mImeiSv; + } + + public String getImei() { + return mImei; + } + + public String getEsn() { + Log.e(LOG_TAG, "[GSMPhone] getEsn() is a CDMA method"); + return "0"; + } + + public String getMeid() { + Log.e(LOG_TAG, "[GSMPhone] getMeid() is a CDMA method"); + return "0"; + } + + public String getSubscriberId() { + return mIccRecords.getIMSI(); + } + + public String getLine1Number() { + return mIccRecords.getMsisdnNumber(); + } + + @Override + public String getMsisdn() { + return mIccRecords.getMsisdnNumber(); + } + + public String getLine1AlphaTag() { + return mIccRecords.getMsisdnAlphaTag(); + } + + public void setLine1Number(String alphaTag, String number, Message onComplete) { + mIccRecords.setMsisdnNumber(alphaTag, number, onComplete); + } + + public void setVoiceMailNumber(String alphaTag, + String voiceMailNumber, + Message onComplete) { + + Message resp; + mVmNumber = voiceMailNumber; + resp = obtainMessage(EVENT_SET_VM_NUMBER_DONE, 0, 0, onComplete); + mIccRecords.setVoiceMailNumber(alphaTag, mVmNumber, resp); + } + + private boolean isValidCommandInterfaceCFReason (int commandInterfaceCFReason) { + switch (commandInterfaceCFReason) { + case CF_REASON_UNCONDITIONAL: + case CF_REASON_BUSY: + case CF_REASON_NO_REPLY: + case CF_REASON_NOT_REACHABLE: + case CF_REASON_ALL: + case CF_REASON_ALL_CONDITIONAL: + return true; + default: + return false; + } + } + + private boolean isValidCommandInterfaceCFAction (int commandInterfaceCFAction) { + switch (commandInterfaceCFAction) { + case CF_ACTION_DISABLE: + case CF_ACTION_ENABLE: + case CF_ACTION_REGISTRATION: + case CF_ACTION_ERASURE: + return true; + default: + return false; + } + } + + protected boolean isCfEnable(int action) { + return (action == CF_ACTION_ENABLE) || (action == CF_ACTION_REGISTRATION); + } + + public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) { + if (isValidCommandInterfaceCFReason(commandInterfaceCFReason)) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "requesting call forwarding query."); + Message resp; + if (commandInterfaceCFReason == CF_REASON_UNCONDITIONAL) { + resp = obtainMessage(EVENT_GET_CALL_FORWARD_DONE, onComplete); + } else { + resp = onComplete; + } + mCM.queryCallForwardStatus(commandInterfaceCFReason,0,null,resp); + } + } + + public void setCallForwardingOption(int commandInterfaceCFAction, + int commandInterfaceCFReason, + String dialingNumber, + int timerSeconds, + Message onComplete) { + if ( (isValidCommandInterfaceCFAction(commandInterfaceCFAction)) && + (isValidCommandInterfaceCFReason(commandInterfaceCFReason))) { + + Message resp; + if (commandInterfaceCFReason == CF_REASON_UNCONDITIONAL) { + resp = obtainMessage(EVENT_SET_CALL_FORWARD_DONE, + isCfEnable(commandInterfaceCFAction) ? 1 : 0, 0, onComplete); + } else { + resp = onComplete; + } + mCM.setCallForward(commandInterfaceCFAction, + commandInterfaceCFReason, + CommandsInterface.SERVICE_CLASS_VOICE, + dialingNumber, + timerSeconds, + resp); + } + } + + public void getOutgoingCallerIdDisplay(Message onComplete) { + mCM.getCLIR(onComplete); + } + + public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, + Message onComplete) { + mCM.setCLIR(commandInterfaceCLIRMode, + obtainMessage(EVENT_SET_CLIR_COMPLETE, commandInterfaceCLIRMode, 0, onComplete)); + } + + public void getCallWaiting(Message onComplete) { + //As per 3GPP TS 24.083, section 1.6 UE doesn't need to send service + //class parameter in call waiting interrogation to network + mCM.queryCallWaiting(CommandsInterface.SERVICE_CLASS_NONE, onComplete); + } + + public void setCallWaiting(boolean enable, Message onComplete) { + mCM.setCallWaiting(enable, CommandsInterface.SERVICE_CLASS_VOICE, onComplete); + } + + public void + getAvailableNetworks(Message response) { + mCM.getAvailableNetworks(response); + } + + /** + * Small container class used to hold information relevant to + * the carrier selection process. operatorNumeric can be "" + * if we are looking for automatic selection. operatorAlphaLong is the + * corresponding operator name. + */ + private static class NetworkSelectMessage { + public Message message; + public String operatorNumeric; + public String operatorAlphaLong; + } + + public void + setNetworkSelectionModeAutomatic(Message response) { + // wrap the response message in our own message along with + // an empty string (to indicate automatic selection) for the + // operator's id. + NetworkSelectMessage nsm = new NetworkSelectMessage(); + nsm.message = response; + nsm.operatorNumeric = ""; + nsm.operatorAlphaLong = ""; + + // get the message + Message msg = obtainMessage(EVENT_SET_NETWORK_AUTOMATIC_COMPLETE, nsm); + if (LOCAL_DEBUG) + Log.d(LOG_TAG, "wrapping and sending message to connect automatically"); + + mCM.setNetworkSelectionModeAutomatic(msg); + } + + public void + selectNetworkManually(OperatorInfo network, + Message response) { + // wrap the response message in our own message along with + // the operator's id. + NetworkSelectMessage nsm = new NetworkSelectMessage(); + nsm.message = response; + nsm.operatorNumeric = network.getOperatorNumeric(); + nsm.operatorAlphaLong = network.getOperatorAlphaLong(); + + // get the message + Message msg = obtainMessage(EVENT_SET_NETWORK_MANUAL_COMPLETE, nsm); + + mCM.setNetworkSelectionModeManual(network.getOperatorNumeric(), msg); + } + + public void + getNeighboringCids(Message response) { + mCM.getNeighboringCids(response); + } + + public void setOnPostDialCharacter(Handler h, int what, Object obj) { + mPostDialHandler = new Registrant(h, what, obj); + } + + public void setMute(boolean muted) { + mCT.setMute(muted); + } + + public boolean getMute() { + return mCT.getMute(); + } + + public void getDataCallList(Message response) { + mCM.getDataCallList(response); + } + + public void updateServiceLocation() { + mSST.enableSingleLocationUpdate(); + } + + public void enableLocationUpdates() { + mSST.enableLocationUpdates(); + } + + public void disableLocationUpdates() { + mSST.disableLocationUpdates(); + } + + public boolean getDataRoamingEnabled() { + return mDataConnectionTracker.getDataOnRoamingEnabled(); + } + + public void setDataRoamingEnabled(boolean enable) { + mDataConnectionTracker.setDataOnRoamingEnabled(enable); + } + + /** + * Removes the given MMI from the pending list and notifies + * registrants that it is complete. + * @param mmi MMI that is done + */ + /*package*/ void + onMMIDone(GsmMmiCode mmi) { + /* Only notify complete if it's on the pending list. + * Otherwise, it's already been handled (eg, previously canceled). + * The exception is cancellation of an incoming USSD-REQUEST, which is + * not on the list. + */ + if (mPendingMMIs.remove(mmi) || mmi.isUssdRequest()) { + mMmiCompleteRegistrants.notifyRegistrants( + new AsyncResult(null, mmi, null)); + } + } + + + private void + onNetworkInitiatedUssd(GsmMmiCode mmi) { + mMmiCompleteRegistrants.notifyRegistrants( + new AsyncResult(null, mmi, null)); + } + + + /** ussdMode is one of CommandsInterface.USSD_MODE_* */ + private void + onIncomingUSSD (int ussdMode, String ussdMessage) { + boolean isUssdError; + boolean isUssdRequest; + + isUssdRequest + = (ussdMode == CommandsInterface.USSD_MODE_REQUEST); + + isUssdError + = (ussdMode != CommandsInterface.USSD_MODE_NOTIFY + && ussdMode != CommandsInterface.USSD_MODE_REQUEST); + + // See comments in GsmMmiCode.java + // USSD requests aren't finished until one + // of these two events happen + GsmMmiCode found = null; + for (int i = 0, s = mPendingMMIs.size() ; i < s; i++) { + if(mPendingMMIs.get(i).isPendingUSSD()) { + found = mPendingMMIs.get(i); + break; + } + } + + if (found != null) { + // Complete pending USSD + + if (isUssdError) { + found.onUssdFinishedError(); + } else { + found.onUssdFinished(ussdMessage, isUssdRequest); + } + } else { // pending USSD not found + // The network may initiate its own USSD request + + // ignore everything that isnt a Notify or a Request + // also, discard if there is no message to present + if (!isUssdError && ussdMessage != null) { + GsmMmiCode mmi; + mmi = GsmMmiCode.newNetworkInitiatedUssd(ussdMessage, + isUssdRequest, + GSMPhone.this); + onNetworkInitiatedUssd(mmi); + } + } + } + + /** + * Make sure the network knows our preferred setting. + */ + protected void syncClirSetting() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + int clirSetting = sp.getInt(CLIR_KEY, -1); + if (clirSetting >= 0) { + mCM.setCLIR(clirSetting, null); + } + } + + @Override + public void handleMessage (Message msg) { + AsyncResult ar; + Message onComplete; + + switch (msg.what) { + case EVENT_RADIO_AVAILABLE: { + mCM.getBasebandVersion( + obtainMessage(EVENT_GET_BASEBAND_VERSION_DONE)); + + mCM.getIMEI(obtainMessage(EVENT_GET_IMEI_DONE)); + mCM.getIMEISV(obtainMessage(EVENT_GET_IMEISV_DONE)); + } + break; + + case EVENT_RADIO_ON: + break; + + case EVENT_REGISTERED_TO_NETWORK: + syncClirSetting(); + break; + + case EVENT_SIM_RECORDS_LOADED: + updateCurrentCarrierInProvider(); + + // Check if this is a different SIM than the previous one. If so unset the + // voice mail number. + String imsi = getVmSimImsi(); + String imsiFromSIM = getSubscriberId(); + if (imsi != null && imsiFromSIM != null && !imsiFromSIM.equals(imsi)) { + storeVoiceMailNumber(null); + setVmSimImsi(null); + } + + break; + + case EVENT_GET_BASEBAND_VERSION_DONE: + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + + if (LOCAL_DEBUG) Log.d(LOG_TAG, "Baseband version: " + ar.result); + setSystemProperty(PROPERTY_BASEBAND_VERSION, (String)ar.result); + break; + + case EVENT_GET_IMEI_DONE: + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + + mImei = (String)ar.result; + break; + + case EVENT_GET_IMEISV_DONE: + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + + mImeiSv = (String)ar.result; + break; + + case EVENT_USSD: + ar = (AsyncResult)msg.obj; + + String[] ussdResult = (String[]) ar.result; + + if (ussdResult.length > 1) { + try { + onIncomingUSSD(Integer.parseInt(ussdResult[0]), ussdResult[1]); + } catch (NumberFormatException e) { + Log.w(LOG_TAG, "error parsing USSD"); + } + } + break; + + case EVENT_RADIO_OFF_OR_NOT_AVAILABLE: + // Some MMI requests (eg USSD) are not completed + // within the course of a CommandsInterface request + // If the radio shuts off or resets while one of these + // is pending, we need to clean up. + + for (int i = mPendingMMIs.size() - 1; i >= 0; i--) { + if (mPendingMMIs.get(i).isPendingUSSD()) { + mPendingMMIs.get(i).onUssdFinishedError(); + } + } + break; + + case EVENT_SSN: + ar = (AsyncResult)msg.obj; + SuppServiceNotification not = (SuppServiceNotification) ar.result; + mSsnRegistrants.notifyRegistrants(ar); + break; + + case EVENT_SET_CALL_FORWARD_DONE: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + mIccRecords.setVoiceCallForwardingFlag(1, msg.arg1 == 1); + } + onComplete = (Message) ar.userObj; + if (onComplete != null) { + AsyncResult.forMessage(onComplete, ar.result, ar.exception); + onComplete.sendToTarget(); + } + break; + + case EVENT_SET_VM_NUMBER_DONE: + ar = (AsyncResult)msg.obj; + if (IccVmNotSupportedException.class.isInstance(ar.exception)) { + storeVoiceMailNumber(mVmNumber); + ar.exception = null; + } + onComplete = (Message) ar.userObj; + if (onComplete != null) { + AsyncResult.forMessage(onComplete, ar.result, ar.exception); + onComplete.sendToTarget(); + } + break; + + + case EVENT_GET_CALL_FORWARD_DONE: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + handleCfuQueryResult((CallForwardInfo[])ar.result); + } + onComplete = (Message) ar.userObj; + if (onComplete != null) { + AsyncResult.forMessage(onComplete, ar.result, ar.exception); + onComplete.sendToTarget(); + } + break; + + case EVENT_NEW_ICC_SMS: + ar = (AsyncResult)msg.obj; + mSMS.dispatchMessage((SmsMessage)ar.result); + break; + + case EVENT_SET_NETWORK_AUTOMATIC: + ar = (AsyncResult)msg.obj; + setNetworkSelectionModeAutomatic((Message)ar.result); + break; + + case EVENT_ICC_RECORD_EVENTS: + ar = (AsyncResult)msg.obj; + processIccRecordEvents((Integer)ar.result); + break; + + // handle the select network completion callbacks. + case EVENT_SET_NETWORK_MANUAL_COMPLETE: + case EVENT_SET_NETWORK_AUTOMATIC_COMPLETE: + handleSetSelectNetwork((AsyncResult) msg.obj); + break; + + case EVENT_SET_CLIR_COMPLETE: + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + saveClirSetting(msg.arg1); + } + onComplete = (Message) ar.userObj; + if (onComplete != null) { + AsyncResult.forMessage(onComplete, ar.result, ar.exception); + onComplete.sendToTarget(); + } + break; + + default: + super.handleMessage(msg); + } + } + + private void processIccRecordEvents(int eventCode) { + switch (eventCode) { + case SIMRecords.EVENT_CFI: + notifyCallForwardingIndicator(); + break; + case SIMRecords.EVENT_MWI: + notifyMessageWaitingIndicator(); + break; + } + } + + /** + * Sets the "current" field in the telephony provider according to the SIM's operator + * + * @return true for success; false otherwise. + */ + boolean updateCurrentCarrierInProvider() { + if (mIccRecords != null) { + try { + Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current"); + ContentValues map = new ContentValues(); + map.put(Telephony.Carriers.NUMERIC, mIccRecords.getOperatorNumeric()); + mContext.getContentResolver().insert(uri, map); + return true; + } catch (SQLException e) { + Log.e(LOG_TAG, "Can't store current operator", e); + } + } + return false; + } + + /** + * Used to track the settings upon completion of the network change. + */ + private void handleSetSelectNetwork(AsyncResult ar) { + // look for our wrapper within the asyncresult, skip the rest if it + // is null. + if (!(ar.userObj instanceof NetworkSelectMessage)) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "unexpected result from user object."); + return; + } + + NetworkSelectMessage nsm = (NetworkSelectMessage) ar.userObj; + + // found the object, now we send off the message we had originally + // attached to the request. + if (nsm.message != null) { + if (LOCAL_DEBUG) Log.d(LOG_TAG, "sending original message to recipient"); + AsyncResult.forMessage(nsm.message, ar.result, ar.exception); + nsm.message.sendToTarget(); + } + + // open the shared preferences editor, and write the value. + // nsm.operatorNumeric is "" if we're in automatic.selection. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putString(NETWORK_SELECTION_KEY, nsm.operatorNumeric); + editor.putString(NETWORK_SELECTION_NAME_KEY, nsm.operatorAlphaLong); + + // commit and log the result. + if (! editor.commit()) { + Log.e(LOG_TAG, "failed to commit network selection preference"); + } + + } + + /** + * Saves CLIR setting so that we can re-apply it as necessary + * (in case the RIL resets it across reboots). + */ + public void saveClirSetting(int commandInterfaceCLIRMode) { + // open the shared preferences editor, and write the value. + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences.Editor editor = sp.edit(); + editor.putInt(CLIR_KEY, commandInterfaceCLIRMode); + + // commit and log the result. + if (! editor.commit()) { + Log.e(LOG_TAG, "failed to commit CLIR preference"); + } + } + + private void handleCfuQueryResult(CallForwardInfo[] infos) { + if (infos == null || infos.length == 0) { + // Assume the default is not active + // Set unconditional CFF in SIM to false + mIccRecords.setVoiceCallForwardingFlag(1, false); + } else { + for (int i = 0, s = infos.length; i < s; i++) { + if ((infos[i].serviceClass & SERVICE_CLASS_VOICE) != 0) { + mIccRecords.setVoiceCallForwardingFlag(1, (infos[i].status == 1)); + // should only have the one + break; + } + } + } + } + + /** + * Retrieves the PhoneSubInfo of the GSMPhone + */ + public PhoneSubInfo getPhoneSubInfo(){ + return mSubInfo; + } + + /** + * Retrieves the IccSmsInterfaceManager of the GSMPhone + */ + public IccSmsInterfaceManager getIccSmsInterfaceManager(){ + return mSimSmsIntManager; + } + + /** + * Retrieves the IccPhoneBookInterfaceManager of the GSMPhone + */ + public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){ + return mSimPhoneBookIntManager; + } + + /** + * Activate or deactivate cell broadcast SMS. + * + * @param activate 0 = activate, 1 = deactivate + * @param response Callback message is empty on completion + */ + public void activateCellBroadcastSms(int activate, Message response) { + Log.e(LOG_TAG, "[GSMPhone] activateCellBroadcastSms() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + /** + * Query the current configuration of cdma cell broadcast SMS. + * + * @param response Callback message is empty on completion + */ + public void getCellBroadcastSmsConfig(Message response) { + Log.e(LOG_TAG, "[GSMPhone] getCellBroadcastSmsConfig() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + /** + * Configure cdma cell broadcast SMS. + * + * @param response Callback message is empty on completion + */ + public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response) { + Log.e(LOG_TAG, "[GSMPhone] setCellBroadcastSmsConfig() is obsolete; use SmsManager"); + response.sendToTarget(); + } + + public boolean isCspPlmnEnabled() { + return mIccRecords.isCspPlmnEnabled(); + } + + private void registerForSimRecordEvents() { + mIccRecords.registerForNetworkSelectionModeAutomatic( + this, EVENT_SET_NETWORK_AUTOMATIC, null); + mIccRecords.registerForNewSms(this, EVENT_NEW_ICC_SMS, null); + mIccRecords.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null); + mIccRecords.registerForRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null); + } + + private void unregisterForSimRecordEvents() { + mIccRecords.unregisterForNetworkSelectionModeAutomatic(this); + mIccRecords.unregisterForNewSms(this); + mIccRecords.unregisterForRecordsEvents(this); + mIccRecords.unregisterForRecordsLoaded(this); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GSMPhone extends:"); + super.dump(fd, pw, args); + pw.println(" mCT=" + mCT); + pw.println(" mSST=" + mSST); + pw.println(" mPendingMMIs=" + mPendingMMIs); + pw.println(" mSimPhoneBookIntManager=" + mSimPhoneBookIntManager); + pw.println(" mSimSmsIntManager=" + mSimSmsIntManager); + pw.println(" mSubInfo=" + mSubInfo); + if (VDBG) pw.println(" mImei=" + mImei); + if (VDBG) pw.println(" mImeiSv=" + mImeiSv); + pw.println(" mVmNumber=" + mVmNumber); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmCall.java b/src/java/com/android/internal/telephony/gsm/GsmCall.java new file mode 100644 index 0000000..58124a2 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmCall.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.DriverCall; +import com.android.internal.telephony.Phone; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@hide} + */ +class GsmCall extends Call { + /*************************** Instance Variables **************************/ + + /*package*/ ArrayList<Connection> connections = new ArrayList<Connection>(); + /*package*/ GsmCallTracker owner; + + + /***************************** Class Methods *****************************/ + + static State + stateFromDCState (DriverCall.State dcState) { + switch (dcState) { + case ACTIVE: return State.ACTIVE; + case HOLDING: return State.HOLDING; + case DIALING: return State.DIALING; + case ALERTING: return State.ALERTING; + case INCOMING: return State.INCOMING; + case WAITING: return State.WAITING; + default: throw new RuntimeException ("illegal call state:" + dcState); + } + } + + + /****************************** Constructors *****************************/ + /*package*/ + GsmCall (GsmCallTracker owner) { + this.owner = owner; + } + + public void dispose() { + } + + /************************** Overridden from Call *************************/ + + public List<Connection> + getConnections() { + // FIXME should return Collections.unmodifiableList(); + return connections; + } + + public Phone + getPhone() { + return owner.phone; + } + + public boolean + isMultiparty() { + return connections.size() > 1; + } + + /** Please note: if this is the foreground call and a + * background call exists, the background call will be resumed + * because an AT+CHLD=1 will be sent + */ + public void + hangup() throws CallStateException { + owner.hangup(this); + } + + public String + toString() { + return state.toString(); + } + + //***** Called from GsmConnection + + /*package*/ void + attach(Connection conn, DriverCall dc) { + connections.add(conn); + + state = stateFromDCState (dc.state); + } + + /*package*/ void + attachFake(Connection conn, State state) { + connections.add(conn); + + this.state = state; + } + + /** + * Called by GsmConnection when it has disconnected + */ + void + connectionDisconnected(GsmConnection conn) { + if (state != State.DISCONNECTED) { + /* If only disconnected connections remain, we are disconnected*/ + + boolean hasOnlyDisconnectedConnections = true; + + for (int i = 0, s = connections.size() ; i < s; i ++) { + if (connections.get(i).getState() + != State.DISCONNECTED + ) { + hasOnlyDisconnectedConnections = false; + break; + } + } + + if (hasOnlyDisconnectedConnections) { + state = State.DISCONNECTED; + } + } + } + + + /*package*/ void + detach(GsmConnection conn) { + connections.remove(conn); + + if (connections.size() == 0) { + state = State.IDLE; + } + } + + /*package*/ boolean + update (GsmConnection conn, DriverCall dc) { + State newState; + boolean changed = false; + + newState = stateFromDCState(dc.state); + + if (newState != state) { + state = newState; + changed = true; + } + + return changed; + } + + /** + * @return true if there's no space in this call for additional + * connections to be added via "conference" + */ + /*package*/ boolean + isFull() { + return connections.size() == GsmCallTracker.MAX_CONNECTIONS_PER_CALL; + } + + //***** Called from GsmCallTracker + + + /** + * Called when this Call is being hung up locally (eg, user pressed "end") + * Note that at this point, the hangup request has been dispatched to the radio + * but no response has yet been received so update() has not yet been called + */ + void + onHangupLocal() { + for (int i = 0, s = connections.size() + ; i < s; i++ + ) { + GsmConnection cn = (GsmConnection)connections.get(i); + + cn.onHangupLocal(); + } + state = State.DISCONNECTING; + } + + /** + * Called when it's time to clean up disconnected Connection objects + */ + void + clearDisconnected() { + for (int i = connections.size() - 1 ; i >= 0 ; i--) { + GsmConnection cn = (GsmConnection)connections.get(i); + + if (cn.getState() == State.DISCONNECTED) { + connections.remove(i); + } + } + + if (connections.size() == 0) { + state = State.IDLE; + } + } +} + diff --git a/src/java/com/android/internal/telephony/gsm/GsmCallTracker.java b/src/java/com/android/internal/telephony/gsm/GsmCallTracker.java new file mode 100644 index 0000000..fc7fe8a --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmCallTracker.java @@ -0,0 +1,952 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemProperties; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.telephony.gsm.GsmCellLocation; +import android.util.EventLog; +import android.util.Log; + +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.CallTracker; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.DriverCall; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.UUSInfo; +import com.android.internal.telephony.gsm.CallFailCause; +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.gsm.GsmCall; +import com.android.internal.telephony.gsm.GsmConnection; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; +import java.util.ArrayList; + +/** + * {@hide} + */ +public final class GsmCallTracker extends CallTracker { + static final String LOG_TAG = "GSM"; + private static final boolean REPEAT_POLLING = false; + + private static final boolean DBG_POLL = false; + + //***** Constants + + static final int MAX_CONNECTIONS = 7; // only 7 connections allowed in GSM + static final int MAX_CONNECTIONS_PER_CALL = 5; // only 5 connections allowed per call + + //***** Instance Variables + GsmConnection connections[] = new GsmConnection[MAX_CONNECTIONS]; + RegistrantList voiceCallEndedRegistrants = new RegistrantList(); + RegistrantList voiceCallStartedRegistrants = new RegistrantList(); + + + // connections dropped during last poll + ArrayList<GsmConnection> droppedDuringPoll + = new ArrayList<GsmConnection>(MAX_CONNECTIONS); + + GsmCall ringingCall = new GsmCall(this); + // A call that is ringing or (call) waiting + GsmCall foregroundCall = new GsmCall(this); + GsmCall backgroundCall = new GsmCall(this); + + GsmConnection pendingMO; + boolean hangupPendingMO; + + GSMPhone phone; + + boolean desiredMute = false; // false = mute off + + PhoneConstants.State state = PhoneConstants.State.IDLE; + + + + //***** Events + + + //***** Constructors + + GsmCallTracker (GSMPhone phone) { + this.phone = phone; + cm = phone.mCM; + + cm.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null); + + cm.registerForOn(this, EVENT_RADIO_AVAILABLE, null); + cm.registerForNotAvailable(this, EVENT_RADIO_NOT_AVAILABLE, null); + } + + public void dispose() { + //Unregister for all events + cm.unregisterForCallStateChanged(this); + cm.unregisterForOn(this); + cm.unregisterForNotAvailable(this); + + for(GsmConnection c : connections) { + try { + if(c != null) hangup(c); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup during dispose"); + } + } + + try { + if(pendingMO != null) hangup(pendingMO); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup during dispose"); + } + + clearDisconnected(); + } + + protected void finalize() { + Log.d(LOG_TAG, "GsmCallTracker finalized"); + } + + //***** Instance Methods + + //***** Public Methods + public void registerForVoiceCallStarted(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + voiceCallStartedRegistrants.add(r); + } + + public void unregisterForVoiceCallStarted(Handler h) { + voiceCallStartedRegistrants.remove(h); + } + + public void registerForVoiceCallEnded(Handler h, int what, Object obj) { + Registrant r = new Registrant(h, what, obj); + voiceCallEndedRegistrants.add(r); + } + + public void unregisterForVoiceCallEnded(Handler h) { + voiceCallEndedRegistrants.remove(h); + } + + private void + fakeHoldForegroundBeforeDial() { + List<Connection> connCopy; + + // We need to make a copy here, since fakeHoldBeforeDial() + // modifies the lists, and we don't want to reverse the order + connCopy = (List<Connection>) foregroundCall.connections.clone(); + + for (int i = 0, s = connCopy.size() ; i < s ; i++) { + GsmConnection conn = (GsmConnection)connCopy.get(i); + + conn.fakeHoldBeforeDial(); + } + } + + /** + * clirMode is one of the CLIR_ constants + */ + synchronized Connection + dial (String dialString, int clirMode, UUSInfo uusInfo) throws CallStateException { + // note that this triggers call state changed notif + clearDisconnected(); + + if (!canDial()) { + throw new CallStateException("cannot dial in current state"); + } + + // The new call must be assigned to the foreground call. + // That call must be idle, so place anything that's + // there on hold + if (foregroundCall.getState() == GsmCall.State.ACTIVE) { + // this will probably be done by the radio anyway + // but the dial might fail before this happens + // and we need to make sure the foreground call is clear + // for the newly dialed connection + switchWaitingOrHoldingAndActive(); + + // Fake local state so that + // a) foregroundCall is empty for the newly dialed connection + // b) hasNonHangupStateChanged remains false in the + // next poll, so that we don't clear a failed dialing call + fakeHoldForegroundBeforeDial(); + } + + if (foregroundCall.getState() != GsmCall.State.IDLE) { + //we should have failed in !canDial() above before we get here + throw new CallStateException("cannot dial in current state"); + } + + pendingMO = new GsmConnection(phone.getContext(), checkForTestEmergencyNumber(dialString), + this, foregroundCall); + hangupPendingMO = false; + + if (pendingMO.address == null || pendingMO.address.length() == 0 + || pendingMO.address.indexOf(PhoneNumberUtils.WILD) >= 0 + ) { + // Phone number is invalid + pendingMO.cause = Connection.DisconnectCause.INVALID_NUMBER; + + // handlePollCalls() will notice this call not present + // and will mark it as dropped. + pollCallsWhenSafe(); + } else { + // Always unmute when initiating a new call + setMute(false); + + cm.dial(pendingMO.address, clirMode, uusInfo, obtainCompleteMessage()); + } + + updatePhoneState(); + phone.notifyPreciseCallStateChanged(); + + return pendingMO; + } + + Connection + dial(String dialString) throws CallStateException { + return dial(dialString, CommandsInterface.CLIR_DEFAULT, null); + } + + Connection + dial(String dialString, UUSInfo uusInfo) throws CallStateException { + return dial(dialString, CommandsInterface.CLIR_DEFAULT, uusInfo); + } + + Connection + dial(String dialString, int clirMode) throws CallStateException { + return dial(dialString, clirMode, null); + } + + void + acceptCall () throws CallStateException { + // FIXME if SWITCH fails, should retry with ANSWER + // in case the active/holding call disappeared and this + // is no longer call waiting + + if (ringingCall.getState() == GsmCall.State.INCOMING) { + Log.i("phone", "acceptCall: incoming..."); + // Always unmute when answering a new call + setMute(false); + cm.acceptCall(obtainCompleteMessage()); + } else if (ringingCall.getState() == GsmCall.State.WAITING) { + setMute(false); + switchWaitingOrHoldingAndActive(); + } else { + throw new CallStateException("phone not ringing"); + } + } + + void + rejectCall () throws CallStateException { + // AT+CHLD=0 means "release held or UDUB" + // so if the phone isn't ringing, this could hang up held + if (ringingCall.getState().isRinging()) { + cm.rejectCall(obtainCompleteMessage()); + } else { + throw new CallStateException("phone not ringing"); + } + } + + void + switchWaitingOrHoldingAndActive() throws CallStateException { + // Should we bother with this check? + if (ringingCall.getState() == GsmCall.State.INCOMING) { + throw new CallStateException("cannot be in the incoming state"); + } else { + cm.switchWaitingOrHoldingAndActive( + obtainCompleteMessage(EVENT_SWITCH_RESULT)); + } + } + + void + conference() throws CallStateException { + cm.conference(obtainCompleteMessage(EVENT_CONFERENCE_RESULT)); + } + + void + explicitCallTransfer() throws CallStateException { + cm.explicitCallTransfer(obtainCompleteMessage(EVENT_ECT_RESULT)); + } + + void + clearDisconnected() { + internalClearDisconnected(); + + updatePhoneState(); + phone.notifyPreciseCallStateChanged(); + } + + boolean + canConference() { + return foregroundCall.getState() == GsmCall.State.ACTIVE + && backgroundCall.getState() == GsmCall.State.HOLDING + && !backgroundCall.isFull() + && !foregroundCall.isFull(); + } + + boolean + canDial() { + boolean ret; + int serviceState = phone.getServiceState().getState(); + String disableCall = SystemProperties.get( + TelephonyProperties.PROPERTY_DISABLE_CALL, "false"); + + ret = (serviceState != ServiceState.STATE_POWER_OFF) + && pendingMO == null + && !ringingCall.isRinging() + && !disableCall.equals("true") + && (!foregroundCall.getState().isAlive() + || !backgroundCall.getState().isAlive()); + + return ret; + } + + boolean + canTransfer() { + return foregroundCall.getState() == GsmCall.State.ACTIVE + && backgroundCall.getState() == GsmCall.State.HOLDING; + } + + //***** Private Instance Methods + + private void + internalClearDisconnected() { + ringingCall.clearDisconnected(); + foregroundCall.clearDisconnected(); + backgroundCall.clearDisconnected(); + } + + /** + * Obtain a message to use for signalling "invoke getCurrentCalls() when + * this operation and all other pending operations are complete + */ + private Message + obtainCompleteMessage() { + return obtainCompleteMessage(EVENT_OPERATION_COMPLETE); + } + + /** + * Obtain a message to use for signalling "invoke getCurrentCalls() when + * this operation and all other pending operations are complete + */ + private Message + obtainCompleteMessage(int what) { + pendingOperations++; + lastRelevantPoll = null; + needsPoll = true; + + if (DBG_POLL) log("obtainCompleteMessage: pendingOperations=" + + pendingOperations + ", needsPoll=" + needsPoll); + + return obtainMessage(what); + } + + private void + operationComplete() { + pendingOperations--; + + if (DBG_POLL) log("operationComplete: pendingOperations=" + + pendingOperations + ", needsPoll=" + needsPoll); + + if (pendingOperations == 0 && needsPoll) { + lastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT); + cm.getCurrentCalls(lastRelevantPoll); + } else if (pendingOperations < 0) { + // this should never happen + Log.e(LOG_TAG,"GsmCallTracker.pendingOperations < 0"); + pendingOperations = 0; + } + } + + private void + updatePhoneState() { + PhoneConstants.State oldState = state; + + if (ringingCall.isRinging()) { + state = PhoneConstants.State.RINGING; + } else if (pendingMO != null || + !(foregroundCall.isIdle() && backgroundCall.isIdle())) { + state = PhoneConstants.State.OFFHOOK; + } else { + state = PhoneConstants.State.IDLE; + } + + if (state == PhoneConstants.State.IDLE && oldState != state) { + voiceCallEndedRegistrants.notifyRegistrants( + new AsyncResult(null, null, null)); + } else if (oldState == PhoneConstants.State.IDLE && oldState != state) { + voiceCallStartedRegistrants.notifyRegistrants ( + new AsyncResult(null, null, null)); + } + + if (state != oldState) { + phone.notifyPhoneStateChanged(); + } + } + + protected synchronized void + handlePollCalls(AsyncResult ar) { + List polledCalls; + + if (ar.exception == null) { + polledCalls = (List)ar.result; + } else if (isCommandExceptionRadioNotAvailable(ar.exception)) { + // just a dummy empty ArrayList to cause the loop + // to hang up all the calls + polledCalls = new ArrayList(); + } else { + // Radio probably wasn't ready--try again in a bit + // But don't keep polling if the channel is closed + pollCallsAfterDelay(); + return; + } + + Connection newRinging = null; //or waiting + boolean hasNonHangupStateChanged = false; // Any change besides + // a dropped connection + boolean needsPollDelay = false; + boolean unknownConnectionAppeared = false; + + for (int i = 0, curDC = 0, dcSize = polledCalls.size() + ; i < connections.length; i++) { + GsmConnection conn = connections[i]; + DriverCall dc = null; + + // polledCall list is sparse + if (curDC < dcSize) { + dc = (DriverCall) polledCalls.get(curDC); + + if (dc.index == i+1) { + curDC++; + } else { + dc = null; + } + } + + if (DBG_POLL) log("poll: conn[i=" + i + "]=" + + conn+", dc=" + dc); + + if (conn == null && dc != null) { + // Connection appeared in CLCC response that we don't know about + if (pendingMO != null && pendingMO.compareTo(dc)) { + + if (DBG_POLL) log("poll: pendingMO=" + pendingMO); + + // It's our pending mobile originating call + connections[i] = pendingMO; + pendingMO.index = i; + pendingMO.update(dc); + pendingMO = null; + + // Someone has already asked to hangup this call + if (hangupPendingMO) { + hangupPendingMO = false; + try { + if (Phone.DEBUG_PHONE) log( + "poll: hangupPendingMO, hangup conn " + i); + hangup(connections[i]); + } catch (CallStateException ex) { + Log.e(LOG_TAG, "unexpected error on hangup"); + } + + // Do not continue processing this poll + // Wait for hangup and repoll + return; + } + } else { + connections[i] = new GsmConnection(phone.getContext(), dc, this, i); + + // it's a ringing call + if (connections[i].getCall() == ringingCall) { + newRinging = connections[i]; + } else { + // Something strange happened: a call appeared + // which is neither a ringing call or one we created. + // Either we've crashed and re-attached to an existing + // call, or something else (eg, SIM) initiated the call. + + Log.i(LOG_TAG,"Phantom call appeared " + dc); + + // If it's a connected call, set the connect time so that + // it's non-zero. It may not be accurate, but at least + // it won't appear as a Missed Call. + if (dc.state != DriverCall.State.ALERTING + && dc.state != DriverCall.State.DIALING) { + connections[i].connectTime = System.currentTimeMillis(); + } + + unknownConnectionAppeared = true; + } + } + hasNonHangupStateChanged = true; + } else if (conn != null && dc == null) { + // Connection missing in CLCC response that we were + // tracking. + droppedDuringPoll.add(conn); + // Dropped connections are removed from the CallTracker + // list but kept in the GsmCall list + connections[i] = null; + } else if (conn != null && dc != null && !conn.compareTo(dc)) { + // Connection in CLCC response does not match what + // we were tracking. Assume dropped call and new call + + droppedDuringPoll.add(conn); + connections[i] = new GsmConnection (phone.getContext(), dc, this, i); + + if (connections[i].getCall() == ringingCall) { + newRinging = connections[i]; + } // else something strange happened + hasNonHangupStateChanged = true; + } else if (conn != null && dc != null) { /* implicit conn.compareTo(dc) */ + boolean changed; + changed = conn.update(dc); + hasNonHangupStateChanged = hasNonHangupStateChanged || changed; + } + + if (REPEAT_POLLING) { + if (dc != null) { + // FIXME with RIL, we should not need this anymore + if ((dc.state == DriverCall.State.DIALING + /*&& cm.getOption(cm.OPTION_POLL_DIALING)*/) + || (dc.state == DriverCall.State.ALERTING + /*&& cm.getOption(cm.OPTION_POLL_ALERTING)*/) + || (dc.state == DriverCall.State.INCOMING + /*&& cm.getOption(cm.OPTION_POLL_INCOMING)*/) + || (dc.state == DriverCall.State.WAITING + /*&& cm.getOption(cm.OPTION_POLL_WAITING)*/) + ) { + // Sometimes there's no unsolicited notification + // for state transitions + needsPollDelay = true; + } + } + } + } + + // This is the first poll after an ATD. + // We expect the pending call to appear in the list + // If it does not, we land here + if (pendingMO != null) { + Log.d(LOG_TAG,"Pending MO dropped before poll fg state:" + + foregroundCall.getState()); + + droppedDuringPoll.add(pendingMO); + pendingMO = null; + hangupPendingMO = false; + } + + if (newRinging != null) { + phone.notifyNewRingingConnection(newRinging); + } + + // clear the "local hangup" and "missed/rejected call" + // cases from the "dropped during poll" list + // These cases need no "last call fail" reason + for (int i = droppedDuringPoll.size() - 1; i >= 0 ; i--) { + GsmConnection conn = droppedDuringPoll.get(i); + + if (conn.isIncoming() && conn.getConnectTime() == 0) { + // Missed or rejected call + Connection.DisconnectCause cause; + if (conn.cause == Connection.DisconnectCause.LOCAL) { + cause = Connection.DisconnectCause.INCOMING_REJECTED; + } else { + cause = Connection.DisconnectCause.INCOMING_MISSED; + } + + if (Phone.DEBUG_PHONE) { + log("missed/rejected call, conn.cause=" + conn.cause); + log("setting cause to " + cause); + } + droppedDuringPoll.remove(i); + conn.onDisconnect(cause); + } else if (conn.cause == Connection.DisconnectCause.LOCAL) { + // Local hangup + droppedDuringPoll.remove(i); + conn.onDisconnect(Connection.DisconnectCause.LOCAL); + } else if (conn.cause == + Connection.DisconnectCause.INVALID_NUMBER) { + droppedDuringPoll.remove(i); + conn.onDisconnect(Connection.DisconnectCause.INVALID_NUMBER); + } + } + + // Any non-local disconnects: determine cause + if (droppedDuringPoll.size() > 0) { + cm.getLastCallFailCause( + obtainNoPollCompleteMessage(EVENT_GET_LAST_CALL_FAIL_CAUSE)); + } + + if (needsPollDelay) { + pollCallsAfterDelay(); + } + + // Cases when we can no longer keep disconnected Connection's + // with their previous calls + // 1) the phone has started to ring + // 2) A Call/Connection object has changed state... + // we may have switched or held or answered (but not hung up) + if (newRinging != null || hasNonHangupStateChanged) { + internalClearDisconnected(); + } + + updatePhoneState(); + + if (unknownConnectionAppeared) { + phone.notifyUnknownConnection(); + } + + if (hasNonHangupStateChanged || newRinging != null) { + phone.notifyPreciseCallStateChanged(); + } + + //dumpState(); + } + + private void + handleRadioNotAvailable() { + // handlePollCalls will clear out its + // call list when it gets the CommandException + // error result from this + pollCallsWhenSafe(); + } + + private void + dumpState() { + List l; + + Log.i(LOG_TAG,"Phone State:" + state); + + Log.i(LOG_TAG,"Ringing call: " + ringingCall.toString()); + + l = ringingCall.getConnections(); + for (int i = 0, s = l.size(); i < s; i++) { + Log.i(LOG_TAG,l.get(i).toString()); + } + + Log.i(LOG_TAG,"Foreground call: " + foregroundCall.toString()); + + l = foregroundCall.getConnections(); + for (int i = 0, s = l.size(); i < s; i++) { + Log.i(LOG_TAG,l.get(i).toString()); + } + + Log.i(LOG_TAG,"Background call: " + backgroundCall.toString()); + + l = backgroundCall.getConnections(); + for (int i = 0, s = l.size(); i < s; i++) { + Log.i(LOG_TAG,l.get(i).toString()); + } + + } + + //***** Called from GsmConnection + + /*package*/ void + hangup (GsmConnection conn) throws CallStateException { + if (conn.owner != this) { + throw new CallStateException ("GsmConnection " + conn + + "does not belong to GsmCallTracker " + this); + } + + if (conn == pendingMO) { + // We're hanging up an outgoing call that doesn't have it's + // GSM index assigned yet + + if (Phone.DEBUG_PHONE) log("hangup: set hangupPendingMO to true"); + hangupPendingMO = true; + } else { + try { + cm.hangupConnection (conn.getGSMIndex(), obtainCompleteMessage()); + } catch (CallStateException ex) { + // Ignore "connection not found" + // Call may have hung up already + Log.w(LOG_TAG,"GsmCallTracker WARN: hangup() on absent connection " + + conn); + } + } + + conn.onHangupLocal(); + } + + /*package*/ void + separate (GsmConnection conn) throws CallStateException { + if (conn.owner != this) { + throw new CallStateException ("GsmConnection " + conn + + "does not belong to GsmCallTracker " + this); + } + try { + cm.separateConnection (conn.getGSMIndex(), + obtainCompleteMessage(EVENT_SEPARATE_RESULT)); + } catch (CallStateException ex) { + // Ignore "connection not found" + // Call may have hung up already + Log.w(LOG_TAG,"GsmCallTracker WARN: separate() on absent connection " + + conn); + } + } + + //***** Called from GSMPhone + + /*package*/ void + setMute(boolean mute) { + desiredMute = mute; + cm.setMute(desiredMute, null); + } + + /*package*/ boolean + getMute() { + return desiredMute; + } + + + //***** Called from GsmCall + + /* package */ void + hangup (GsmCall call) throws CallStateException { + if (call.getConnections().size() == 0) { + throw new CallStateException("no connections in call"); + } + + if (call == ringingCall) { + if (Phone.DEBUG_PHONE) log("(ringing) hangup waiting or background"); + cm.hangupWaitingOrBackground(obtainCompleteMessage()); + } else if (call == foregroundCall) { + if (call.isDialingOrAlerting()) { + if (Phone.DEBUG_PHONE) { + log("(foregnd) hangup dialing or alerting..."); + } + hangup((GsmConnection)(call.getConnections().get(0))); + } else { + hangupForegroundResumeBackground(); + } + } else if (call == backgroundCall) { + if (ringingCall.isRinging()) { + if (Phone.DEBUG_PHONE) { + log("hangup all conns in background call"); + } + hangupAllConnections(call); + } else { + hangupWaitingOrBackground(); + } + } else { + throw new RuntimeException ("GsmCall " + call + + "does not belong to GsmCallTracker " + this); + } + + call.onHangupLocal(); + phone.notifyPreciseCallStateChanged(); + } + + /* package */ + void hangupWaitingOrBackground() { + if (Phone.DEBUG_PHONE) log("hangupWaitingOrBackground"); + cm.hangupWaitingOrBackground(obtainCompleteMessage()); + } + + /* package */ + void hangupForegroundResumeBackground() { + if (Phone.DEBUG_PHONE) log("hangupForegroundResumeBackground"); + cm.hangupForegroundResumeBackground(obtainCompleteMessage()); + } + + void hangupConnectionByIndex(GsmCall call, int index) + throws CallStateException { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + GsmConnection cn = (GsmConnection)call.connections.get(i); + if (cn.getGSMIndex() == index) { + cm.hangupConnection(index, obtainCompleteMessage()); + return; + } + } + + throw new CallStateException("no gsm index found"); + } + + void hangupAllConnections(GsmCall call) throws CallStateException{ + try { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + GsmConnection cn = (GsmConnection)call.connections.get(i); + cm.hangupConnection(cn.getGSMIndex(), obtainCompleteMessage()); + } + } catch (CallStateException ex) { + Log.e(LOG_TAG, "hangupConnectionByIndex caught " + ex); + } + } + + /* package */ + GsmConnection getConnectionByIndex(GsmCall call, int index) + throws CallStateException { + int count = call.connections.size(); + for (int i = 0; i < count; i++) { + GsmConnection cn = (GsmConnection)call.connections.get(i); + if (cn.getGSMIndex() == index) { + return cn; + } + } + + return null; + } + + private Phone.SuppService getFailedService(int what) { + switch (what) { + case EVENT_SWITCH_RESULT: + return Phone.SuppService.SWITCH; + case EVENT_CONFERENCE_RESULT: + return Phone.SuppService.CONFERENCE; + case EVENT_SEPARATE_RESULT: + return Phone.SuppService.SEPARATE; + case EVENT_ECT_RESULT: + return Phone.SuppService.TRANSFER; + } + return Phone.SuppService.UNKNOWN; + } + + //****** Overridden from Handler + + public void + handleMessage (Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_POLL_CALLS_RESULT: + ar = (AsyncResult)msg.obj; + + if (msg == lastRelevantPoll) { + if (DBG_POLL) log( + "handle EVENT_POLL_CALL_RESULT: set needsPoll=F"); + needsPoll = false; + lastRelevantPoll = null; + handlePollCalls((AsyncResult)msg.obj); + } + break; + + case EVENT_OPERATION_COMPLETE: + ar = (AsyncResult)msg.obj; + operationComplete(); + break; + + case EVENT_SWITCH_RESULT: + case EVENT_CONFERENCE_RESULT: + case EVENT_SEPARATE_RESULT: + case EVENT_ECT_RESULT: + ar = (AsyncResult)msg.obj; + if (ar.exception != null) { + phone.notifySuppServiceFailed(getFailedService(msg.what)); + } + operationComplete(); + break; + + case EVENT_GET_LAST_CALL_FAIL_CAUSE: + int causeCode; + ar = (AsyncResult)msg.obj; + + operationComplete(); + + if (ar.exception != null) { + // An exception occurred...just treat the disconnect + // cause as "normal" + causeCode = CallFailCause.NORMAL_CLEARING; + Log.i(LOG_TAG, + "Exception during getLastCallFailCause, assuming normal disconnect"); + } else { + causeCode = ((int[])ar.result)[0]; + } + // Log the causeCode if its not normal + if (causeCode == CallFailCause.NO_CIRCUIT_AVAIL || + causeCode == CallFailCause.TEMPORARY_FAILURE || + causeCode == CallFailCause.SWITCHING_CONGESTION || + causeCode == CallFailCause.CHANNEL_NOT_AVAIL || + causeCode == CallFailCause.QOS_NOT_AVAIL || + causeCode == CallFailCause.BEARER_NOT_AVAIL || + causeCode == CallFailCause.ERROR_UNSPECIFIED) { + GsmCellLocation loc = ((GsmCellLocation)phone.getCellLocation()); + EventLog.writeEvent(EventLogTags.CALL_DROP, + causeCode, loc != null ? loc.getCid() : -1, + TelephonyManager.getDefault().getNetworkType()); + } + + for (int i = 0, s = droppedDuringPoll.size() + ; i < s ; i++ + ) { + GsmConnection conn = droppedDuringPoll.get(i); + + conn.onRemoteDisconnect(causeCode); + } + + updatePhoneState(); + + phone.notifyPreciseCallStateChanged(); + droppedDuringPoll.clear(); + break; + + case EVENT_REPOLL_AFTER_DELAY: + case EVENT_CALL_STATE_CHANGE: + pollCallsWhenSafe(); + break; + + case EVENT_RADIO_AVAILABLE: + handleRadioAvailable(); + break; + + case EVENT_RADIO_NOT_AVAILABLE: + handleRadioNotAvailable(); + break; + } + } + + protected void log(String msg) { + Log.d(LOG_TAG, "[GsmCallTracker] " + msg); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GsmCallTracker extends:"); + super.dump(fd, pw, args); + pw.println("connections: length=" + connections.length); + for(int i=0; i < connections.length; i++) { + pw.printf(" connections[%d]=%s\n", i, connections[i]); + } + pw.println(" voiceCallEndedRegistrants=" + voiceCallEndedRegistrants); + pw.println(" voiceCallStartedRegistrants=" + voiceCallStartedRegistrants); + pw.println(" droppedDuringPoll: size=" + droppedDuringPoll.size()); + for(int i = 0; i < droppedDuringPoll.size(); i++) { + pw.printf( " droppedDuringPoll[%d]=%s\n", i, droppedDuringPoll.get(i)); + } + pw.println(" ringingCall=" + ringingCall); + pw.println(" foregroundCall=" + foregroundCall); + pw.println(" backgroundCall=" + backgroundCall); + pw.println(" pendingMO=" + pendingMO); + pw.println(" hangupPendingMO=" + hangupPendingMO); + pw.println(" phone=" + phone); + pw.println(" desiredMute=" + desiredMute); + pw.println(" state=" + state); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmConnection.java b/src/java/com/android/internal/telephony/gsm/GsmConnection.java new file mode 100644 index 0000000..9fc94a5 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmConnection.java @@ -0,0 +1,761 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.Registrant; +import android.os.SystemClock; +import android.util.Log; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.text.TextUtils; + +import com.android.internal.telephony.*; + +/** + * {@hide} + */ +public class GsmConnection extends Connection { + static final String LOG_TAG = "GSM"; + + //***** Instance Variables + + GsmCallTracker owner; + GsmCall parent; + + String address; // MAY BE NULL!!! + String dialString; // outgoing calls only + String postDialString; // outgoing calls only + boolean isIncoming; + boolean disconnected; + + int index; // index in GsmCallTracker.connections[], -1 if unassigned + // The GSM index is 1 + this + + /* + * These time/timespan values are based on System.currentTimeMillis(), + * i.e., "wall clock" time. + */ + long createTime; + long connectTime; + long disconnectTime; + + /* + * These time/timespan values are based on SystemClock.elapsedRealTime(), + * i.e., time since boot. They are appropriate for comparison and + * calculating deltas. + */ + long connectTimeReal; + long duration; + long holdingStartTime; // The time when the Connection last transitioned + // into HOLDING + + int nextPostDialChar; // index into postDialString + + DisconnectCause cause = DisconnectCause.NOT_DISCONNECTED; + PostDialState postDialState = PostDialState.NOT_STARTED; + int numberPresentation = PhoneConstants.PRESENTATION_ALLOWED; + UUSInfo uusInfo; + + Handler h; + + private PowerManager.WakeLock mPartialWakeLock; + + //***** Event Constants + static final int EVENT_DTMF_DONE = 1; + static final int EVENT_PAUSE_DONE = 2; + static final int EVENT_NEXT_POST_DIAL = 3; + static final int EVENT_WAKE_LOCK_TIMEOUT = 4; + + //***** Constants + static final int PAUSE_DELAY_FIRST_MILLIS = 100; + static final int PAUSE_DELAY_MILLIS = 3 * 1000; + static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000; + + //***** Inner Classes + + class MyHandler extends Handler { + MyHandler(Looper l) {super(l);} + + public void + handleMessage(Message msg) { + + switch (msg.what) { + case EVENT_NEXT_POST_DIAL: + case EVENT_DTMF_DONE: + case EVENT_PAUSE_DONE: + processNextPostDialChar(); + break; + case EVENT_WAKE_LOCK_TIMEOUT: + releaseWakeLock(); + break; + } + } + } + + //***** Constructors + + /** This is probably an MT call that we first saw in a CLCC response */ + /*package*/ + GsmConnection (Context context, DriverCall dc, GsmCallTracker ct, int index) { + createWakeLock(context); + acquireWakeLock(); + + owner = ct; + h = new MyHandler(owner.getLooper()); + + address = dc.number; + + isIncoming = dc.isMT; + createTime = System.currentTimeMillis(); + cnapName = dc.name; + cnapNamePresentation = dc.namePresentation; + numberPresentation = dc.numberPresentation; + uusInfo = dc.uusInfo; + + this.index = index; + + parent = parentFromDCState (dc.state); + parent.attach(this, dc); + } + + /** This is an MO call, created when dialing */ + /*package*/ + GsmConnection (Context context, String dialString, GsmCallTracker ct, GsmCall parent) { + createWakeLock(context); + acquireWakeLock(); + + owner = ct; + h = new MyHandler(owner.getLooper()); + + this.dialString = dialString; + + this.address = PhoneNumberUtils.extractNetworkPortionAlt(dialString); + this.postDialString = PhoneNumberUtils.extractPostDialPortion(dialString); + + index = -1; + + isIncoming = false; + cnapName = null; + cnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED; + numberPresentation = PhoneConstants.PRESENTATION_ALLOWED; + createTime = System.currentTimeMillis(); + + this.parent = parent; + parent.attachFake(this, GsmCall.State.DIALING); + } + + public void dispose() { + } + + static boolean + equalsHandlesNulls (Object a, Object b) { + return (a == null) ? (b == null) : a.equals (b); + } + + /*package*/ boolean + compareTo(DriverCall c) { + // On mobile originated (MO) calls, the phone number may have changed + // due to a SIM Toolkit call control modification. + // + // We assume we know when MO calls are created (since we created them) + // and therefore don't need to compare the phone number anyway. + if (! (isIncoming || c.isMT)) return true; + + // ... but we can compare phone numbers on MT calls, and we have + // no control over when they begin, so we might as well + + String cAddress = PhoneNumberUtils.stringFromStringAndTOA(c.number, c.TOA); + return isIncoming == c.isMT && equalsHandlesNulls(address, cAddress); + } + + public String getAddress() { + return address; + } + + public GsmCall getCall() { + return parent; + } + + public long getCreateTime() { + return createTime; + } + + public long getConnectTime() { + return connectTime; + } + + public long getDisconnectTime() { + return disconnectTime; + } + + public long getDurationMillis() { + if (connectTimeReal == 0) { + return 0; + } else if (duration == 0) { + return SystemClock.elapsedRealtime() - connectTimeReal; + } else { + return duration; + } + } + + public long getHoldDurationMillis() { + if (getState() != GsmCall.State.HOLDING) { + // If not holding, return 0 + return 0; + } else { + return SystemClock.elapsedRealtime() - holdingStartTime; + } + } + + public DisconnectCause getDisconnectCause() { + return cause; + } + + public boolean isIncoming() { + return isIncoming; + } + + public GsmCall.State getState() { + if (disconnected) { + return GsmCall.State.DISCONNECTED; + } else { + return super.getState(); + } + } + + public void hangup() throws CallStateException { + if (!disconnected) { + owner.hangup(this); + } else { + throw new CallStateException ("disconnected"); + } + } + + public void separate() throws CallStateException { + if (!disconnected) { + owner.separate(this); + } else { + throw new CallStateException ("disconnected"); + } + } + + public PostDialState getPostDialState() { + return postDialState; + } + + public void proceedAfterWaitChar() { + if (postDialState != PostDialState.WAIT) { + Log.w(LOG_TAG, "GsmConnection.proceedAfterWaitChar(): Expected " + + "getPostDialState() to be WAIT but was " + postDialState); + return; + } + + setPostDialState(PostDialState.STARTED); + + processNextPostDialChar(); + } + + public void proceedAfterWildChar(String str) { + if (postDialState != PostDialState.WILD) { + Log.w(LOG_TAG, "GsmConnection.proceedAfterWaitChar(): Expected " + + "getPostDialState() to be WILD but was " + postDialState); + return; + } + + setPostDialState(PostDialState.STARTED); + + if (false) { + boolean playedTone = false; + int len = (str != null ? str.length() : 0); + + for (int i=0; i<len; i++) { + char c = str.charAt(i); + Message msg = null; + + if (i == len-1) { + msg = h.obtainMessage(EVENT_DTMF_DONE); + } + + if (PhoneNumberUtils.is12Key(c)) { + owner.cm.sendDtmf(c, msg); + playedTone = true; + } + } + + if (!playedTone) { + processNextPostDialChar(); + } + } else { + // make a new postDialString, with the wild char replacement string + // at the beginning, followed by the remaining postDialString. + + StringBuilder buf = new StringBuilder(str); + buf.append(postDialString.substring(nextPostDialChar)); + postDialString = buf.toString(); + nextPostDialChar = 0; + if (Phone.DEBUG_PHONE) { + log("proceedAfterWildChar: new postDialString is " + + postDialString); + } + + processNextPostDialChar(); + } + } + + public void cancelPostDial() { + setPostDialState(PostDialState.CANCELLED); + } + + /** + * Called when this Connection is being hung up locally (eg, user pressed "end") + * Note that at this point, the hangup request has been dispatched to the radio + * but no response has yet been received so update() has not yet been called + */ + void + onHangupLocal() { + cause = DisconnectCause.LOCAL; + } + + DisconnectCause + disconnectCauseFromCode(int causeCode) { + /** + * See 22.001 Annex F.4 for mapping of cause codes + * to local tones + */ + + switch (causeCode) { + case CallFailCause.USER_BUSY: + return DisconnectCause.BUSY; + + case CallFailCause.NO_CIRCUIT_AVAIL: + case CallFailCause.TEMPORARY_FAILURE: + case CallFailCause.SWITCHING_CONGESTION: + case CallFailCause.CHANNEL_NOT_AVAIL: + case CallFailCause.QOS_NOT_AVAIL: + case CallFailCause.BEARER_NOT_AVAIL: + return DisconnectCause.CONGESTION; + + case CallFailCause.ACM_LIMIT_EXCEEDED: + return DisconnectCause.LIMIT_EXCEEDED; + + case CallFailCause.CALL_BARRED: + return DisconnectCause.CALL_BARRED; + + case CallFailCause.FDN_BLOCKED: + return DisconnectCause.FDN_BLOCKED; + + case CallFailCause.UNOBTAINABLE_NUMBER: + return DisconnectCause.UNOBTAINABLE_NUMBER; + + case CallFailCause.ERROR_UNSPECIFIED: + case CallFailCause.NORMAL_CLEARING: + default: + GSMPhone phone = owner.phone; + int serviceState = phone.getServiceState().getState(); + if (serviceState == ServiceState.STATE_POWER_OFF) { + return DisconnectCause.POWER_OFF; + } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE + || serviceState == ServiceState.STATE_EMERGENCY_ONLY ) { + return DisconnectCause.OUT_OF_SERVICE; + } else if (phone.getIccCard().getState() != IccCardConstants.State.READY) { + return DisconnectCause.ICC_ERROR; + } else if (causeCode == CallFailCause.ERROR_UNSPECIFIED) { + if (phone.mSST.mRestrictedState.isCsRestricted()) { + return DisconnectCause.CS_RESTRICTED; + } else if (phone.mSST.mRestrictedState.isCsEmergencyRestricted()) { + return DisconnectCause.CS_RESTRICTED_EMERGENCY; + } else if (phone.mSST.mRestrictedState.isCsNormalRestricted()) { + return DisconnectCause.CS_RESTRICTED_NORMAL; + } else { + return DisconnectCause.ERROR_UNSPECIFIED; + } + } else if (causeCode == CallFailCause.NORMAL_CLEARING) { + return DisconnectCause.NORMAL; + } else { + // If nothing else matches, report unknown call drop reason + // to app, not NORMAL call end. + return DisconnectCause.ERROR_UNSPECIFIED; + } + } + } + + /*package*/ void + onRemoteDisconnect(int causeCode) { + onDisconnect(disconnectCauseFromCode(causeCode)); + } + + /** Called when the radio indicates the connection has been disconnected */ + /*package*/ void + onDisconnect(DisconnectCause cause) { + this.cause = cause; + + if (!disconnected) { + index = -1; + + disconnectTime = System.currentTimeMillis(); + duration = SystemClock.elapsedRealtime() - connectTimeReal; + disconnected = true; + + if (false) Log.d(LOG_TAG, + "[GSMConn] onDisconnect: cause=" + cause); + + owner.phone.notifyDisconnect(this); + + if (parent != null) { + parent.connectionDisconnected(this); + } + } + releaseWakeLock(); + } + + // Returns true if state has changed, false if nothing changed + /*package*/ boolean + update (DriverCall dc) { + GsmCall newParent; + boolean changed = false; + boolean wasConnectingInOrOut = isConnectingInOrOut(); + boolean wasHolding = (getState() == GsmCall.State.HOLDING); + + newParent = parentFromDCState(dc.state); + + if (!equalsHandlesNulls(address, dc.number)) { + if (Phone.DEBUG_PHONE) log("update: phone # changed!"); + address = dc.number; + changed = true; + } + + // A null cnapName should be the same as "" + if (TextUtils.isEmpty(dc.name)) { + if (!TextUtils.isEmpty(cnapName)) { + changed = true; + cnapName = ""; + } + } else if (!dc.name.equals(cnapName)) { + changed = true; + cnapName = dc.name; + } + + if (Phone.DEBUG_PHONE) log("--dssds----"+cnapName); + cnapNamePresentation = dc.namePresentation; + numberPresentation = dc.numberPresentation; + + if (newParent != parent) { + if (parent != null) { + parent.detach(this); + } + newParent.attach(this, dc); + parent = newParent; + changed = true; + } else { + boolean parentStateChange; + parentStateChange = parent.update (this, dc); + changed = changed || parentStateChange; + } + + /** Some state-transition events */ + + if (Phone.DEBUG_PHONE) log( + "update: parent=" + parent + + ", hasNewParent=" + (newParent != parent) + + ", wasConnectingInOrOut=" + wasConnectingInOrOut + + ", wasHolding=" + wasHolding + + ", isConnectingInOrOut=" + isConnectingInOrOut() + + ", changed=" + changed); + + + if (wasConnectingInOrOut && !isConnectingInOrOut()) { + onConnectedInOrOut(); + } + + if (changed && !wasHolding && (getState() == GsmCall.State.HOLDING)) { + // We've transitioned into HOLDING + onStartedHolding(); + } + + return changed; + } + + /** + * Called when this Connection is in the foregroundCall + * when a dial is initiated. + * We know we're ACTIVE, and we know we're going to end up + * HOLDING in the backgroundCall + */ + void + fakeHoldBeforeDial() { + if (parent != null) { + parent.detach(this); + } + + parent = owner.backgroundCall; + parent.attachFake(this, GsmCall.State.HOLDING); + + onStartedHolding(); + } + + /*package*/ int + getGSMIndex() throws CallStateException { + if (index >= 0) { + return index + 1; + } else { + throw new CallStateException ("GSM index not yet assigned"); + } + } + + /** + * An incoming or outgoing call has connected + */ + void + onConnectedInOrOut() { + connectTime = System.currentTimeMillis(); + connectTimeReal = SystemClock.elapsedRealtime(); + duration = 0; + + // bug #678474: incoming call interpreted as missed call, even though + // it sounds like the user has picked up the call. + if (Phone.DEBUG_PHONE) { + log("onConnectedInOrOut: connectTime=" + connectTime); + } + + if (!isIncoming) { + // outgoing calls only + processNextPostDialChar(); + } + releaseWakeLock(); + } + + private void + onStartedHolding() { + holdingStartTime = SystemClock.elapsedRealtime(); + } + /** + * Performs the appropriate action for a post-dial char, but does not + * notify application. returns false if the character is invalid and + * should be ignored + */ + private boolean + processPostDialChar(char c) { + if (PhoneNumberUtils.is12Key(c)) { + owner.cm.sendDtmf(c, h.obtainMessage(EVENT_DTMF_DONE)); + } else if (c == PhoneNumberUtils.PAUSE) { + // From TS 22.101: + + // "The first occurrence of the "DTMF Control Digits Separator" + // shall be used by the ME to distinguish between the addressing + // digits (i.e. the phone number) and the DTMF digits...." + + if (nextPostDialChar == 1) { + // The first occurrence. + // We don't need to pause here, but wait for just a bit anyway + h.sendMessageDelayed(h.obtainMessage(EVENT_PAUSE_DONE), + PAUSE_DELAY_FIRST_MILLIS); + } else { + // It continues... + // "Upon subsequent occurrences of the separator, the UE shall + // pause again for 3 seconds (\u00B1 20 %) before sending any + // further DTMF digits." + h.sendMessageDelayed(h.obtainMessage(EVENT_PAUSE_DONE), + PAUSE_DELAY_MILLIS); + } + } else if (c == PhoneNumberUtils.WAIT) { + setPostDialState(PostDialState.WAIT); + } else if (c == PhoneNumberUtils.WILD) { + setPostDialState(PostDialState.WILD); + } else { + return false; + } + + return true; + } + + public String + getRemainingPostDialString() { + if (postDialState == PostDialState.CANCELLED + || postDialState == PostDialState.COMPLETE + || postDialString == null + || postDialString.length() <= nextPostDialChar + ) { + return ""; + } + + return postDialString.substring(nextPostDialChar); + } + + @Override + protected void finalize() + { + /** + * It is understood that This finializer is not guaranteed + * to be called and the release lock call is here just in + * case there is some path that doesn't call onDisconnect + * and or onConnectedInOrOut. + */ + if (mPartialWakeLock.isHeld()) { + Log.e(LOG_TAG, "[GSMConn] UNEXPECTED; mPartialWakeLock is held when finalizing."); + } + releaseWakeLock(); + } + + private void + processNextPostDialChar() { + char c = 0; + Registrant postDialHandler; + + if (postDialState == PostDialState.CANCELLED) { + //Log.v("GSM", "##### processNextPostDialChar: postDialState == CANCELLED, bail"); + return; + } + + if (postDialString == null || + postDialString.length() <= nextPostDialChar) { + setPostDialState(PostDialState.COMPLETE); + + // notifyMessage.arg1 is 0 on complete + c = 0; + } else { + boolean isValid; + + setPostDialState(PostDialState.STARTED); + + c = postDialString.charAt(nextPostDialChar++); + + isValid = processPostDialChar(c); + + if (!isValid) { + // Will call processNextPostDialChar + h.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget(); + // Don't notify application + Log.e("GSM", "processNextPostDialChar: c=" + c + " isn't valid!"); + return; + } + } + + postDialHandler = owner.phone.mPostDialHandler; + + Message notifyMessage; + + if (postDialHandler != null + && (notifyMessage = postDialHandler.messageForRegistrant()) != null) { + // The AsyncResult.result is the Connection object + PostDialState state = postDialState; + AsyncResult ar = AsyncResult.forMessage(notifyMessage); + ar.result = this; + ar.userObj = state; + + // arg1 is the character that was/is being processed + notifyMessage.arg1 = c; + + //Log.v("GSM", "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c); + notifyMessage.sendToTarget(); + } + } + + + /** "connecting" means "has never been ACTIVE" for both incoming + * and outgoing calls + */ + private boolean + isConnectingInOrOut() { + return parent == null || parent == owner.ringingCall + || parent.state == GsmCall.State.DIALING + || parent.state == GsmCall.State.ALERTING; + } + + private GsmCall + parentFromDCState (DriverCall.State state) { + switch (state) { + case ACTIVE: + case DIALING: + case ALERTING: + return owner.foregroundCall; + //break; + + case HOLDING: + return owner.backgroundCall; + //break; + + case INCOMING: + case WAITING: + return owner.ringingCall; + //break; + + default: + throw new RuntimeException("illegal call state: " + state); + } + } + + /** + * Set post dial state and acquire wake lock while switching to "started" + * state, the wake lock will be released if state switches out of "started" + * state or after WAKE_LOCK_TIMEOUT_MILLIS. + * @param s new PostDialState + */ + private void setPostDialState(PostDialState s) { + if (postDialState != PostDialState.STARTED + && s == PostDialState.STARTED) { + acquireWakeLock(); + Message msg = h.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT); + h.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS); + } else if (postDialState == PostDialState.STARTED + && s != PostDialState.STARTED) { + h.removeMessages(EVENT_WAKE_LOCK_TIMEOUT); + releaseWakeLock(); + } + postDialState = s; + } + + private void + createWakeLock(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG); + } + + private void + acquireWakeLock() { + log("acquireWakeLock"); + mPartialWakeLock.acquire(); + } + + private void + releaseWakeLock() { + synchronized(mPartialWakeLock) { + if (mPartialWakeLock.isHeld()) { + log("releaseWakeLock"); + mPartialWakeLock.release(); + } + } + } + + private void log(String msg) { + Log.d(LOG_TAG, "[GSMConn] " + msg); + } + + @Override + public int getNumberPresentation() { + return numberPresentation; + } + + @Override + public UUSInfo getUUSInfo() { + return uusInfo; + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmDataConnection.java b/src/java/com/android/internal/telephony/gsm/GsmDataConnection.java new file mode 100644 index 0000000..156574d --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmDataConnection.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.os.Message; +import android.util.Log; +import android.util.Patterns; +import android.text.TextUtils; + +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.RetryManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * {@hide} + */ +public class GsmDataConnection extends DataConnection { + + private static final String LOG_TAG = "GSM"; + + //***** Instance Variables + protected int mProfileId = RILConstants.DATA_PROFILE_DEFAULT; + //***** Constructor + private GsmDataConnection(PhoneBase phone, String name, int id, RetryManager rm, + DataConnectionTracker dct) { + super(phone, name, id, rm, dct); + } + + /** + * Create the connection object + * + * @param phone the Phone + * @param id the connection id + * @param rm the RetryManager + * @return GsmDataConnection that was created. + */ + static GsmDataConnection makeDataConnection(PhoneBase phone, int id, RetryManager rm, + DataConnectionTracker dct) { + GsmDataConnection gsmDc = new GsmDataConnection(phone, + "GsmDC-" + mCount.incrementAndGet(), id, rm, dct); + gsmDc.start(); + if (DBG) gsmDc.log("Made " + gsmDc.getName()); + return gsmDc; + } + + /** + * Begin setting up a data connection, calls setupDataCall + * and the ConnectionParams will be returned with the + * EVENT_SETUP_DATA_CONNECTION_DONE AsyncResul.userObj. + * + * @param cp is the connection parameters + */ + @Override + protected + void onConnect(ConnectionParams cp) { + mApn = cp.apn; + + if (DBG) log("Connecting to carrier: '" + mApn.carrier + + "' APN: '" + mApn.apn + + "' proxy: '" + mApn.proxy + "' port: '" + mApn.port); + + createTime = -1; + lastFailTime = -1; + lastFailCause = FailCause.NONE; + + // msg.obj will be returned in AsyncResult.userObj; + Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp); + msg.obj = cp; + + int authType = mApn.authType; + if (authType == -1) { + authType = TextUtils.isEmpty(mApn.user) ? RILConstants.SETUP_DATA_AUTH_NONE + : RILConstants.SETUP_DATA_AUTH_PAP_CHAP; + } + + String protocol; + if (phone.getServiceState().getRoaming()) { + protocol = mApn.roamingProtocol; + } else { + protocol = mApn.protocol; + } + + phone.mCM.setupDataCall( + Integer.toString(getRilRadioTechnology(RILConstants.SETUP_DATA_TECH_GSM)), + Integer.toString(mProfileId), + mApn.apn, mApn.user, mApn.password, + Integer.toString(authType), + protocol, msg); + } + + public void setProfileId(int profileId) { + mProfileId = profileId; + } + + public int getProfileId() { + return mProfileId; + } + + @Override + public String toString() { + return "{" + getName() + ": State=" + getCurrentState().getName() + + " apnSetting=" + mApn + " apnList= " + mApnList + " RefCount=" + mRefCount + + " cid=" + cid + " create=" + createTime + " lastFail=" + lastFailTime + + " lastFailCause=" + lastFailCause + "}"; + } + + @Override + protected boolean isDnsOk(String[] domainNameServers) { + if (NULL_IP.equals(domainNameServers[0]) && NULL_IP.equals(domainNameServers[1]) + && !phone.isDnsCheckDisabled()) { + // Work around a race condition where QMI does not fill in DNS: + // Deactivate PDP and let DataConnectionTracker retry. + // Do not apply the race condition workaround for MMS APN + // if Proxy is an IP-address. + // Otherwise, the default APN will not be restored anymore. + if (!mApn.types[0].equals(PhoneConstants.APN_TYPE_MMS) + || !isIpAddress(mApn.mmsProxy)) { + log(String.format( + "isDnsOk: return false apn.types[0]=%s APN_TYPE_MMS=%s isIpAddress(%s)=%s", + mApn.types[0], PhoneConstants.APN_TYPE_MMS, mApn.mmsProxy, + isIpAddress(mApn.mmsProxy))); + return false; + } + } + return true; + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[" + getName() + "] " + s); + } + + private boolean isIpAddress(String address) { + if (address == null) return false; + + return Patterns.IP_ADDRESS.matcher(address).matches(); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GsmDataConnection extends:"); + super.dump(fd, pw, args); + pw.println(" mProfileId=" + mProfileId); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmDataConnectionTracker.java b/src/java/com/android/internal/telephony/gsm/GsmDataConnectionTracker.java new file mode 100644 index 0000000..016513c --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmDataConnectionTracker.java @@ -0,0 +1,2642 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.LinkCapabilities; +import android.net.LinkProperties; +import android.net.LinkProperties.CompareResult; +import android.net.NetworkConfig; +import android.net.NetworkUtils; +import android.net.ProxyProperties; +import android.net.TrafficStats; +import android.net.Uri; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Telephony; +import android.telephony.CellLocation; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.telephony.cdma.CdmaCellLocation; +import android.telephony.gsm.GsmCellLocation; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; + +import com.android.internal.telephony.ApnContext; +import com.android.internal.telephony.ApnSetting; +import com.android.internal.telephony.DataCallState; +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.DataConnection.FailCause; +import com.android.internal.telephony.DataConnection.UpdateLinkPropertyResult; +import com.android.internal.telephony.DataConnectionAc; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.DctConstants; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.RetryManager; +import com.android.internal.util.AsyncChannel; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@hide} + */ +public final class GsmDataConnectionTracker extends DataConnectionTracker { + protected final String LOG_TAG = "GSM"; + private static final boolean RADIO_TESTS = false; + + /** + * Handles changes to the APN db. + */ + private class ApnChangeObserver extends ContentObserver { + public ApnChangeObserver () { + super(mDataConnectionTracker); + } + + @Override + public void onChange(boolean selfChange) { + sendMessage(obtainMessage(DctConstants.EVENT_APN_CHANGED)); + } + } + + //***** Instance Variables + + private boolean mReregisterOnReconnectFailure = false; + private ContentResolver mResolver; + + // Recovery action taken in case of data stall + private static class RecoveryAction { + public static final int GET_DATA_CALL_LIST = 0; + public static final int CLEANUP = 1; + public static final int REREGISTER = 2; + public static final int RADIO_RESTART = 3; + public static final int RADIO_RESTART_WITH_PROP = 4; + + private static boolean isAggressiveRecovery(int value) { + return ((value == RecoveryAction.CLEANUP) || + (value == RecoveryAction.REREGISTER) || + (value == RecoveryAction.RADIO_RESTART) || + (value == RecoveryAction.RADIO_RESTART_WITH_PROP)); + } + } + + public int getRecoveryAction() { + int action = Settings.System.getInt(mPhone.getContext().getContentResolver(), + "radio.data.stall.recovery.action", RecoveryAction.GET_DATA_CALL_LIST); + if (VDBG) log("getRecoveryAction: " + action); + return action; + } + public void putRecoveryAction(int action) { + Settings.System.putInt(mPhone.getContext().getContentResolver(), + "radio.data.stall.recovery.action", action); + if (VDBG) log("putRecoveryAction: " + action); + } + + //***** Constants + + private static final int POLL_PDP_MILLIS = 5 * 1000; + + private static final String INTENT_RECONNECT_ALARM = + "com.android.internal.telephony.gprs-reconnect"; + private static final String INTENT_RECONNECT_ALARM_EXTRA_TYPE = "reconnect_alarm_extra_type"; + private static final String INTENT_RECONNECT_ALARM_EXTRA_RETRY_COUNT = + "reconnect_alaram_extra_retry_count"; + + private static final String INTENT_DATA_STALL_ALARM = + "com.android.internal.telephony.gprs-data-stall"; + + static final Uri PREFERAPN_NO_UPDATE_URI = + Uri.parse("content://telephony/carriers/preferapn_no_update"); + static final String APN_ID = "apn_id"; + private boolean canSetPreferApn = false; + + private static final boolean DATA_STALL_SUSPECTED = true; + private static final boolean DATA_STALL_NOT_SUSPECTED = false; + + @Override + protected void onActionIntentReconnectAlarm(Intent intent) { + String reason = intent.getStringExtra(INTENT_RECONNECT_ALARM_EXTRA_REASON); + int connectionId = intent.getIntExtra(INTENT_RECONNECT_ALARM_EXTRA_TYPE, -1); + int retryCount = intent.getIntExtra(INTENT_RECONNECT_ALARM_EXTRA_RETRY_COUNT, 0); + + DataConnectionAc dcac= mDataConnectionAsyncChannels.get(connectionId); + + if (DBG) { + log("onActionIntentReconnectAlarm: mState=" + mState + " reason=" + reason + + " connectionId=" + connectionId + " retryCount=" + retryCount); + } + + if (dcac != null) { + for (ApnContext apnContext : dcac.getApnListSync()) { + apnContext.setDataConnectionAc(null); + apnContext.setDataConnection(null); + apnContext.setReason(reason); + apnContext.setRetryCount(retryCount); + if (apnContext.getState() ==DctConstants.State.FAILED) { + apnContext.setState(DctConstants.State.IDLE); + } + sendMessage(obtainMessage(DctConstants.EVENT_TRY_SETUP_DATA, apnContext)); + } + // Alram had expired. Clear pending intent recorded on the DataConnection. + dcac.setReconnectIntentSync(null); + } + } + + /** Watches for changes to the APN db. */ + private ApnChangeObserver mApnObserver; + + //***** Constructor + + public GsmDataConnectionTracker(PhoneBase p) { + super(p); + if (DBG) log("GsmDCT.constructor"); + p.mCM.registerForAvailable (this, DctConstants.EVENT_RADIO_AVAILABLE, null); + p.mCM.registerForOffOrNotAvailable(this, DctConstants.EVENT_RADIO_OFF_OR_NOT_AVAILABLE, + null); + p.mIccRecords.registerForRecordsLoaded(this, DctConstants.EVENT_RECORDS_LOADED, null); + p.mCM.registerForDataNetworkStateChanged (this, DctConstants.EVENT_DATA_STATE_CHANGED, + null); + p.getCallTracker().registerForVoiceCallEnded (this, DctConstants.EVENT_VOICE_CALL_ENDED, + null); + p.getCallTracker().registerForVoiceCallStarted (this, DctConstants.EVENT_VOICE_CALL_STARTED, + null); + p.getServiceStateTracker().registerForDataConnectionAttached(this, + DctConstants.EVENT_DATA_CONNECTION_ATTACHED, null); + p.getServiceStateTracker().registerForDataConnectionDetached(this, + DctConstants.EVENT_DATA_CONNECTION_DETACHED, null); + p.getServiceStateTracker().registerForRoamingOn(this, DctConstants.EVENT_ROAMING_ON, null); + p.getServiceStateTracker().registerForRoamingOff(this, DctConstants.EVENT_ROAMING_OFF, + null); + p.getServiceStateTracker().registerForPsRestrictedEnabled(this, + DctConstants.EVENT_PS_RESTRICT_ENABLED, null); + p.getServiceStateTracker().registerForPsRestrictedDisabled(this, + DctConstants.EVENT_PS_RESTRICT_DISABLED, null); + + // install reconnect intent filter for this data connection. + IntentFilter filter = new IntentFilter(); + filter.addAction(INTENT_DATA_STALL_ALARM); + p.getContext().registerReceiver(mIntentReceiver, filter, null, p); + + mDataConnectionTracker = this; + mResolver = mPhone.getContext().getContentResolver(); + + mApnObserver = new ApnChangeObserver(); + p.getContext().getContentResolver().registerContentObserver( + Telephony.Carriers.CONTENT_URI, true, mApnObserver); + + initApnContextsAndDataConnection(); + broadcastMessenger(); + } + + @Override + public void dispose() { + if (DBG) log("GsmDCT.dispose"); + cleanUpAllConnections(false, null); + + super.dispose(); + + //Unregister for all events + mPhone.mCM.unregisterForAvailable(this); + mPhone.mCM.unregisterForOffOrNotAvailable(this); + mPhone.mIccRecords.unregisterForRecordsLoaded(this); + mPhone.mCM.unregisterForDataNetworkStateChanged(this); + mPhone.getCallTracker().unregisterForVoiceCallEnded(this); + mPhone.getCallTracker().unregisterForVoiceCallStarted(this); + mPhone.getServiceStateTracker().unregisterForDataConnectionAttached(this); + mPhone.getServiceStateTracker().unregisterForDataConnectionDetached(this); + mPhone.getServiceStateTracker().unregisterForRoamingOn(this); + mPhone.getServiceStateTracker().unregisterForRoamingOff(this); + mPhone.getServiceStateTracker().unregisterForPsRestrictedEnabled(this); + mPhone.getServiceStateTracker().unregisterForPsRestrictedDisabled(this); + + mPhone.getContext().getContentResolver().unregisterContentObserver(this.mApnObserver); + mApnContexts.clear(); + + destroyDataConnections(); + } + + @Override + public boolean isApnTypeActive(String type) { + ApnContext apnContext = mApnContexts.get(type); + if (apnContext == null) return false; + + return (apnContext.getDataConnection() != null); + } + + @Override + protected boolean isDataPossible(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext == null) { + return false; + } + boolean apnContextIsEnabled = apnContext.isEnabled(); + DctConstants.State apnContextState = apnContext.getState(); + boolean apnTypePossible = !(apnContextIsEnabled && + (apnContextState == DctConstants.State.FAILED)); + boolean dataAllowed = isDataAllowed(); + boolean possible = dataAllowed && apnTypePossible; + + if (DBG) { + log(String.format("isDataPossible(%s): possible=%b isDataAllowed=%b " + + "apnTypePossible=%b apnContextisEnabled=%b apnContextState()=%s", + apnType, possible, dataAllowed, apnTypePossible, + apnContextIsEnabled, apnContextState)); + } + return possible; + } + + @Override + protected void finalize() { + if(DBG) log("finalize"); + } + + @Override + protected String getActionIntentReconnectAlarm() { + return INTENT_RECONNECT_ALARM; + } + + @Override + protected String getActionIntentDataStallAlarm() { + return INTENT_DATA_STALL_ALARM; + } + + private ApnContext addApnContext(String type) { + ApnContext apnContext = new ApnContext(type, LOG_TAG); + apnContext.setDependencyMet(false); + mApnContexts.put(type, apnContext); + return apnContext; + } + + protected void initApnContextsAndDataConnection() { + boolean defaultEnabled = SystemProperties.getBoolean(DEFALUT_DATA_ON_BOOT_PROP, true); + // Load device network attributes from resources + String[] networkConfigStrings = mPhone.getContext().getResources().getStringArray( + com.android.internal.R.array.networkAttributes); + for (String networkConfigString : networkConfigStrings) { + NetworkConfig networkConfig = new NetworkConfig(networkConfigString); + ApnContext apnContext = null; + + switch (networkConfig.type) { + case ConnectivityManager.TYPE_MOBILE: + apnContext = addApnContext(PhoneConstants.APN_TYPE_DEFAULT); + apnContext.setEnabled(defaultEnabled); + break; + case ConnectivityManager.TYPE_MOBILE_MMS: + apnContext = addApnContext(PhoneConstants.APN_TYPE_MMS); + break; + case ConnectivityManager.TYPE_MOBILE_SUPL: + apnContext = addApnContext(PhoneConstants.APN_TYPE_SUPL); + break; + case ConnectivityManager.TYPE_MOBILE_DUN: + apnContext = addApnContext(PhoneConstants.APN_TYPE_DUN); + break; + case ConnectivityManager.TYPE_MOBILE_HIPRI: + apnContext = addApnContext(PhoneConstants.APN_TYPE_HIPRI); + ApnContext defaultContext = mApnContexts.get(PhoneConstants.APN_TYPE_DEFAULT); + if (defaultContext != null) { + applyNewState(apnContext, apnContext.isEnabled(), + defaultContext.getDependencyMet()); + } else { + // the default will set the hipri dep-met when it is created + } + continue; + case ConnectivityManager.TYPE_MOBILE_FOTA: + apnContext = addApnContext(PhoneConstants.APN_TYPE_FOTA); + break; + case ConnectivityManager.TYPE_MOBILE_IMS: + apnContext = addApnContext(PhoneConstants.APN_TYPE_IMS); + break; + case ConnectivityManager.TYPE_MOBILE_CBS: + apnContext = addApnContext(PhoneConstants.APN_TYPE_CBS); + break; + default: + // skip unknown types + continue; + } + if (apnContext != null) { + // set the prop, but also apply the newly set enabled and dependency values + onSetDependencyMet(apnContext.getApnType(), networkConfig.dependencyMet); + } + } + } + + @Override + protected LinkProperties getLinkProperties(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext != null) { + DataConnectionAc dcac = apnContext.getDataConnectionAc(); + if (dcac != null) { + if (DBG) log("return link properites for " + apnType); + return dcac.getLinkPropertiesSync(); + } + } + if (DBG) log("return new LinkProperties"); + return new LinkProperties(); + } + + @Override + protected LinkCapabilities getLinkCapabilities(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext!=null) { + DataConnectionAc dataConnectionAc = apnContext.getDataConnectionAc(); + if (dataConnectionAc != null) { + if (DBG) log("get active pdp is not null, return link Capabilities for " + apnType); + return dataConnectionAc.getLinkCapabilitiesSync(); + } + } + if (DBG) log("return new LinkCapabilities"); + return new LinkCapabilities(); + } + + @Override + // Return all active apn types + public String[] getActiveApnTypes() { + if (DBG) log("get all active apn types"); + ArrayList<String> result = new ArrayList<String>(); + + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.isReady()) { + result.add(apnContext.getApnType()); + } + } + + return (String[])result.toArray(new String[0]); + } + + @Override + // Return active apn of specific apn type + public String getActiveApnString(String apnType) { + if (DBG) log( "get active apn string for type:" + apnType); + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext != null) { + ApnSetting apnSetting = apnContext.getApnSetting(); + if (apnSetting != null) { + return apnSetting.apn; + } + } + return null; + } + + @Override + public boolean isApnTypeEnabled(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext == null) { + return false; + } + return apnContext.isEnabled(); + } + + @Override + protected void setState(DctConstants.State s) { + if (DBG) log("setState should not be used in GSM" + s); + } + + // Return state of specific apn type + @Override + public DctConstants.State getState(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext != null) { + return apnContext.getState(); + } + return DctConstants.State.FAILED; + } + + // Return state of overall + public DctConstants.State getOverallState() { + boolean isConnecting = false; + boolean isFailed = true; // All enabled Apns should be FAILED. + boolean isAnyEnabled = false; + + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.isEnabled()) { + isAnyEnabled = true; + switch (apnContext.getState()) { + case CONNECTED: + case DISCONNECTING: + if (DBG) log("overall state is CONNECTED"); + return DctConstants.State.CONNECTED; + case CONNECTING: + case INITING: + isConnecting = true; + isFailed = false; + break; + case IDLE: + case SCANNING: + isFailed = false; + break; + } + } + } + + if (!isAnyEnabled) { // Nothing enabled. return IDLE. + if (DBG) log( "overall state is IDLE"); + return DctConstants.State.IDLE; + } + + if (isConnecting) { + if (DBG) log( "overall state is CONNECTING"); + return DctConstants.State.CONNECTING; + } else if (!isFailed) { + if (DBG) log( "overall state is IDLE"); + return DctConstants.State.IDLE; + } else { + if (DBG) log( "overall state is FAILED"); + return DctConstants.State.FAILED; + } + } + + /** + * Ensure that we are connected to an APN of the specified type. + * + * @param type the APN type + * @return Success is indicated by {@code PhoneConstants.APN_ALREADY_ACTIVE} or + * {@code PhoneConstants.APN_REQUEST_STARTED}. In the latter case, a + * broadcast will be sent by the ConnectivityManager when a + * connection to the APN has been established. + */ + @Override + public synchronized int enableApnType(String apnType) { + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext == null || !isApnTypeAvailable(apnType)) { + if (DBG) log("enableApnType: " + apnType + " is type not available"); + return PhoneConstants.APN_TYPE_NOT_AVAILABLE; + } + + // If already active, return + if (DBG) log("enableApnType: " + apnType + " mState(" + apnContext.getState() + ")"); + + if (apnContext.getState() == DctConstants.State.CONNECTED) { + if (DBG) log("enableApnType: return APN_ALREADY_ACTIVE"); + return PhoneConstants.APN_ALREADY_ACTIVE; + } + setEnabled(apnTypeToId(apnType), true); + if (DBG) { + log("enableApnType: new apn request for type " + apnType + + " return APN_REQUEST_STARTED"); + } + return PhoneConstants.APN_REQUEST_STARTED; + } + + // A new APN has gone active and needs to send events to catch up with the + // current condition + private void notifyApnIdUpToCurrent(String reason, ApnContext apnContext, String type) { + switch (apnContext.getState()) { + case IDLE: + case INITING: + break; + case CONNECTING: + case SCANNING: + mPhone.notifyDataConnection(reason, type, PhoneConstants.DataState.CONNECTING); + break; + case CONNECTED: + case DISCONNECTING: + mPhone.notifyDataConnection(reason, type, PhoneConstants.DataState.CONNECTING); + mPhone.notifyDataConnection(reason, type, PhoneConstants.DataState.CONNECTED); + break; + } + } + + @Override + public synchronized int disableApnType(String type) { + if (DBG) log("disableApnType:" + type); + ApnContext apnContext = mApnContexts.get(type); + + if (apnContext != null) { + setEnabled(apnTypeToId(type), false); + if (apnContext.getState() != DctConstants.State.IDLE && apnContext.getState() + != DctConstants.State.FAILED) { + if (DBG) log("diableApnType: return APN_REQUEST_STARTED"); + return PhoneConstants.APN_REQUEST_STARTED; + } else { + if (DBG) log("disableApnType: return APN_ALREADY_INACTIVE"); + return PhoneConstants.APN_ALREADY_INACTIVE; + } + + } else { + if (DBG) { + log("disableApnType: no apn context was found, return APN_REQUEST_FAILED"); + } + return PhoneConstants.APN_REQUEST_FAILED; + } + } + + @Override + protected boolean isApnTypeAvailable(String type) { + if (type.equals(PhoneConstants.APN_TYPE_DUN) && fetchDunApn() != null) { + return true; + } + + if (mAllApns != null) { + for (ApnSetting apn : mAllApns) { + if (apn.canHandleType(type)) { + return true; + } + } + } + return false; + } + + /** + * Report on whether data connectivity is enabled for any APN. + * @return {@code false} if data connectivity has been explicitly disabled, + * {@code true} otherwise. + */ + @Override + public boolean getAnyDataEnabled() { + synchronized (mDataEnabledLock) { + if (!(mInternalDataEnabled && mUserDataEnabled && sPolicyDataEnabled)) return false; + for (ApnContext apnContext : mApnContexts.values()) { + // Make sure we dont have a context that going down + // and is explicitly disabled. + if (isDataAllowed(apnContext)) { + return true; + } + } + return false; + } + } + + private boolean isDataAllowed(ApnContext apnContext) { + return apnContext.isReady() && isDataAllowed(); + } + + //****** Called from ServiceStateTracker + /** + * Invoked when ServiceStateTracker observes a transition from GPRS + * attach to detach. + */ + protected void onDataConnectionDetached() { + /* + * We presently believe it is unnecessary to tear down the PDP context + * when GPRS detaches, but we should stop the network polling. + */ + if (DBG) log ("onDataConnectionDetached: stop polling and notify detached"); + stopNetStatPoll(); + stopDataStallAlarm(); + notifyDataConnection(Phone.REASON_DATA_DETACHED); + } + + private void onDataConnectionAttached() { + if (DBG) log("onDataConnectionAttached"); + if (getOverallState() == DctConstants.State.CONNECTED) { + if (DBG) log("onDataConnectionAttached: start polling notify attached"); + startNetStatPoll(); + startDataStallAlarm(DATA_STALL_NOT_SUSPECTED); + notifyDataConnection(Phone.REASON_DATA_ATTACHED); + } else { + // update APN availability so that APN can be enabled. + notifyOffApnsOfAvailability(Phone.REASON_DATA_ATTACHED); + } + + setupDataOnReadyApns(Phone.REASON_DATA_ATTACHED); + } + + @Override + protected boolean isDataAllowed() { + final boolean internalDataEnabled; + synchronized (mDataEnabledLock) { + internalDataEnabled = mInternalDataEnabled; + } + + int gprsState = mPhone.getServiceStateTracker().getCurrentDataConnectionState(); + boolean desiredPowerState = mPhone.getServiceStateTracker().getDesiredPowerState(); + + boolean allowed = + (gprsState == ServiceState.STATE_IN_SERVICE || mAutoAttachOnCreation) && + mPhone.mIccRecords.getRecordsLoaded() && + (mPhone.getState() == PhoneConstants.State.IDLE || + mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) && + internalDataEnabled && + (!mPhone.getServiceState().getRoaming() || getDataOnRoamingEnabled()) && + !mIsPsRestricted && + desiredPowerState; + if (!allowed && DBG) { + String reason = ""; + if (!((gprsState == ServiceState.STATE_IN_SERVICE) || mAutoAttachOnCreation)) { + reason += " - gprs= " + gprsState; + } + if (!mPhone.mIccRecords.getRecordsLoaded()) reason += " - SIM not loaded"; + if (mPhone.getState() != PhoneConstants.State.IDLE && + !mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) { + reason += " - PhoneState= " + mPhone.getState(); + reason += " - Concurrent voice and data not allowed"; + } + if (!internalDataEnabled) reason += " - mInternalDataEnabled= false"; + if (mPhone.getServiceState().getRoaming() && !getDataOnRoamingEnabled()) { + reason += " - Roaming and data roaming not enabled"; + } + if (mIsPsRestricted) reason += " - mIsPsRestricted= true"; + if (!desiredPowerState) reason += " - desiredPowerState= false"; + if (DBG) log("isDataAllowed: not allowed due to" + reason); + } + return allowed; + } + + private void setupDataOnReadyApns(String reason) { + // Stop reconnect alarms on all data connections pending + // retry. Reset ApnContext state to IDLE. + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + if (dcac.getReconnectIntentSync() != null) { + cancelReconnectAlarm(dcac); + } + // update retry config for existing calls to match up + // ones for the new RAT. + if (dcac.dataConnection != null) { + Collection<ApnContext> apns = dcac.getApnListSync(); + + boolean hasDefault = false; + for (ApnContext apnContext : apns) { + if (apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT)) { + hasDefault = true; + break; + } + } + configureRetry(dcac.dataConnection, hasDefault, 0); + } + } + + // Be sure retry counts for Apncontexts and DC's are sync'd. + // When DCT/ApnContexts are refactored and we cleanup retrying + // this won't be needed. + resetAllRetryCounts(); + + // Only check for default APN state + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.getState() == DctConstants.State.FAILED) { + // By this time, alarms for all failed Apns + // should be stopped if any. + // Make sure to set the state back to IDLE + // so that setup data can happen. + apnContext.setState(DctConstants.State.IDLE); + } + if (apnContext.isReady()) { + if (apnContext.getState() == DctConstants.State.IDLE) { + apnContext.setReason(reason); + trySetupData(apnContext); + } + } + } + } + + private boolean trySetupData(String reason, String type) { + if (DBG) { + log("trySetupData: " + type + " due to " + (reason == null ? "(unspecified)" : reason) + + " isPsRestricted=" + mIsPsRestricted); + } + + if (type == null) { + type = PhoneConstants.APN_TYPE_DEFAULT; + } + + ApnContext apnContext = mApnContexts.get(type); + + if (apnContext == null ){ + if (DBG) log("trySetupData new apn context for type:" + type); + apnContext = new ApnContext(type, LOG_TAG); + mApnContexts.put(type, apnContext); + } + apnContext.setReason(reason); + + return trySetupData(apnContext); + } + + private boolean trySetupData(ApnContext apnContext) { + if (DBG) { + log("trySetupData for type:" + apnContext.getApnType() + + " due to " + apnContext.getReason()); + log("trySetupData with mIsPsRestricted=" + mIsPsRestricted); + } + + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + apnContext.setState(DctConstants.State.CONNECTED); + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + + log("trySetupData: (fix?) We're on the simulator; assuming data is connected"); + return true; + } + + boolean desiredPowerState = mPhone.getServiceStateTracker().getDesiredPowerState(); + + if ((apnContext.getState() == DctConstants.State.IDLE || + apnContext.getState() == DctConstants.State.SCANNING) && + isDataAllowed(apnContext) && getAnyDataEnabled() && !isEmergency()) { + + if (apnContext.getState() == DctConstants.State.IDLE) { + ArrayList<ApnSetting> waitingApns = buildWaitingApns(apnContext.getApnType()); + if (waitingApns.isEmpty()) { + if (DBG) log("trySetupData: No APN found"); + notifyNoData(GsmDataConnection.FailCause.MISSING_UNKNOWN_APN, apnContext); + notifyOffApnsOfAvailability(apnContext.getReason()); + return false; + } else { + apnContext.setWaitingApns(waitingApns); + if (DBG) { + log ("trySetupData: Create from mAllApns : " + apnListToString(mAllApns)); + } + } + } + + if (DBG) { + log ("Setup watingApns : " + apnListToString(apnContext.getWaitingApns())); + } + // apnContext.setReason(apnContext.getReason()); + boolean retValue = setupData(apnContext); + notifyOffApnsOfAvailability(apnContext.getReason()); + return retValue; + } else { + // TODO: check the condition. + if (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT) + && (apnContext.getState() == DctConstants.State.IDLE + || apnContext.getState() == DctConstants.State.SCANNING)) + mPhone.notifyDataConnectionFailed(apnContext.getReason(), apnContext.getApnType()); + notifyOffApnsOfAvailability(apnContext.getReason()); + return false; + } + } + + @Override + // Disabled apn's still need avail/unavail notificiations - send them out + protected void notifyOffApnsOfAvailability(String reason) { + for (ApnContext apnContext : mApnContexts.values()) { + if (!apnContext.isReady()) { + if (DBG) log("notifyOffApnOfAvailability type:" + apnContext.getApnType()); + mPhone.notifyDataConnection(reason != null ? reason : apnContext.getReason(), + apnContext.getApnType(), + PhoneConstants.DataState.DISCONNECTED); + } else { + if (DBG) { + log("notifyOffApnsOfAvailability skipped apn due to isReady==false: " + + apnContext.toString()); + } + } + } + } + + /** + * If tearDown is true, this only tears down a CONNECTED session. Presently, + * there is no mechanism for abandoning an INITING/CONNECTING session, + * but would likely involve cancelling pending async requests or + * setting a flag or new state to ignore them when they came in + * @param tearDown true if the underlying GsmDataConnection should be + * disconnected. + * @param reason reason for the clean up. + */ + protected void cleanUpAllConnections(boolean tearDown, String reason) { + if (DBG) log("cleanUpAllConnections: tearDown=" + tearDown + " reason=" + reason); + + for (ApnContext apnContext : mApnContexts.values()) { + apnContext.setReason(reason); + cleanUpConnection(tearDown, apnContext); + } + + stopNetStatPoll(); + stopDataStallAlarm(); + + // TODO: Do we need mRequestedApnType? + mRequestedApnType = PhoneConstants.APN_TYPE_DEFAULT; + } + + /** + * Cleanup all connections. + * + * TODO: Cleanup only a specified connection passed as a parameter. + * Also, make sure when you clean up a conn, if it is last apply + * logic as though it is cleanupAllConnections + * + * @param tearDown true if the underlying DataConnection should be disconnected. + * @param reason for the clean up. + */ + + @Override + protected void onCleanUpAllConnections(String cause) { + cleanUpAllConnections(true, cause); + } + + private void cleanUpConnection(boolean tearDown, ApnContext apnContext) { + + if (apnContext == null) { + if (DBG) log("cleanUpConnection: apn context is null"); + return; + } + + DataConnectionAc dcac = apnContext.getDataConnectionAc(); + if (DBG) { + log("cleanUpConnection: E tearDown=" + tearDown + " reason=" + apnContext.getReason() + + " apnContext=" + apnContext); + } + if (tearDown) { + if (apnContext.isDisconnected()) { + // The request is tearDown and but ApnContext is not connected. + // If apnContext is not enabled anymore, break the linkage to the DCAC/DC. + apnContext.setState(DctConstants.State.IDLE); + if (!apnContext.isReady()) { + apnContext.setDataConnection(null); + apnContext.setDataConnectionAc(null); + } + } else { + // Connection is still there. Try to clean up. + if (dcac != null) { + if (apnContext.getState() != DctConstants.State.DISCONNECTING) { + boolean disconnectAll = false; + if (PhoneConstants.APN_TYPE_DUN.equals(apnContext.getApnType())) { + ApnSetting dunSetting = fetchDunApn(); + if (dunSetting != null && + dunSetting.equals(apnContext.getApnSetting())) { + if (DBG) log("tearing down dedicated DUN connection"); + // we need to tear it down - we brought it up just for dun and + // other people are camped on it and now dun is done. We need + // to stop using it and let the normal apn list get used to find + // connections for the remaining desired connections + disconnectAll = true; + } + } + if (DBG) { + log("cleanUpConnection: tearing down" + (disconnectAll ? " all" :"")); + } + Message msg = obtainMessage(DctConstants.EVENT_DISCONNECT_DONE, apnContext); + if (disconnectAll) { + apnContext.getDataConnection().tearDownAll(apnContext.getReason(), msg); + } else { + apnContext.getDataConnection().tearDown(apnContext.getReason(), msg); + } + apnContext.setState(DctConstants.State.DISCONNECTING); + } + } else { + // apn is connected but no reference to dcac. + // Should not be happen, but reset the state in case. + apnContext.setState(DctConstants.State.IDLE); + mPhone.notifyDataConnection(apnContext.getReason(), + apnContext.getApnType()); + } + } + } else { + // force clean up the data connection. + if (dcac != null) dcac.resetSync(); + apnContext.setState(DctConstants.State.IDLE); + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + apnContext.setDataConnection(null); + apnContext.setDataConnectionAc(null); + } + + // make sure reconnection alarm is cleaned up if there is no ApnContext + // associated to the connection. + if (dcac != null) { + Collection<ApnContext> apnList = dcac.getApnListSync(); + if (apnList.isEmpty()) { + cancelReconnectAlarm(dcac); + } + } + if (DBG) { + log("cleanUpConnection: X tearDown=" + tearDown + " reason=" + apnContext.getReason() + + " apnContext=" + apnContext + " dc=" + apnContext.getDataConnection()); + } + } + + /** + * Cancels the alarm associated with DCAC. + * + * @param DataConnectionAc on which the alarm should be stopped. + */ + private void cancelReconnectAlarm(DataConnectionAc dcac) { + if (dcac == null) return; + + PendingIntent intent = dcac.getReconnectIntentSync(); + + if (intent != null) { + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + am.cancel(intent); + dcac.setReconnectIntentSync(null); + } + } + + /** + * @param types comma delimited list of APN types + * @return array of APN types + */ + private String[] parseTypes(String types) { + String[] result; + // If unset, set to DEFAULT. + if (types == null || types.equals("")) { + result = new String[1]; + result[0] = PhoneConstants.APN_TYPE_ALL; + } else { + result = types.split(","); + } + return result; + } + + private ArrayList<ApnSetting> createApnList(Cursor cursor) { + ArrayList<ApnSetting> result = new ArrayList<ApnSetting>(); + if (cursor.moveToFirst()) { + do { + String[] types = parseTypes( + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE))); + ApnSetting apn = new ApnSetting( + cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NUMERIC)), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NAME)), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.APN)), + NetworkUtils.trimV4AddrZeros( + cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.Carriers.PROXY))), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PORT)), + NetworkUtils.trimV4AddrZeros( + cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSC))), + NetworkUtils.trimV4AddrZeros( + cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPROXY))), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPORT)), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.USER)), + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PASSWORD)), + cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.AUTH_TYPE)), + types, + cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROTOCOL)), + cursor.getString(cursor.getColumnIndexOrThrow( + Telephony.Carriers.ROAMING_PROTOCOL)), + cursor.getInt(cursor.getColumnIndexOrThrow( + Telephony.Carriers.CARRIER_ENABLED)) == 1, + cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.BEARER))); + result.add(apn); + } while (cursor.moveToNext()); + } + if (DBG) log("createApnList: X result=" + result); + return result; + } + + private boolean dataConnectionNotInUse(DataConnectionAc dcac) { + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.getDataConnectionAc() == dcac) return false; + } + return true; + } + + private GsmDataConnection findFreeDataConnection() { + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + if (dcac.isInactiveSync() && dataConnectionNotInUse(dcac)) { + DataConnection dc = dcac.dataConnection; + if (DBG) { + log("findFreeDataConnection: found free GsmDataConnection=" + + " dcac=" + dcac + " dc=" + dc); + } + return (GsmDataConnection) dc; + } + } + log("findFreeDataConnection: NO free GsmDataConnection"); + return null; + } + + protected GsmDataConnection findReadyDataConnection(ApnSetting apn) { + if (apn == null) { + return null; + } + if (DBG) { + log("findReadyDataConnection: apn string <" + apn + ">" + + " dcacs.size=" + mDataConnectionAsyncChannels.size()); + } + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + ApnSetting apnSetting = dcac.getApnSettingSync(); + if (DBG) { + log("findReadyDataConnection: dc apn string <" + + (apnSetting != null ? (apnSetting.toString()) : "null") + ">"); + } + if ((apnSetting != null) && TextUtils.equals(apnSetting.toString(), apn.toString())) { + DataConnection dc = dcac.dataConnection; + if (DBG) { + log("findReadyDataConnection: found ready GsmDataConnection=" + + " dcac=" + dcac + " dc=" + dc); + } + return (GsmDataConnection) dc; + } + } + return null; + } + + + private boolean setupData(ApnContext apnContext) { + if (DBG) log("setupData: apnContext=" + apnContext); + ApnSetting apn; + GsmDataConnection dc; + + int profileId = getApnProfileID(apnContext.getApnType()); + apn = apnContext.getNextWaitingApn(); + if (apn == null) { + if (DBG) log("setupData: return for no apn found!"); + return false; + } + + + dc = (GsmDataConnection) checkForConnectionForApnContext(apnContext); + + if (dc == null) { + dc = findReadyDataConnection(apn); + + if (dc == null) { + if (DBG) log("setupData: No ready GsmDataConnection found!"); + // TODO: When allocating you are mapping type to id. If more than 1 free, + // then could findFreeDataConnection get the wrong one?? + dc = findFreeDataConnection(); + } + + if (dc == null) { + dc = createDataConnection(); + } + + if (dc == null) { + if (DBG) log("setupData: No free GsmDataConnection found!"); + return false; + } + } else { + apn = mDataConnectionAsyncChannels.get(dc.getDataConnectionId()).getApnSettingSync(); + } + + DataConnectionAc dcac = mDataConnectionAsyncChannels.get(dc.getDataConnectionId()); + dc.setProfileId( profileId ); // assumed no connection sharing on profiled types + + int refCount = dcac.getRefCountSync(); + if (DBG) log("setupData: init dc and apnContext refCount=" + refCount); + + // configure retry count if no other Apn is using the same connection. + if (refCount == 0) { + configureRetry(dc, apn.canHandleType(PhoneConstants.APN_TYPE_DEFAULT), + apnContext.getRetryCount()); + } + apnContext.setDataConnectionAc(dcac); + apnContext.setDataConnection(dc); + + apnContext.setApnSetting(apn); + apnContext.setState(DctConstants.State.INITING); + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + // If reconnect alarm is active on this DataConnection, wait for the alarm being + // fired so that we don't disruppt data retry pattern engaged. + if (apnContext.getDataConnectionAc().getReconnectIntentSync() != null) { + if (DBG) log("setupData: data reconnection pending"); + apnContext.setState(DctConstants.State.FAILED); + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + return true; + } + + Message msg = obtainMessage(); + msg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE; + msg.obj = apnContext; + dc.bringUp(msg, apn); + + if (DBG) log("setupData: initing!"); + return true; + } + + /** + * Handles changes to the APN database. + */ + private void onApnChanged() { + DctConstants.State overallState = getOverallState(); + boolean isDisconnected = (overallState == DctConstants.State.IDLE || + overallState == DctConstants.State.FAILED); + + if (mPhone instanceof GSMPhone) { + // The "current" may no longer be valid. MMS depends on this to send properly. TBD + ((GSMPhone)mPhone).updateCurrentCarrierInProvider(); + } + + // TODO: It'd be nice to only do this if the changed entrie(s) + // match the current operator. + if (DBG) log("onApnChanged: createAllApnList and cleanUpAllConnections"); + createAllApnList(); + cleanUpAllConnections(!isDisconnected, Phone.REASON_APN_CHANGED); + if (isDisconnected) { + setupDataOnReadyApns(Phone.REASON_APN_CHANGED); + } + } + + /** + * @param cid Connection id provided from RIL. + * @return DataConnectionAc associated with specified cid. + */ + private DataConnectionAc findDataConnectionAcByCid(int cid) { + for (DataConnectionAc dcac : mDataConnectionAsyncChannels.values()) { + if (dcac.getCidSync() == cid) { + return dcac; + } + } + return null; + } + + /** + * @param dcacs Collection of DataConnectionAc reported from RIL. + * @return List of ApnContext which is connected, but is not present in + * data connection list reported from RIL. + */ + private List<ApnContext> findApnContextToClean(Collection<DataConnectionAc> dcacs) { + if (dcacs == null) return null; + + if (DBG) log("findApnContextToClean(ar): E dcacs=" + dcacs); + + ArrayList<ApnContext> list = new ArrayList<ApnContext>(); + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.getState() == DctConstants.State.CONNECTED) { + boolean found = false; + for (DataConnectionAc dcac : dcacs) { + if (dcac == apnContext.getDataConnectionAc()) { + // ApnContext holds the ref to dcac present in data call list. + found = true; + break; + } + } + if (!found) { + // ApnContext does not have dcac reported in data call list. + // Fetch all the ApnContexts that map to this dcac which are in + // INITING state too. + if (DBG) log("findApnContextToClean(ar): Connected apn not found in the list (" + + apnContext.toString() + ")"); + if (apnContext.getDataConnectionAc() != null) { + list.addAll(apnContext.getDataConnectionAc().getApnListSync()); + } else { + list.add(apnContext); + } + } + } + } + if (DBG) log("findApnContextToClean(ar): X list=" + list); + return list; + } + + /** + * @param ar is the result of RIL_REQUEST_DATA_CALL_LIST + * or RIL_UNSOL_DATA_CALL_LIST_CHANGED + */ + private void onDataStateChanged (AsyncResult ar) { + ArrayList<DataCallState> dataCallStates; + + if (DBG) log("onDataStateChanged(ar): E"); + dataCallStates = (ArrayList<DataCallState>)(ar.result); + + if (ar.exception != null) { + // This is probably "radio not available" or something + // of that sort. If so, the whole connection is going + // to come down soon anyway + if (DBG) log("onDataStateChanged(ar): exception; likely radio not available, ignore"); + return; + } + if (DBG) log("onDataStateChanged(ar): DataCallState size=" + dataCallStates.size()); + + // Create a hash map to store the dataCallState of each DataConnectionAc + HashMap<DataCallState, DataConnectionAc> dataCallStateToDcac; + dataCallStateToDcac = new HashMap<DataCallState, DataConnectionAc>(); + for (DataCallState dataCallState : dataCallStates) { + DataConnectionAc dcac = findDataConnectionAcByCid(dataCallState.cid); + + if (dcac != null) dataCallStateToDcac.put(dataCallState, dcac); + } + + // A list of apns to cleanup, those that aren't in the list we know we have to cleanup + List<ApnContext> apnsToCleanup = findApnContextToClean(dataCallStateToDcac.values()); + + // Find which connections have changed state and send a notification or cleanup + for (DataCallState newState : dataCallStates) { + DataConnectionAc dcac = dataCallStateToDcac.get(newState); + + if (dcac == null) { + loge("onDataStateChanged(ar): No associated DataConnection ignore"); + continue; + } + + // The list of apn's associated with this DataConnection + Collection<ApnContext> apns = dcac.getApnListSync(); + + // Find which ApnContexts of this DC are in the "Connected/Connecting" state. + ArrayList<ApnContext> connectedApns = new ArrayList<ApnContext>(); + for (ApnContext apnContext : apns) { + if (apnContext.getState() == DctConstants.State.CONNECTED || + apnContext.getState() == DctConstants.State.CONNECTING || + apnContext.getState() == DctConstants.State.INITING) { + connectedApns.add(apnContext); + } + } + if (connectedApns.size() == 0) { + if (DBG) log("onDataStateChanged(ar): no connected apns"); + } else { + // Determine if the connection/apnContext should be cleaned up + // or just a notification should be sent out. + if (DBG) log("onDataStateChanged(ar): Found ConnId=" + newState.cid + + " newState=" + newState.toString()); + if (newState.active == 0) { + if (DBG) { + log("onDataStateChanged(ar): inactive, cleanup apns=" + connectedApns); + } + apnsToCleanup.addAll(connectedApns); + } else { + // Its active so update the DataConnections link properties + UpdateLinkPropertyResult result = + dcac.updateLinkPropertiesDataCallStateSync(newState); + if (result.oldLp.equals(result.newLp)) { + if (DBG) log("onDataStateChanged(ar): no change"); + } else { + if (result.oldLp.isIdenticalInterfaceName(result.newLp)) { + if (! result.oldLp.isIdenticalDnses(result.newLp) || + ! result.oldLp.isIdenticalRoutes(result.newLp) || + ! result.oldLp.isIdenticalHttpProxy(result.newLp) || + ! result.oldLp.isIdenticalAddresses(result.newLp)) { + // If the same address type was removed and added we need to cleanup + CompareResult<LinkAddress> car = + result.oldLp.compareAddresses(result.newLp); + if (DBG) { + log("onDataStateChanged: oldLp=" + result.oldLp + + " newLp=" + result.newLp + " car=" + car); + } + boolean needToClean = false; + for (LinkAddress added : car.added) { + for (LinkAddress removed : car.removed) { + if (NetworkUtils.addressTypeMatches(removed.getAddress(), + added.getAddress())) { + needToClean = true; + break; + } + } + } + if (needToClean) { + if (DBG) { + log("onDataStateChanged(ar): addr change, cleanup apns=" + + connectedApns + " oldLp=" + result.oldLp + + " newLp=" + result.newLp); + } + apnsToCleanup.addAll(connectedApns); + } else { + if (DBG) log("onDataStateChanged(ar): simple change"); + for (ApnContext apnContext : connectedApns) { + mPhone.notifyDataConnection( + PhoneConstants.REASON_LINK_PROPERTIES_CHANGED, + apnContext.getApnType()); + } + } + } else { + if (DBG) { + log("onDataStateChanged(ar): no changes"); + } + } + } else { + if (DBG) { + log("onDataStateChanged(ar): interface change, cleanup apns=" + + connectedApns); + } + apnsToCleanup.addAll(connectedApns); + } + } + } + } + } + + if (apnsToCleanup.size() != 0) { + // Add an event log when the network drops PDP + int cid = getCellLocationId(); + EventLog.writeEvent(EventLogTags.PDP_NETWORK_DROP, cid, + TelephonyManager.getDefault().getNetworkType()); + } + + // Cleanup those dropped connections + if (DBG) log("onDataStateChange(ar): apnsToCleanup=" + apnsToCleanup); + for (ApnContext apnContext : apnsToCleanup) { + cleanUpConnection(true, apnContext); + } + + if (DBG) log("onDataStateChanged(ar): X"); + } + + private void notifyDefaultData(ApnContext apnContext) { + if (DBG) { + log("notifyDefaultData: type=" + apnContext.getApnType() + + ", reason:" + apnContext.getReason()); + } + apnContext.setState(DctConstants.State.CONNECTED); + // setState(DctConstants.State.CONNECTED); + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + startNetStatPoll(); + startDataStallAlarm(DATA_STALL_NOT_SUSPECTED); + // reset reconnect timer + apnContext.setRetryCount(0); + } + + // TODO: For multiple Active APNs not exactly sure how to do this. + protected void gotoIdleAndNotifyDataConnection(String reason) { + if (DBG) log("gotoIdleAndNotifyDataConnection: reason=" + reason); + notifyDataConnection(reason); + mActiveApn = null; + } + + private void resetPollStats() { + mTxPkts = -1; + mRxPkts = -1; + mNetStatPollPeriod = POLL_NETSTAT_MILLIS; + } + + private void doRecovery() { + if (getOverallState() == DctConstants.State.CONNECTED) { + // Go through a series of recovery steps, each action transitions to the next action + int recoveryAction = getRecoveryAction(); + switch (recoveryAction) { + case RecoveryAction.GET_DATA_CALL_LIST: + EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_GET_DATA_CALL_LIST, + mSentSinceLastRecv); + if (DBG) log("doRecovery() get data call list"); + mPhone.mCM.getDataCallList(obtainMessage(DctConstants.EVENT_DATA_STATE_CHANGED)); + putRecoveryAction(RecoveryAction.CLEANUP); + break; + case RecoveryAction.CLEANUP: + EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_CLEANUP, mSentSinceLastRecv); + if (DBG) log("doRecovery() cleanup all connections"); + cleanUpAllConnections(true, Phone.REASON_PDP_RESET); + putRecoveryAction(RecoveryAction.REREGISTER); + break; + case RecoveryAction.REREGISTER: + EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_REREGISTER, + mSentSinceLastRecv); + if (DBG) log("doRecovery() re-register"); + mPhone.getServiceStateTracker().reRegisterNetwork(null); + putRecoveryAction(RecoveryAction.RADIO_RESTART); + break; + case RecoveryAction.RADIO_RESTART: + EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_RADIO_RESTART, + mSentSinceLastRecv); + if (DBG) log("restarting radio"); + putRecoveryAction(RecoveryAction.RADIO_RESTART_WITH_PROP); + restartRadio(); + break; + case RecoveryAction.RADIO_RESTART_WITH_PROP: + // This is in case radio restart has not recovered the data. + // It will set an additional "gsm.radioreset" property to tell + // RIL or system to take further action. + // The implementation of hard reset recovery action is up to OEM product. + // Once gsm.radioreset property is consumed, it is expected to set back + // to false by RIL. + EventLog.writeEvent(EventLogTags.DATA_STALL_RECOVERY_RADIO_RESTART_WITH_PROP, -1); + if (DBG) log("restarting radio with gsm.radioreset to true"); + SystemProperties.set("gsm.radioreset", "true"); + // give 1 sec so property change can be notified. + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + restartRadio(); + putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST); + break; + default: + throw new RuntimeException("doRecovery: Invalid recoveryAction=" + + recoveryAction); + } + } + } + + @Override + protected void startNetStatPoll() { + if (getOverallState() == DctConstants.State.CONNECTED && mNetStatPollEnabled == false) { + if (DBG) log("startNetStatPoll"); + resetPollStats(); + mNetStatPollEnabled = true; + mPollNetStat.run(); + } + } + + @Override + protected void stopNetStatPoll() { + mNetStatPollEnabled = false; + removeCallbacks(mPollNetStat); + if (DBG) log("stopNetStatPoll"); + } + + @Override + protected void restartRadio() { + if (DBG) log("restartRadio: ************TURN OFF RADIO**************"); + cleanUpAllConnections(true, Phone.REASON_RADIO_TURNED_OFF); + mPhone.getServiceStateTracker().powerOffRadioSafely(this); + /* Note: no need to call setRadioPower(true). Assuming the desired + * radio power state is still ON (as tracked by ServiceStateTracker), + * ServiceStateTracker will call setRadioPower when it receives the + * RADIO_STATE_CHANGED notification for the power off. And if the + * desired power state has changed in the interim, we don't want to + * override it with an unconditional power on. + */ + + int reset = Integer.parseInt(SystemProperties.get("net.ppp.reset-by-timeout", "0")); + SystemProperties.set("net.ppp.reset-by-timeout", String.valueOf(reset+1)); + } + + + private void updateDataStallInfo() { + long sent, received; + + TxRxSum preTxRxSum = new TxRxSum(mDataStallTxRxSum); + mDataStallTxRxSum.updateTxRxSum(); + + if (VDBG) { + log("updateDataStallInfo: mDataStallTxRxSum=" + mDataStallTxRxSum + + " preTxRxSum=" + preTxRxSum); + } + + sent = mDataStallTxRxSum.txPkts - preTxRxSum.txPkts; + received = mDataStallTxRxSum.rxPkts - preTxRxSum.rxPkts; + + if (RADIO_TESTS) { + if (SystemProperties.getBoolean("radio.test.data.stall", false)) { + log("updateDataStallInfo: radio.test.data.stall true received = 0;"); + received = 0; + } + } + if ( sent > 0 && received > 0 ) { + if (VDBG) log("updateDataStallInfo: IN/OUT"); + mSentSinceLastRecv = 0; + putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST); + } else if (sent > 0 && received == 0) { + if (mPhone.getState() == PhoneConstants.State.IDLE) { + mSentSinceLastRecv += sent; + } else { + mSentSinceLastRecv = 0; + } + if (DBG) { + log("updateDataStallInfo: OUT sent=" + sent + + " mSentSinceLastRecv=" + mSentSinceLastRecv); + } + } else if (sent == 0 && received > 0) { + if (VDBG) log("updateDataStallInfo: IN"); + mSentSinceLastRecv = 0; + putRecoveryAction(RecoveryAction.GET_DATA_CALL_LIST); + } else { + if (VDBG) log("updateDataStallInfo: NONE"); + } + } + + @Override + protected void onDataStallAlarm(int tag) { + if (mDataStallAlarmTag != tag) { + if (DBG) { + log("onDataStallAlarm: ignore, tag=" + tag + " expecting " + mDataStallAlarmTag); + } + return; + } + updateDataStallInfo(); + + int hangWatchdogTrigger = Settings.Secure.getInt(mResolver, + Settings.Secure.PDP_WATCHDOG_TRIGGER_PACKET_COUNT, + NUMBER_SENT_PACKETS_OF_HANG); + + boolean suspectedStall = DATA_STALL_NOT_SUSPECTED; + if (mSentSinceLastRecv >= hangWatchdogTrigger) { + if (DBG) { + log("onDataStallAlarm: tag=" + tag + " do recovery action=" + getRecoveryAction()); + } + suspectedStall = DATA_STALL_SUSPECTED; + sendMessage(obtainMessage(DctConstants.EVENT_DO_RECOVERY)); + } else { + if (VDBG) { + log("onDataStallAlarm: tag=" + tag + " Sent " + String.valueOf(mSentSinceLastRecv) + + " pkts since last received, < watchdogTrigger=" + hangWatchdogTrigger); + } + } + startDataStallAlarm(suspectedStall); + } + + + private void updateDataActivity() { + long sent, received; + + DctConstants.Activity newActivity; + + TxRxSum preTxRxSum = new TxRxSum(mTxPkts, mRxPkts); + TxRxSum curTxRxSum = new TxRxSum(); + curTxRxSum.updateTxRxSum(); + mTxPkts = curTxRxSum.txPkts; + mRxPkts = curTxRxSum.rxPkts; + + if (VDBG) { + log("updateDataActivity: curTxRxSum=" + curTxRxSum + " preTxRxSum=" + preTxRxSum); + } + + if (mNetStatPollEnabled && (preTxRxSum.txPkts > 0 || preTxRxSum.rxPkts > 0)) { + sent = mTxPkts - preTxRxSum.txPkts; + received = mRxPkts - preTxRxSum.rxPkts; + + if (VDBG) log("updateDataActivity: sent=" + sent + " received=" + received); + if ( sent > 0 && received > 0 ) { + newActivity = DctConstants.Activity.DATAINANDOUT; + } else if (sent > 0 && received == 0) { + newActivity = DctConstants.Activity.DATAOUT; + } else if (sent == 0 && received > 0) { + newActivity = DctConstants.Activity.DATAIN; + } else { + newActivity = DctConstants.Activity.NONE; + } + + if (mActivity != newActivity && mIsScreenOn) { + if (VDBG) log("updateDataActivity: newActivity=" + newActivity); + mActivity = newActivity; + mPhone.notifyDataActivity(); + } + } + } + + private Runnable mPollNetStat = new Runnable() + { + @Override + public void run() { + updateDataActivity(); + + if (mIsScreenOn) { + mNetStatPollPeriod = Settings.Secure.getInt(mResolver, + Settings.Secure.PDP_WATCHDOG_POLL_INTERVAL_MS, POLL_NETSTAT_MILLIS); + } else { + mNetStatPollPeriod = Settings.Secure.getInt(mResolver, + Settings.Secure.PDP_WATCHDOG_LONG_POLL_INTERVAL_MS, + POLL_NETSTAT_SCREEN_OFF_MILLIS); + } + + if (mNetStatPollEnabled) { + mDataConnectionTracker.postDelayed(this, mNetStatPollPeriod); + } + } + }; + + /** + * Returns true if the last fail cause is something that + * seems like it deserves an error notification. + * Transient errors are ignored + */ + private boolean shouldPostNotification(GsmDataConnection.FailCause cause) { + return (cause != GsmDataConnection.FailCause.UNKNOWN); + } + + /** + * Return true if data connection need to be setup after disconnected due to + * reason. + * + * @param reason the reason why data is disconnected + * @return true if try setup data connection is need for this reason + */ + private boolean retryAfterDisconnected(String reason) { + boolean retry = true; + + if ( Phone.REASON_RADIO_TURNED_OFF.equals(reason) ) { + retry = false; + } + return retry; + } + + private void reconnectAfterFail(FailCause lastFailCauseCode, + ApnContext apnContext, int retryOverride) { + if (apnContext == null) { + loge("reconnectAfterFail: apnContext == null, impossible"); + return; + } + if (DBG) { + log("reconnectAfterFail: lastFailCause=" + lastFailCauseCode + + " retryOverride=" + retryOverride + " apnContext=" + apnContext); + } + if ((apnContext.getState() == DctConstants.State.FAILED) && + (apnContext.getDataConnection() != null)) { + if (!apnContext.getDataConnection().isRetryNeeded()) { + if (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT)) { + mPhone.notifyDataConnection(Phone.REASON_APN_FAILED, apnContext.getApnType()); + return; + } + if (mReregisterOnReconnectFailure) { + // We've re-registerd once now just retry forever. + apnContext.getDataConnection().retryForeverUsingLastTimeout(); + } else { + // Try to Re-register to the network. + if (DBG) log("reconnectAfterFail: activate failed, Reregistering to network"); + mReregisterOnReconnectFailure = true; + mPhone.getServiceStateTracker().reRegisterNetwork(null); + apnContext.setRetryCount(0); + return; + } + } + + // If retry needs to be backed off for specific case (determined by RIL/Modem) + // use the specified timer instead of pre-configured retry pattern. + int nextReconnectDelay = retryOverride; + if (nextReconnectDelay < 0) { + nextReconnectDelay = apnContext.getDataConnection().getRetryTimer(); + apnContext.getDataConnection().increaseRetryCount(); + if (DBG) { + log("reconnectAfterFail: increaseRetryCount=" + + apnContext.getDataConnection().getRetryCount() + + " nextReconnectDelay=" + nextReconnectDelay); + } + } + startAlarmForReconnect(nextReconnectDelay, apnContext); + + if (!shouldPostNotification(lastFailCauseCode)) { + if (DBG) { + log("reconnectAfterFail: NOT Posting GPRS Unavailable notification " + + "-- likely transient error"); + } + } else { + notifyNoData(lastFailCauseCode, apnContext); + } + } + } + + private void startAlarmForReconnect(int delay, ApnContext apnContext) { + + DataConnectionAc dcac = apnContext.getDataConnectionAc(); + + if ((dcac == null) || (dcac.dataConnection == null)) { + // should not happen, but just in case. + loge("startAlarmForReconnect: null dcac or dc."); + return; + } + + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + + Intent intent = new Intent(INTENT_RECONNECT_ALARM + '.' + + dcac.dataConnection.getDataConnectionId()); + String reason = apnContext.getReason(); + intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_REASON, reason); + int connectionId = dcac.dataConnection.getDataConnectionId(); + intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_TYPE, connectionId); + + // TODO: Until a real fix is created, which probably entails pushing + // retires into the DC itself, this fix gets the retry count and + // puts it in the reconnect alarm. When the reconnect alarm fires + // onActionIntentReconnectAlarm is called which will use the value saved + // here and save it in the ApnContext and send the EVENT_CONNECT message + // which invokes setupData. Then setupData will use the value in the ApnContext + // and to tell the DC to set the retry count in the retry manager. + int retryCount = dcac.dataConnection.getRetryCount(); + intent.putExtra(INTENT_RECONNECT_ALARM_EXTRA_RETRY_COUNT, retryCount); + + if (DBG) { + log("startAlarmForReconnect: next attempt in " + (delay / 1000) + "s" + + " reason='" + reason + "' connectionId=" + connectionId + + " retryCount=" + retryCount); + } + + PendingIntent alarmIntent = PendingIntent.getBroadcast (mPhone.getContext(), 0, + intent, PendingIntent.FLAG_UPDATE_CURRENT); + dcac.setReconnectIntentSync(alarmIntent); + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + delay, alarmIntent); + + } + + private void startDataStallAlarm(boolean suspectedStall) { + int nextAction = getRecoveryAction(); + int delayInMs; + + // If screen is on or data stall is currently suspected, set the alarm + // with an aggresive timeout. + if (mIsScreenOn || suspectedStall || RecoveryAction.isAggressiveRecovery(nextAction)) { + delayInMs = Settings.Secure.getInt(mResolver, + Settings.Secure.DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS, + DATA_STALL_ALARM_AGGRESSIVE_DELAY_IN_MS_DEFAULT); + } else { + delayInMs = Settings.Secure.getInt(mResolver, + Settings.Secure.DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS, + DATA_STALL_ALARM_NON_AGGRESSIVE_DELAY_IN_MS_DEFAULT); + } + + mDataStallAlarmTag += 1; + if (VDBG) { + log("startDataStallAlarm: tag=" + mDataStallAlarmTag + + " delay=" + (delayInMs / 1000) + "s"); + } + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + + Intent intent = new Intent(INTENT_DATA_STALL_ALARM); + intent.putExtra(DATA_STALL_ALARM_TAG_EXTRA, mDataStallAlarmTag); + mDataStallAlarmIntent = PendingIntent.getBroadcast(mPhone.getContext(), 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + delayInMs, mDataStallAlarmIntent); + } + + private void stopDataStallAlarm() { + AlarmManager am = + (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE); + + if (VDBG) { + log("stopDataStallAlarm: current tag=" + mDataStallAlarmTag + + " mDataStallAlarmIntent=" + mDataStallAlarmIntent); + } + mDataStallAlarmTag += 1; + if (mDataStallAlarmIntent != null) { + am.cancel(mDataStallAlarmIntent); + mDataStallAlarmIntent = null; + } + } + + @Override + protected void restartDataStallAlarm() { + if (isConnected() == false) return; + // To be called on screen status change. + // Do not cancel the alarm if it is set with aggressive timeout. + int nextAction = getRecoveryAction(); + + if (RecoveryAction.isAggressiveRecovery(nextAction)) { + if (DBG) log("data stall recovery action is pending. not resetting the alarm."); + return; + } + stopDataStallAlarm(); + startDataStallAlarm(DATA_STALL_NOT_SUSPECTED); + } + + private void notifyNoData(GsmDataConnection.FailCause lastFailCauseCode, + ApnContext apnContext) { + if (DBG) log( "notifyNoData: type=" + apnContext.getApnType()); + apnContext.setState(DctConstants.State.FAILED); + if (lastFailCauseCode.isPermanentFail() + && (!apnContext.getApnType().equals(PhoneConstants.APN_TYPE_DEFAULT))) { + mPhone.notifyDataConnectionFailed(apnContext.getReason(), apnContext.getApnType()); + } + } + + private void onRecordsLoaded() { + if (DBG) log("onRecordsLoaded: createAllApnList"); + createAllApnList(); + if (mPhone.mCM.getRadioState().isOn()) { + if (DBG) log("onRecordsLoaded: notifying data availability"); + notifyOffApnsOfAvailability(Phone.REASON_SIM_LOADED); + } + setupDataOnReadyApns(Phone.REASON_SIM_LOADED); + } + + @Override + protected void onSetDependencyMet(String apnType, boolean met) { + // don't allow users to tweak hipri to work around default dependency not met + if (PhoneConstants.APN_TYPE_HIPRI.equals(apnType)) return; + + ApnContext apnContext = mApnContexts.get(apnType); + if (apnContext == null) { + loge("onSetDependencyMet: ApnContext not found in onSetDependencyMet(" + + apnType + ", " + met + ")"); + return; + } + applyNewState(apnContext, apnContext.isEnabled(), met); + if (PhoneConstants.APN_TYPE_DEFAULT.equals(apnType)) { + // tie actions on default to similar actions on HIPRI regarding dependencyMet + apnContext = mApnContexts.get(PhoneConstants.APN_TYPE_HIPRI); + if (apnContext != null) applyNewState(apnContext, apnContext.isEnabled(), met); + } + } + + private void applyNewState(ApnContext apnContext, boolean enabled, boolean met) { + boolean cleanup = false; + boolean trySetup = false; + if (DBG) { + log("applyNewState(" + apnContext.getApnType() + ", " + enabled + + "(" + apnContext.isEnabled() + "), " + met + "(" + + apnContext.getDependencyMet() +"))"); + } + if (apnContext.isReady()) { + if (enabled && met) return; + if (!enabled) { + apnContext.setReason(Phone.REASON_DATA_DISABLED); + } else { + apnContext.setReason(Phone.REASON_DATA_DEPENDENCY_UNMET); + } + cleanup = true; + } else { + if (enabled && met) { + if (apnContext.isEnabled()) { + apnContext.setReason(Phone.REASON_DATA_DEPENDENCY_MET); + } else { + apnContext.setReason(Phone.REASON_DATA_ENABLED); + } + if (apnContext.getState() == DctConstants.State.FAILED) { + apnContext.setState(DctConstants.State.IDLE); + } + trySetup = true; + } + } + apnContext.setEnabled(enabled); + apnContext.setDependencyMet(met); + if (cleanup) cleanUpConnection(true, apnContext); + if (trySetup) trySetupData(apnContext); + } + + private DataConnection checkForConnectionForApnContext(ApnContext apnContext) { + // Loop through all apnContexts looking for one with a conn that satisfies this apnType + String apnType = apnContext.getApnType(); + ApnSetting dunSetting = null; + + if (PhoneConstants.APN_TYPE_DUN.equals(apnType)) { + dunSetting = fetchDunApn(); + } + + DataConnection potential = null; + for (ApnContext c : mApnContexts.values()) { + DataConnection conn = c.getDataConnection(); + if (conn != null) { + ApnSetting apnSetting = c.getApnSetting(); + if (dunSetting != null) { + if (dunSetting.equals(apnSetting)) { + switch (c.getState()) { + case CONNECTED: + if (DBG) { + log("checkForConnectionForApnContext: apnContext=" + + apnContext + " found conn=" + conn); + } + return conn; + case CONNECTING: + potential = conn; + } + } + } else if (apnSetting != null && apnSetting.canHandleType(apnType)) { + switch (c.getState()) { + case CONNECTED: + if (DBG) { + log("checkForConnectionForApnContext: apnContext=" + apnContext + + " found conn=" + conn); + } + return conn; + case CONNECTING: + potential = conn; + } + } + } + } + if (potential != null) { + if (DBG) { + log("checkForConnectionForApnContext: apnContext=" + apnContext + + " found conn=" + potential); + } + return potential; + } + + if (DBG) log("checkForConnectionForApnContext: apnContext=" + apnContext + " NO conn"); + return null; + } + + @Override + protected void onEnableApn(int apnId, int enabled) { + ApnContext apnContext = mApnContexts.get(apnIdToType(apnId)); + if (apnContext == null) { + loge("onEnableApn(" + apnId + ", " + enabled + "): NO ApnContext"); + return; + } + // TODO change our retry manager to use the appropriate numbers for the new APN + if (DBG) log("onEnableApn: apnContext=" + apnContext + " call applyNewState"); + applyNewState(apnContext, enabled == DctConstants.ENABLED, apnContext.getDependencyMet()); + } + + @Override + // TODO: We shouldnt need this. + protected boolean onTrySetupData(String reason) { + if (DBG) log("onTrySetupData: reason=" + reason); + setupDataOnReadyApns(reason); + return true; + } + + protected boolean onTrySetupData(ApnContext apnContext) { + if (DBG) log("onTrySetupData: apnContext=" + apnContext); + return trySetupData(apnContext); + } + + @Override + protected void onRoamingOff() { + if (DBG) log("onRoamingOff"); + + if (mUserDataEnabled == false) return; + + if (getDataOnRoamingEnabled() == false) { + notifyOffApnsOfAvailability(Phone.REASON_ROAMING_OFF); + setupDataOnReadyApns(Phone.REASON_ROAMING_OFF); + } else { + notifyDataConnection(Phone.REASON_ROAMING_OFF); + } + } + + @Override + protected void onRoamingOn() { + if (mUserDataEnabled == false) return; + + if (getDataOnRoamingEnabled()) { + if (DBG) log("onRoamingOn: setup data on roaming"); + setupDataOnReadyApns(Phone.REASON_ROAMING_ON); + notifyDataConnection(Phone.REASON_ROAMING_ON); + } else { + if (DBG) log("onRoamingOn: Tear down data connection on roaming."); + cleanUpAllConnections(true, Phone.REASON_ROAMING_ON); + notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON); + } + } + + @Override + protected void onRadioAvailable() { + if (DBG) log("onRadioAvailable"); + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + // setState(DctConstants.State.CONNECTED); + notifyDataConnection(null); + + log("onRadioAvailable: We're on the simulator; assuming data is connected"); + } + + if (mPhone.mIccRecords.getRecordsLoaded()) { + notifyOffApnsOfAvailability(null); + } + + if (getOverallState() != DctConstants.State.IDLE) { + cleanUpConnection(true, null); + } + } + + @Override + protected void onRadioOffOrNotAvailable() { + // Make sure our reconnect delay starts at the initial value + // next time the radio comes on + + resetAllRetryCounts(); + mReregisterOnReconnectFailure = false; + + if (mPhone.getSimulatedRadioControl() != null) { + // Assume data is connected on the simulator + // FIXME this can be improved + log("We're on the simulator; assuming radio off is meaningless"); + } else { + if (DBG) log("onRadioOffOrNotAvailable: is off and clean up all connections"); + cleanUpAllConnections(false, Phone.REASON_RADIO_TURNED_OFF); + } + notifyOffApnsOfAvailability(null); + } + + @Override + protected void onDataSetupComplete(AsyncResult ar) { + + DataConnection.FailCause cause = DataConnection.FailCause.UNKNOWN; + boolean handleError = false; + ApnContext apnContext = null; + + if(ar.userObj instanceof ApnContext){ + apnContext = (ApnContext)ar.userObj; + } else { + throw new RuntimeException("onDataSetupComplete: No apnContext"); + } + + if (isDataSetupCompleteOk(ar)) { + DataConnectionAc dcac = apnContext.getDataConnectionAc(); + + if (RADIO_TESTS) { + // Note: To change radio.test.onDSC.null.dcac from command line you need to + // adb root and adb remount and from the command line you can only change the + // value to 1 once. To change it a second time you can reboot or execute + // adb shell stop and then adb shell start. The command line to set the value is: + // adb shell sqlite3 /data/data/com.android.providers.settings/databases/settings.db "insert into system (name,value) values ('radio.test.onDSC.null.dcac', '1');" + ContentResolver cr = mPhone.getContext().getContentResolver(); + String radioTestProperty = "radio.test.onDSC.null.dcac"; + if (Settings.System.getInt(cr, radioTestProperty, 0) == 1) { + log("onDataSetupComplete: " + radioTestProperty + + " is true, set dcac to null and reset property to false"); + dcac = null; + Settings.System.putInt(cr, radioTestProperty, 0); + log("onDataSetupComplete: " + radioTestProperty + "=" + + Settings.System.getInt(mPhone.getContext().getContentResolver(), + radioTestProperty, -1)); + } + } + if (dcac == null) { + log("onDataSetupComplete: no connection to DC, handle as error"); + cause = DataConnection.FailCause.CONNECTION_TO_DATACONNECTIONAC_BROKEN; + handleError = true; + } else { + DataConnection dc = apnContext.getDataConnection(); + ApnSetting apn = apnContext.getApnSetting(); + if (DBG) { + log("onDataSetupComplete: success apn=" + (apn == null ? "unknown" : apn.apn)); + } + if (apn != null && apn.proxy != null && apn.proxy.length() != 0) { + try { + String port = apn.port; + if (TextUtils.isEmpty(port)) port = "8080"; + ProxyProperties proxy = new ProxyProperties(apn.proxy, + Integer.parseInt(port), null); + dcac.setLinkPropertiesHttpProxySync(proxy); + } catch (NumberFormatException e) { + loge("onDataSetupComplete: NumberFormatException making ProxyProperties (" + + apn.port + "): " + e); + } + } + + // everything is setup + if(TextUtils.equals(apnContext.getApnType(),PhoneConstants.APN_TYPE_DEFAULT)) { + SystemProperties.set("gsm.defaultpdpcontext.active", "true"); + if (canSetPreferApn && mPreferredApn == null) { + if (DBG) log("onDataSetupComplete: PREFERED APN is null"); + mPreferredApn = apn; + if (mPreferredApn != null) { + setPreferredApn(mPreferredApn.id); + } + } + } else { + SystemProperties.set("gsm.defaultpdpcontext.active", "false"); + } + notifyDefaultData(apnContext); + } + } else { + cause = (DataConnection.FailCause) (ar.result); + if (DBG) { + ApnSetting apn = apnContext.getApnSetting(); + log(String.format("onDataSetupComplete: error apn=%s cause=%s", + (apn == null ? "unknown" : apn.apn), cause)); + } + if (cause.isEventLoggable()) { + // Log this failure to the Event Logs. + int cid = getCellLocationId(); + EventLog.writeEvent(EventLogTags.PDP_SETUP_FAIL, + cause.ordinal(), cid, TelephonyManager.getDefault().getNetworkType()); + } + + // Count permanent failures and remove the APN we just tried + if (cause.isPermanentFail()) apnContext.decWaitingApnsPermFailCount(); + + apnContext.removeWaitingApn(apnContext.getApnSetting()); + if (DBG) { + log(String.format("onDataSetupComplete: WaitingApns.size=%d" + + " WaitingApnsPermFailureCountDown=%d", + apnContext.getWaitingApns().size(), + apnContext.getWaitingApnsPermFailCount())); + } + handleError = true; + } + + if (handleError) { + // See if there are more APN's to try + if (apnContext.getWaitingApns().isEmpty()) { + if (apnContext.getWaitingApnsPermFailCount() == 0) { + if (DBG) { + log("onDataSetupComplete: All APN's had permanent failures, stop retrying"); + } + apnContext.setState(DctConstants.State.FAILED); + mPhone.notifyDataConnection(Phone.REASON_APN_FAILED, apnContext.getApnType()); + + apnContext.setDataConnection(null); + apnContext.setDataConnectionAc(null); + } else { + if (DBG) log("onDataSetupComplete: Not all permanent failures, retry"); + // check to see if retry should be overridden for this failure. + int retryOverride = -1; + if (ar.exception instanceof DataConnection.CallSetupException) { + retryOverride = + ((DataConnection.CallSetupException)ar.exception).getRetryOverride(); + } + if (retryOverride == RILConstants.MAX_INT) { + if (DBG) log("No retry is suggested."); + } else { + startDelayedRetry(cause, apnContext, retryOverride); + } + } + } else { + if (DBG) log("onDataSetupComplete: Try next APN"); + apnContext.setState(DctConstants.State.SCANNING); + // Wait a bit before trying the next APN, so that + // we're not tying up the RIL command channel + startAlarmForReconnect(APN_DELAY_MILLIS, apnContext); + } + } + } + + /** + * Called when EVENT_DISCONNECT_DONE is received. + */ + @Override + protected void onDisconnectDone(int connId, AsyncResult ar) { + ApnContext apnContext = null; + + if(DBG) log("onDisconnectDone: EVENT_DISCONNECT_DONE connId=" + connId); + if (ar.userObj instanceof ApnContext) { + apnContext = (ApnContext) ar.userObj; + } else { + loge("Invalid ar in onDisconnectDone"); + return; + } + + apnContext.setState(DctConstants.State.IDLE); + + mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType()); + + // if all data connection are gone, check whether Airplane mode request was + // pending. + if (isDisconnected()) { + if (mPhone.getServiceStateTracker().processPendingRadioPowerOffAfterDataOff()) { + // Radio will be turned off. No need to retry data setup + apnContext.setApnSetting(null); + apnContext.setDataConnection(null); + apnContext.setDataConnectionAc(null); + return; + } + } + + // If APN is still enabled, try to bring it back up automatically + if (apnContext.isReady() && retryAfterDisconnected(apnContext.getReason())) { + SystemProperties.set("gsm.defaultpdpcontext.active", "false"); // TODO - what the heck? This shoudld go + // Wait a bit before trying the next APN, so that + // we're not tying up the RIL command channel. + // This also helps in any external dependency to turn off the context. + startAlarmForReconnect(APN_DELAY_MILLIS, apnContext); + } else { + apnContext.setApnSetting(null); + apnContext.setDataConnection(null); + apnContext.setDataConnectionAc(null); + } + } + + protected void onPollPdp() { + if (getOverallState() == DctConstants.State.CONNECTED) { + // only poll when connected + mPhone.mCM.getDataCallList(this.obtainMessage(DctConstants.EVENT_DATA_STATE_CHANGED)); + sendMessageDelayed(obtainMessage(DctConstants.EVENT_POLL_PDP), POLL_PDP_MILLIS); + } + } + + @Override + protected void onVoiceCallStarted() { + if (DBG) log("onVoiceCallStarted"); + if (isConnected() && ! mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) { + if (DBG) log("onVoiceCallStarted stop polling"); + stopNetStatPoll(); + stopDataStallAlarm(); + notifyDataConnection(Phone.REASON_VOICE_CALL_STARTED); + } + } + + @Override + protected void onVoiceCallEnded() { + if (DBG) log("onVoiceCallEnded"); + if (isConnected()) { + if (!mPhone.getServiceStateTracker().isConcurrentVoiceAndDataAllowed()) { + startNetStatPoll(); + startDataStallAlarm(DATA_STALL_NOT_SUSPECTED); + notifyDataConnection(Phone.REASON_VOICE_CALL_ENDED); + } else { + // clean slate after call end. + resetPollStats(); + } + } + // reset reconnect timer + setupDataOnReadyApns(Phone.REASON_VOICE_CALL_ENDED); + } + + @Override + protected void onCleanUpConnection(boolean tearDown, int apnId, String reason) { + if (DBG) log("onCleanUpConnection"); + ApnContext apnContext = mApnContexts.get(apnIdToType(apnId)); + if (apnContext != null) { + apnContext.setReason(reason); + cleanUpConnection(tearDown, apnContext); + } + } + + protected boolean isConnected() { + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.getState() ==DctConstants.State.CONNECTED) { + // At least one context is connected, return true + return true; + } + } + // There are not any contexts connected, return false + return false; + } + + @Override + public boolean isDisconnected() { + for (ApnContext apnContext : mApnContexts.values()) { + if (!apnContext.isDisconnected()) { + // At least one context was not disconnected return false + return false; + } + } + // All contexts were disconnected so return true + return true; + } + + @Override + protected void notifyDataConnection(String reason) { + if (DBG) log("notifyDataConnection: reason=" + reason); + for (ApnContext apnContext : mApnContexts.values()) { + if (apnContext.isReady()) { + if (DBG) log("notifyDataConnection: type:"+apnContext.getApnType()); + mPhone.notifyDataConnection(reason != null ? reason : apnContext.getReason(), + apnContext.getApnType()); + } + } + notifyOffApnsOfAvailability(reason); + } + + /** + * Based on the sim operator numeric, create a list for all possible + * Data Connections and setup the preferredApn. + */ + private void createAllApnList() { + mAllApns = new ArrayList<ApnSetting>(); + String operator = mPhone.mIccRecords.getOperatorNumeric(); + if (operator != null) { + String selection = "numeric = '" + operator + "'"; + // query only enabled apn. + // carrier_enabled : 1 means enabled apn, 0 disabled apn. + selection += " and carrier_enabled = 1"; + if (DBG) log("createAllApnList: selection=" + selection); + + Cursor cursor = mPhone.getContext().getContentResolver().query( + Telephony.Carriers.CONTENT_URI, null, selection, null, null); + + if (cursor != null) { + if (cursor.getCount() > 0) { + mAllApns = createApnList(cursor); + } + cursor.close(); + } + } + + if (mAllApns.isEmpty()) { + if (DBG) log("createAllApnList: No APN found for carrier: " + operator); + mPreferredApn = null; + // TODO: What is the right behaviour? + //notifyNoData(GsmDataConnection.FailCause.MISSING_UNKNOWN_APN); + } else { + mPreferredApn = getPreferredApn(); + if (mPreferredApn != null && !mPreferredApn.numeric.equals(operator)) { + mPreferredApn = null; + setPreferredApn(-1); + } + if (DBG) log("createAllApnList: mPreferredApn=" + mPreferredApn); + } + if (DBG) log("createAllApnList: X mAllApns=" + mAllApns); + } + + /** Return the id for a new data connection */ + private GsmDataConnection createDataConnection() { + if (DBG) log("createDataConnection E"); + + RetryManager rm = new RetryManager(); + int id = mUniqueIdGenerator.getAndIncrement(); + GsmDataConnection conn = GsmDataConnection.makeDataConnection(mPhone, id, rm, this); + mDataConnections.put(id, conn); + DataConnectionAc dcac = new DataConnectionAc(conn, LOG_TAG); + int status = dcac.fullyConnectSync(mPhone.getContext(), this, conn.getHandler()); + if (status == AsyncChannel.STATUS_SUCCESSFUL) { + mDataConnectionAsyncChannels.put(dcac.dataConnection.getDataConnectionId(), dcac); + } else { + loge("createDataConnection: Could not connect to dcac.mDc=" + dcac.dataConnection + + " status=" + status); + } + + // install reconnect intent filter for this data connection. + IntentFilter filter = new IntentFilter(); + filter.addAction(INTENT_RECONNECT_ALARM + '.' + id); + mPhone.getContext().registerReceiver(mIntentReceiver, filter, null, mPhone); + + if (DBG) log("createDataConnection() X id=" + id + " dc=" + conn); + return conn; + } + + private void configureRetry(DataConnection dc, boolean forDefault, int retryCount) { + if (DBG) { + log("configureRetry: forDefault=" + forDefault + " retryCount=" + retryCount + + " dc=" + dc); + } + if (dc == null) return; + + if (!dc.configureRetry(getReryConfig(forDefault))) { + if (forDefault) { + if (!dc.configureRetry(DEFAULT_DATA_RETRY_CONFIG)) { + // Should never happen, log an error and default to a simple linear sequence. + loge("configureRetry: Could not configure using " + + "DEFAULT_DATA_RETRY_CONFIG=" + DEFAULT_DATA_RETRY_CONFIG); + dc.configureRetry(20, 2000, 1000); + } + } else { + if (!dc.configureRetry(SECONDARY_DATA_RETRY_CONFIG)) { + // Should never happen, log an error and default to a simple sequence. + loge("configureRetry: Could note configure using " + + "SECONDARY_DATA_RETRY_CONFIG=" + SECONDARY_DATA_RETRY_CONFIG); + dc.configureRetry("max_retries=3, 333, 333, 333"); + } + } + } + dc.setRetryCount(retryCount); + } + + private void destroyDataConnections() { + if(mDataConnections != null) { + if (DBG) log("destroyDataConnections: clear mDataConnectionList"); + mDataConnections.clear(); + } else { + if (DBG) log("destroyDataConnections: mDataConnecitonList is empty, ignore"); + } + } + + /** + * Build a list of APNs to be used to create PDP's. + * + * @param requestedApnType + * @return waitingApns list to be used to create PDP + * error when waitingApns.isEmpty() + */ + private ArrayList<ApnSetting> buildWaitingApns(String requestedApnType) { + ArrayList<ApnSetting> apnList = new ArrayList<ApnSetting>(); + + if (requestedApnType.equals(PhoneConstants.APN_TYPE_DUN)) { + ApnSetting dun = fetchDunApn(); + if (dun != null) { + apnList.add(dun); + if (DBG) log("buildWaitingApns: X added APN_TYPE_DUN apnList=" + apnList); + return apnList; + } + } + + String operator = mPhone.mIccRecords.getOperatorNumeric(); + int networkType = mPhone.getServiceState().getNetworkType(); + + if (canSetPreferApn && mPreferredApn != null && + mPreferredApn.canHandleType(requestedApnType)) { + if (DBG) { + log("buildWaitingApns: Preferred APN:" + operator + ":" + + mPreferredApn.numeric + ":" + mPreferredApn); + } + if (mPreferredApn.numeric.equals(operator)) { + if (mPreferredApn.bearer == 0 || mPreferredApn.bearer == networkType) { + apnList.add(mPreferredApn); + if (DBG) log("buildWaitingApns: X added preferred apnList=" + apnList); + return apnList; + } else { + if (DBG) log("buildWaitingApns: no preferred APN"); + setPreferredApn(-1); + mPreferredApn = null; + } + } else { + if (DBG) log("buildWaitingApns: no preferred APN"); + setPreferredApn(-1); + mPreferredApn = null; + } + } + if (mAllApns != null) { + for (ApnSetting apn : mAllApns) { + if (apn.canHandleType(requestedApnType)) { + if (apn.bearer == 0 || apn.bearer == networkType) { + if (DBG) log("apn info : " +apn.toString()); + apnList.add(apn); + } + } + } + } else { + loge("mAllApns is empty!"); + } + if (DBG) log("buildWaitingApns: X apnList=" + apnList); + return apnList; + } + + private String apnListToString (ArrayList<ApnSetting> apns) { + StringBuilder result = new StringBuilder(); + for (int i = 0, size = apns.size(); i < size; i++) { + result.append('[') + .append(apns.get(i).toString()) + .append(']'); + } + return result.toString(); + } + + private void startDelayedRetry(GsmDataConnection.FailCause cause, + ApnContext apnContext, int retryOverride) { + notifyNoData(cause, apnContext); + reconnectAfterFail(cause, apnContext, retryOverride); + } + + private void setPreferredApn(int pos) { + if (!canSetPreferApn) { + log("setPreferredApn: X !canSEtPreferApn"); + return; + } + + log("setPreferredApn: delete"); + ContentResolver resolver = mPhone.getContext().getContentResolver(); + resolver.delete(PREFERAPN_NO_UPDATE_URI, null, null); + + if (pos >= 0) { + log("setPreferredApn: insert"); + ContentValues values = new ContentValues(); + values.put(APN_ID, pos); + resolver.insert(PREFERAPN_NO_UPDATE_URI, values); + } + } + + private ApnSetting getPreferredApn() { + if (mAllApns.isEmpty()) { + log("getPreferredApn: X not found mAllApns.isEmpty"); + return null; + } + + Cursor cursor = mPhone.getContext().getContentResolver().query( + PREFERAPN_NO_UPDATE_URI, new String[] { "_id", "name", "apn" }, + null, null, Telephony.Carriers.DEFAULT_SORT_ORDER); + + if (cursor != null) { + canSetPreferApn = true; + } else { + canSetPreferApn = false; + } + + if (canSetPreferApn && cursor.getCount() > 0) { + int pos; + cursor.moveToFirst(); + pos = cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)); + for(ApnSetting p:mAllApns) { + if (p.id == pos && p.canHandleType(mRequestedApnType)) { + log("getPreferredApn: X found apnSetting" + p); + cursor.close(); + return p; + } + } + } + + if (cursor != null) { + cursor.close(); + } + + log("getPreferredApn: X not found"); + return null; + } + + @Override + public void handleMessage (Message msg) { + if (DBG) log("handleMessage msg=" + msg); + + if (!mPhone.mIsTheCurrentActivePhone || mIsDisposed) { + loge("handleMessage: Ignore GSM msgs since GSM phone is inactive"); + return; + } + + switch (msg.what) { + case DctConstants.EVENT_RECORDS_LOADED: + onRecordsLoaded(); + break; + + case DctConstants.EVENT_DATA_CONNECTION_DETACHED: + onDataConnectionDetached(); + break; + + case DctConstants.EVENT_DATA_CONNECTION_ATTACHED: + onDataConnectionAttached(); + break; + + case DctConstants.EVENT_DATA_STATE_CHANGED: + onDataStateChanged((AsyncResult) msg.obj); + break; + + case DctConstants.EVENT_POLL_PDP: + onPollPdp(); + break; + + case DctConstants.EVENT_DO_RECOVERY: + doRecovery(); + break; + + case DctConstants.EVENT_APN_CHANGED: + onApnChanged(); + break; + + case DctConstants.EVENT_PS_RESTRICT_ENABLED: + /** + * We don't need to explicitly to tear down the PDP context + * when PS restricted is enabled. The base band will deactive + * PDP context and notify us with PDP_CONTEXT_CHANGED. + * But we should stop the network polling and prevent reset PDP. + */ + if (DBG) log("EVENT_PS_RESTRICT_ENABLED " + mIsPsRestricted); + stopNetStatPoll(); + stopDataStallAlarm(); + mIsPsRestricted = true; + break; + + case DctConstants.EVENT_PS_RESTRICT_DISABLED: + /** + * When PS restrict is removed, we need setup PDP connection if + * PDP connection is down. + */ + if (DBG) log("EVENT_PS_RESTRICT_DISABLED " + mIsPsRestricted); + mIsPsRestricted = false; + if (isConnected()) { + startNetStatPoll(); + startDataStallAlarm(DATA_STALL_NOT_SUSPECTED); + } else { + // TODO: Should all PDN states be checked to fail? + if (mState ==DctConstants.State.FAILED) { + cleanUpAllConnections(false, Phone.REASON_PS_RESTRICT_ENABLED); + resetAllRetryCounts(); + mReregisterOnReconnectFailure = false; + } + trySetupData(Phone.REASON_PS_RESTRICT_ENABLED, PhoneConstants.APN_TYPE_DEFAULT); + } + break; + case DctConstants.EVENT_TRY_SETUP_DATA: + if (msg.obj instanceof ApnContext) { + onTrySetupData((ApnContext)msg.obj); + } else if (msg.obj instanceof String) { + onTrySetupData((String)msg.obj); + } else { + loge("EVENT_TRY_SETUP request w/o apnContext or String"); + } + break; + + case DctConstants.EVENT_CLEAN_UP_CONNECTION: + boolean tearDown = (msg.arg1 == 0) ? false : true; + if (DBG) log("EVENT_CLEAN_UP_CONNECTION tearDown=" + tearDown); + if (msg.obj instanceof ApnContext) { + cleanUpConnection(tearDown, (ApnContext)msg.obj); + } else { + loge("EVENT_CLEAN_UP_CONNECTION request w/o apn context"); + } + break; + default: + // handle the message in the super class DataConnectionTracker + super.handleMessage(msg); + break; + } + } + + protected int getApnProfileID(String apnType) { + if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_IMS)) { + return RILConstants.DATA_PROFILE_IMS; + } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_FOTA)) { + return RILConstants.DATA_PROFILE_FOTA; + } else if (TextUtils.equals(apnType, PhoneConstants.APN_TYPE_CBS)) { + return RILConstants.DATA_PROFILE_CBS; + } else { + return RILConstants.DATA_PROFILE_DEFAULT; + } + } + + private int getCellLocationId() { + int cid = -1; + CellLocation loc = mPhone.getCellLocation(); + + if (loc != null) { + if (loc instanceof GsmCellLocation) { + cid = ((GsmCellLocation)loc).getCid(); + } else if (loc instanceof CdmaCellLocation) { + cid = ((CdmaCellLocation)loc).getBaseStationId(); + } + } + return cid; + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[GsmDCT] "+ s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[GsmDCT] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GsmDataConnectionTracker extends:"); + super.dump(fd, pw, args); + pw.println(" RADIO_TESTS=" + RADIO_TESTS); + pw.println(" mReregisterOnReconnectFailure=" + mReregisterOnReconnectFailure); + pw.println(" mResolver=" + mResolver); + pw.println(" canSetPreferApn=" + canSetPreferApn); + pw.println(" mApnObserver=" + mApnObserver); + pw.println(" getOverallState=" + getOverallState()); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java b/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java new file mode 100644 index 0000000..15e6a22 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmMmiCode.java @@ -0,0 +1,1357 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.content.Context; +import com.android.internal.telephony.*; + +import android.os.*; +import android.telephony.PhoneNumberUtils; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.util.Log; + +import static com.android.internal.telephony.CommandsInterface.*; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +/** + * The motto for this file is: + * + * "NOTE: By using the # as a separator, most cases are expected to be unambiguous." + * -- TS 22.030 6.5.2 + * + * {@hide} + * + */ +public final class GsmMmiCode extends Handler implements MmiCode { + static final String LOG_TAG = "GSM"; + + //***** Constants + + // Max Size of the Short Code (aka Short String from TS 22.030 6.5.2) + static final int MAX_LENGTH_SHORT_CODE = 2; + + // TS 22.030 6.5.2 Every Short String USSD command will end with #-key + // (known as #-String) + static final char END_OF_USSD_COMMAND = '#'; + + // From TS 22.030 6.5.2 + static final String ACTION_ACTIVATE = "*"; + static final String ACTION_DEACTIVATE = "#"; + static final String ACTION_INTERROGATE = "*#"; + static final String ACTION_REGISTER = "**"; + static final String ACTION_ERASURE = "##"; + + // Supp Service codes from TS 22.030 Annex B + + //Called line presentation + static final String SC_CLIP = "30"; + static final String SC_CLIR = "31"; + + // Call Forwarding + static final String SC_CFU = "21"; + static final String SC_CFB = "67"; + static final String SC_CFNRy = "61"; + static final String SC_CFNR = "62"; + + static final String SC_CF_All = "002"; + static final String SC_CF_All_Conditional = "004"; + + // Call Waiting + static final String SC_WAIT = "43"; + + // Call Barring + static final String SC_BAOC = "33"; + static final String SC_BAOIC = "331"; + static final String SC_BAOICxH = "332"; + static final String SC_BAIC = "35"; + static final String SC_BAICr = "351"; + + static final String SC_BA_ALL = "330"; + static final String SC_BA_MO = "333"; + static final String SC_BA_MT = "353"; + + // Supp Service Password registration + static final String SC_PWD = "03"; + + // PIN/PIN2/PUK/PUK2 + static final String SC_PIN = "04"; + static final String SC_PIN2 = "042"; + static final String SC_PUK = "05"; + static final String SC_PUK2 = "052"; + + //***** Event Constants + + static final int EVENT_SET_COMPLETE = 1; + static final int EVENT_GET_CLIR_COMPLETE = 2; + static final int EVENT_QUERY_CF_COMPLETE = 3; + static final int EVENT_USSD_COMPLETE = 4; + static final int EVENT_QUERY_COMPLETE = 5; + static final int EVENT_SET_CFF_COMPLETE = 6; + static final int EVENT_USSD_CANCEL_COMPLETE = 7; + + //***** Instance Variables + + GSMPhone phone; + Context context; + + String action; // One of ACTION_* + String sc; // Service Code + String sia, sib, sic; // Service Info a,b,c + String poundString; // Entire MMI string up to and including # + String dialingNumber; + String pwd; // For password registration + + /** Set to true in processCode, not at newFromDialString time */ + private boolean isPendingUSSD; + + private boolean isUssdRequest; + + State state = State.PENDING; + CharSequence message; + + //***** Class Variables + + + // See TS 22.030 6.5.2 "Structure of the MMI" + + static Pattern sPatternSuppService = Pattern.compile( + "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)([^#]*)"); +/* 1 2 3 4 5 6 7 8 9 10 11 12 + + 1 = Full string up to and including # + 2 = action (activation/interrogation/registration/erasure) + 3 = service code + 5 = SIA + 7 = SIB + 9 = SIC + 10 = dialing number which must not include #, e.g. *SCn*SI#DN format +*/ + + static final int MATCH_GROUP_POUND_STRING = 1; + + static final int MATCH_GROUP_ACTION = 2; + //(activation/interrogation/registration/erasure) + + static final int MATCH_GROUP_SERVICE_CODE = 3; + static final int MATCH_GROUP_SIA = 5; + static final int MATCH_GROUP_SIB = 7; + static final int MATCH_GROUP_SIC = 9; + static final int MATCH_GROUP_PWD_CONFIRM = 11; + static final int MATCH_GROUP_DIALING_NUMBER = 12; + static private String[] sTwoDigitNumberPattern; + + //***** Public Class methods + + /** + * Some dial strings in GSM are defined to do non-call setup + * things, such as modify or query supplementary service settings (eg, call + * forwarding). These are generally referred to as "MMI codes". + * We look to see if the dial string contains a valid MMI code (potentially + * with a dial string at the end as well) and return info here. + * + * If the dial string contains no MMI code, we return an instance with + * only "dialingNumber" set + * + * Please see flow chart in TS 22.030 6.5.3.2 + */ + + static GsmMmiCode + newFromDialString(String dialString, GSMPhone phone) { + Matcher m; + GsmMmiCode ret = null; + + m = sPatternSuppService.matcher(dialString); + + // Is this formatted like a standard supplementary service code? + if (m.matches()) { + ret = new GsmMmiCode(phone); + ret.poundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING)); + ret.action = makeEmptyNull(m.group(MATCH_GROUP_ACTION)); + ret.sc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE)); + ret.sia = makeEmptyNull(m.group(MATCH_GROUP_SIA)); + ret.sib = makeEmptyNull(m.group(MATCH_GROUP_SIB)); + ret.sic = makeEmptyNull(m.group(MATCH_GROUP_SIC)); + ret.pwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM)); + ret.dialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER)); + + } else if (dialString.endsWith("#")) { + // TS 22.030 sec 6.5.3.2 + // "Entry of any characters defined in the 3GPP TS 23.038 [8] Default Alphabet + // (up to the maximum defined in 3GPP TS 24.080 [10]), followed by #SEND". + + ret = new GsmMmiCode(phone); + ret.poundString = dialString; + } else if (isTwoDigitShortCode(phone.getContext(), dialString)) { + //Is a country-specific exception to short codes as defined in TS 22.030, 6.5.3.2 + ret = null; + } else if (isShortCode(dialString, phone)) { + // this may be a short code, as defined in TS 22.030, 6.5.3.2 + ret = new GsmMmiCode(phone); + ret.dialingNumber = dialString; + } + + return ret; + } + + static GsmMmiCode + newNetworkInitiatedUssd (String ussdMessage, + boolean isUssdRequest, GSMPhone phone) { + GsmMmiCode ret; + + ret = new GsmMmiCode(phone); + + ret.message = ussdMessage; + ret.isUssdRequest = isUssdRequest; + + // If it's a request, set to PENDING so that it's cancelable. + if (isUssdRequest) { + ret.isPendingUSSD = true; + ret.state = State.PENDING; + } else { + ret.state = State.COMPLETE; + } + + return ret; + } + + static GsmMmiCode newFromUssdUserInput(String ussdMessge, GSMPhone phone) { + GsmMmiCode ret = new GsmMmiCode(phone); + + ret.message = ussdMessge; + ret.state = State.PENDING; + ret.isPendingUSSD = true; + + return ret; + } + + //***** Private Class methods + + /** make empty strings be null. + * Regexp returns empty strings for empty groups + */ + private static String + makeEmptyNull (String s) { + if (s != null && s.length() == 0) return null; + + return s; + } + + /** returns true of the string is empty or null */ + private static boolean + isEmptyOrNull(CharSequence s) { + return s == null || (s.length() == 0); + } + + + private static int + scToCallForwardReason(String sc) { + if (sc == null) { + throw new RuntimeException ("invalid call forward sc"); + } + + if (sc.equals(SC_CF_All)) { + return CommandsInterface.CF_REASON_ALL; + } else if (sc.equals(SC_CFU)) { + return CommandsInterface.CF_REASON_UNCONDITIONAL; + } else if (sc.equals(SC_CFB)) { + return CommandsInterface.CF_REASON_BUSY; + } else if (sc.equals(SC_CFNR)) { + return CommandsInterface.CF_REASON_NOT_REACHABLE; + } else if (sc.equals(SC_CFNRy)) { + return CommandsInterface.CF_REASON_NO_REPLY; + } else if (sc.equals(SC_CF_All_Conditional)) { + return CommandsInterface.CF_REASON_ALL_CONDITIONAL; + } else { + throw new RuntimeException ("invalid call forward sc"); + } + } + + private static int + siToServiceClass(String si) { + if (si == null || si.length() == 0) { + return SERVICE_CLASS_NONE; + } else { + // NumberFormatException should cause MMI fail + int serviceCode = Integer.parseInt(si, 10); + + switch (serviceCode) { + case 10: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX + SERVICE_CLASS_VOICE; + case 11: return SERVICE_CLASS_VOICE; + case 12: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX; + case 13: return SERVICE_CLASS_FAX; + + case 16: return SERVICE_CLASS_SMS; + + case 19: return SERVICE_CLASS_FAX + SERVICE_CLASS_VOICE; +/* + Note for code 20: + From TS 22.030 Annex C: + "All GPRS bearer services" are not included in "All tele and bearer services" + and "All bearer services"." +....so SERVICE_CLASS_DATA, which (according to 27.007) includes GPRS +*/ + case 20: return SERVICE_CLASS_DATA_ASYNC + SERVICE_CLASS_DATA_SYNC; + + case 21: return SERVICE_CLASS_PAD + SERVICE_CLASS_DATA_ASYNC; + case 22: return SERVICE_CLASS_PACKET + SERVICE_CLASS_DATA_SYNC; + case 24: return SERVICE_CLASS_DATA_SYNC; + case 25: return SERVICE_CLASS_DATA_ASYNC; + case 26: return SERVICE_CLASS_DATA_SYNC + SERVICE_CLASS_VOICE; + case 99: return SERVICE_CLASS_PACKET; + + default: + throw new RuntimeException("unsupported MMI service code " + si); + } + } + } + + private static int + siToTime (String si) { + if (si == null || si.length() == 0) { + return 0; + } else { + // NumberFormatException should cause MMI fail + return Integer.parseInt(si, 10); + } + } + + static boolean + isServiceCodeCallForwarding(String sc) { + return sc != null && + (sc.equals(SC_CFU) + || sc.equals(SC_CFB) || sc.equals(SC_CFNRy) + || sc.equals(SC_CFNR) || sc.equals(SC_CF_All) + || sc.equals(SC_CF_All_Conditional)); + } + + static boolean + isServiceCodeCallBarring(String sc) { + return sc != null && + (sc.equals(SC_BAOC) + || sc.equals(SC_BAOIC) + || sc.equals(SC_BAOICxH) + || sc.equals(SC_BAIC) + || sc.equals(SC_BAICr) + || sc.equals(SC_BA_ALL) + || sc.equals(SC_BA_MO) + || sc.equals(SC_BA_MT)); + } + + static String + scToBarringFacility(String sc) { + if (sc == null) { + throw new RuntimeException ("invalid call barring sc"); + } + + if (sc.equals(SC_BAOC)) { + return CommandsInterface.CB_FACILITY_BAOC; + } else if (sc.equals(SC_BAOIC)) { + return CommandsInterface.CB_FACILITY_BAOIC; + } else if (sc.equals(SC_BAOICxH)) { + return CommandsInterface.CB_FACILITY_BAOICxH; + } else if (sc.equals(SC_BAIC)) { + return CommandsInterface.CB_FACILITY_BAIC; + } else if (sc.equals(SC_BAICr)) { + return CommandsInterface.CB_FACILITY_BAICr; + } else if (sc.equals(SC_BA_ALL)) { + return CommandsInterface.CB_FACILITY_BA_ALL; + } else if (sc.equals(SC_BA_MO)) { + return CommandsInterface.CB_FACILITY_BA_MO; + } else if (sc.equals(SC_BA_MT)) { + return CommandsInterface.CB_FACILITY_BA_MT; + } else { + throw new RuntimeException ("invalid call barring sc"); + } + } + + //***** Constructor + + GsmMmiCode (GSMPhone phone) { + // The telephony unit-test cases may create GsmMmiCode's + // in secondary threads + super(phone.getHandler().getLooper()); + this.phone = phone; + this.context = phone.getContext(); + } + + //***** MmiCode implementation + + public State + getState() { + return state; + } + + public CharSequence + getMessage() { + return message; + } + + // inherited javadoc suffices + public void + cancel() { + // Complete or failed cannot be cancelled + if (state == State.COMPLETE || state == State.FAILED) { + return; + } + + state = State.CANCELLED; + + if (isPendingUSSD) { + /* + * There can only be one pending USSD session, so tell the radio to + * cancel it. + */ + phone.mCM.cancelPendingUssd(obtainMessage(EVENT_USSD_CANCEL_COMPLETE, this)); + + /* + * Don't call phone.onMMIDone here; wait for CANCEL_COMPLETE notice + * from RIL. + */ + } else { + // TODO in cases other than USSD, it would be nice to cancel + // the pending radio operation. This requires RIL cancellation + // support, which does not presently exist. + + phone.onMMIDone (this); + } + + } + + public boolean isCancelable() { + /* Can only cancel pending USSD sessions. */ + return isPendingUSSD; + } + + //***** Instance Methods + + /** Does this dial string contain a structured or unstructured MMI code? */ + boolean + isMMI() { + return poundString != null; + } + + /* Is this a 1 or 2 digit "short code" as defined in TS 22.030 sec 6.5.3.2? */ + boolean + isShortCode() { + return poundString == null + && dialingNumber != null && dialingNumber.length() <= 2; + + } + + static private boolean + isTwoDigitShortCode(Context context, String dialString) { + Log.d(LOG_TAG, "isTwoDigitShortCode"); + + if (dialString == null || dialString.length() != 2) return false; + + if (sTwoDigitNumberPattern == null) { + sTwoDigitNumberPattern = context.getResources().getStringArray( + com.android.internal.R.array.config_twoDigitNumberPattern); + } + + for (String dialnumber : sTwoDigitNumberPattern) { + Log.d(LOG_TAG, "Two Digit Number Pattern " + dialnumber); + if (dialString.equals(dialnumber)) { + Log.d(LOG_TAG, "Two Digit Number Pattern -true"); + return true; + } + } + Log.d(LOG_TAG, "Two Digit Number Pattern -false"); + return false; + } + + /** + * Helper function for newFromDialString. Returns true if dialString appears + * to be a short code AND conditions are correct for it to be treated as + * such. + */ + static private boolean isShortCode(String dialString, GSMPhone phone) { + // Refer to TS 22.030 Figure 3.5.3.2: + if (dialString == null) { + return false; + } + + // Illegal dial string characters will give a ZERO length. + // At this point we do not want to crash as any application with + // call privileges may send a non dial string. + // It return false as when the dialString is equal to NULL. + if (dialString.length() == 0) { + return false; + } + + if (PhoneNumberUtils.isLocalEmergencyNumber(dialString, phone.getContext())) { + return false; + } else { + return isShortCodeUSSD(dialString, phone); + } + } + + /** + * Helper function for isShortCode. Returns true if dialString appears to be + * a short code and it is a USSD structure + * + * According to the 3PGG TS 22.030 specification Figure 3.5.3.2: A 1 or 2 + * digit "short code" is treated as USSD if it is entered while on a call or + * does not satisfy the condition (exactly 2 digits && starts with '1'), there + * are however exceptions to this rule (see below) + * + * Exception (1) to Call initiation is: If the user of the device is already in a call + * and enters a Short String without any #-key at the end and the length of the Short String is + * equal or less then the MAX_LENGTH_SHORT_CODE [constant that is equal to 2] + * + * The phone shall initiate a USSD/SS commands. + * + * Exception (2) to Call initiation is: If the user of the device enters one + * Digit followed by the #-key. This rule defines this String as the + * #-String which is a USSD/SS command. + * + * The phone shall initiate a USSD/SS command. + */ + static private boolean isShortCodeUSSD(String dialString, GSMPhone phone) { + if (dialString != null) { + if (phone.isInCall()) { + // The maximum length of a Short Code (aka Short String) is 2 + if (dialString.length() <= MAX_LENGTH_SHORT_CODE) { + return true; + } + } + + // The maximum length of a Short Code (aka Short String) is 2 + if (dialString.length() <= MAX_LENGTH_SHORT_CODE) { + if (dialString.charAt(dialString.length() - 1) == END_OF_USSD_COMMAND) { + return true; + } + } + } + return false; + } + + /** + * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related + */ + boolean isPinCommand() { + return sc != null && (sc.equals(SC_PIN) || sc.equals(SC_PIN2) + || sc.equals(SC_PUK) || sc.equals(SC_PUK2)); + } + + /** + * See TS 22.030 Annex B. + * In temporary mode, to suppress CLIR for a single call, enter: + * " * 31 # [called number] SEND " + * In temporary mode, to invoke CLIR for a single call enter: + * " # 31 # [called number] SEND " + */ + boolean + isTemporaryModeCLIR() { + return sc != null && sc.equals(SC_CLIR) && dialingNumber != null + && (isActivate() || isDeactivate()); + } + + /** + * returns CommandsInterface.CLIR_* + * See also isTemporaryModeCLIR() + */ + int + getCLIRMode() { + if (sc != null && sc.equals(SC_CLIR)) { + if (isActivate()) { + return CommandsInterface.CLIR_SUPPRESSION; + } else if (isDeactivate()) { + return CommandsInterface.CLIR_INVOCATION; + } + } + + return CommandsInterface.CLIR_DEFAULT; + } + + boolean isActivate() { + return action != null && action.equals(ACTION_ACTIVATE); + } + + boolean isDeactivate() { + return action != null && action.equals(ACTION_DEACTIVATE); + } + + boolean isInterrogate() { + return action != null && action.equals(ACTION_INTERROGATE); + } + + boolean isRegister() { + return action != null && action.equals(ACTION_REGISTER); + } + + boolean isErasure() { + return action != null && action.equals(ACTION_ERASURE); + } + + /** + * Returns true if this is a USSD code that's been submitted to the + * network...eg, after processCode() is called + */ + public boolean isPendingUSSD() { + return isPendingUSSD; + } + + public boolean isUssdRequest() { + return isUssdRequest; + } + + /** Process a MMI code or short code...anything that isn't a dialing number */ + void + processCode () { + try { + if (isShortCode()) { + Log.d(LOG_TAG, "isShortCode"); + // These just get treated as USSD. + sendUssd(dialingNumber); + } else if (dialingNumber != null) { + // We should have no dialing numbers here + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } else if (sc != null && sc.equals(SC_CLIP)) { + Log.d(LOG_TAG, "is CLIP"); + if (isInterrogate()) { + phone.mCM.queryCLIP( + obtainMessage(EVENT_QUERY_COMPLETE, this)); + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } else if (sc != null && sc.equals(SC_CLIR)) { + Log.d(LOG_TAG, "is CLIR"); + if (isActivate()) { + phone.mCM.setCLIR(CommandsInterface.CLIR_INVOCATION, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (isDeactivate()) { + phone.mCM.setCLIR(CommandsInterface.CLIR_SUPPRESSION, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (isInterrogate()) { + phone.mCM.getCLIR( + obtainMessage(EVENT_GET_CLIR_COMPLETE, this)); + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } else if (isServiceCodeCallForwarding(sc)) { + Log.d(LOG_TAG, "is CF"); + + String dialingNumber = sia; + int serviceClass = siToServiceClass(sib); + int reason = scToCallForwardReason(sc); + int time = siToTime(sic); + + if (isInterrogate()) { + phone.mCM.queryCallForwardStatus( + reason, serviceClass, dialingNumber, + obtainMessage(EVENT_QUERY_CF_COMPLETE, this)); + } else { + int cfAction; + + if (isActivate()) { + cfAction = CommandsInterface.CF_ACTION_ENABLE; + } else if (isDeactivate()) { + cfAction = CommandsInterface.CF_ACTION_DISABLE; + } else if (isRegister()) { + cfAction = CommandsInterface.CF_ACTION_REGISTRATION; + } else if (isErasure()) { + cfAction = CommandsInterface.CF_ACTION_ERASURE; + } else { + throw new RuntimeException ("invalid action"); + } + + int isSettingUnconditionalVoice = + (((reason == CommandsInterface.CF_REASON_UNCONDITIONAL) || + (reason == CommandsInterface.CF_REASON_ALL)) && + (((serviceClass & CommandsInterface.SERVICE_CLASS_VOICE) != 0) || + (serviceClass == CommandsInterface.SERVICE_CLASS_NONE))) ? 1 : 0; + + int isEnableDesired = + ((cfAction == CommandsInterface.CF_ACTION_ENABLE) || + (cfAction == CommandsInterface.CF_ACTION_REGISTRATION)) ? 1 : 0; + + Log.d(LOG_TAG, "is CF setCallForward"); + phone.mCM.setCallForward(cfAction, reason, serviceClass, + dialingNumber, time, obtainMessage( + EVENT_SET_CFF_COMPLETE, + isSettingUnconditionalVoice, + isEnableDesired, this)); + } + } else if (isServiceCodeCallBarring(sc)) { + // sia = password + // sib = basic service group + + String password = sia; + int serviceClass = siToServiceClass(sib); + String facility = scToBarringFacility(sc); + + if (isInterrogate()) { + phone.mCM.queryFacilityLock(facility, password, + serviceClass, obtainMessage(EVENT_QUERY_COMPLETE, this)); + } else if (isActivate() || isDeactivate()) { + phone.mCM.setFacilityLock(facility, isActivate(), password, + serviceClass, obtainMessage(EVENT_SET_COMPLETE, this)); + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + + } else if (sc != null && sc.equals(SC_PWD)) { + // sia = fac + // sib = old pwd + // sic = new pwd + // pwd = new pwd + String facility; + String oldPwd = sib; + String newPwd = sic; + if (isActivate() || isRegister()) { + // Even though ACTIVATE is acceptable, this is really termed a REGISTER + action = ACTION_REGISTER; + + if (sia == null) { + // If sc was not specified, treat it as BA_ALL. + facility = CommandsInterface.CB_FACILITY_BA_ALL; + } else { + facility = scToBarringFacility(sia); + } + if (newPwd.equals(pwd)) { + phone.mCM.changeBarringPassword(facility, oldPwd, + newPwd, obtainMessage(EVENT_SET_COMPLETE, this)); + } else { + // password mismatch; return error + handlePasswordError(com.android.internal.R.string.passwordIncorrect); + } + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + + } else if (sc != null && sc.equals(SC_WAIT)) { + // sia = basic service group + int serviceClass = siToServiceClass(sia); + + if (isActivate() || isDeactivate()) { + phone.mCM.setCallWaiting(isActivate(), serviceClass, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (isInterrogate()) { + phone.mCM.queryCallWaiting(serviceClass, + obtainMessage(EVENT_QUERY_COMPLETE, this)); + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } else if (isPinCommand()) { + // sia = old PIN or PUK + // sib = new PIN + // sic = new PIN + String oldPinOrPuk = sia; + String newPin = sib; + int pinLen = newPin.length(); + if (isRegister()) { + if (!newPin.equals(sic)) { + // password mismatch; return error + handlePasswordError(com.android.internal.R.string.mismatchPin); + } else if (pinLen < 4 || pinLen > 8 ) { + // invalid length + handlePasswordError(com.android.internal.R.string.invalidPin); + } else if (sc.equals(SC_PIN) && phone.getIccCard().getState() == + IccCardConstants.State.PUK_REQUIRED ) { + // Sim is puk-locked + handlePasswordError(com.android.internal.R.string.needPuk); + } else { + // pre-checks OK + if (sc.equals(SC_PIN)) { + phone.mCM.changeIccPin(oldPinOrPuk, newPin, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (sc.equals(SC_PIN2)) { + phone.mCM.changeIccPin2(oldPinOrPuk, newPin, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (sc.equals(SC_PUK)) { + phone.mCM.supplyIccPuk(oldPinOrPuk, newPin, + obtainMessage(EVENT_SET_COMPLETE, this)); + } else if (sc.equals(SC_PUK2)) { + phone.mCM.supplyIccPuk2(oldPinOrPuk, newPin, + obtainMessage(EVENT_SET_COMPLETE, this)); + } + } + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } else if (poundString != null) { + sendUssd(poundString); + } else { + throw new RuntimeException ("Invalid or Unsupported MMI Code"); + } + } catch (RuntimeException exc) { + state = State.FAILED; + message = context.getText(com.android.internal.R.string.mmiError); + phone.onMMIDone(this); + } + } + + private void handlePasswordError(int res) { + state = State.FAILED; + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + sb.append(context.getText(res)); + message = sb; + phone.onMMIDone(this); + } + + /** + * Called from GSMPhone + * + * An unsolicited USSD NOTIFY or REQUEST has come in matching + * up with this pending USSD request + * + * Note: If REQUEST, this exchange is complete, but the session remains + * active (ie, the network expects user input). + */ + void + onUssdFinished(String ussdMessage, boolean isUssdRequest) { + if (state == State.PENDING) { + if (ussdMessage == null) { + message = context.getText(com.android.internal.R.string.mmiComplete); + } else { + message = ussdMessage; + } + this.isUssdRequest = isUssdRequest; + // If it's a request, leave it PENDING so that it's cancelable. + if (!isUssdRequest) { + state = State.COMPLETE; + } + + phone.onMMIDone(this); + } + } + + /** + * Called from GSMPhone + * + * The radio has reset, and this is still pending + */ + + void + onUssdFinishedError() { + if (state == State.PENDING) { + state = State.FAILED; + message = context.getText(com.android.internal.R.string.mmiError); + + phone.onMMIDone(this); + } + } + + void sendUssd(String ussdMessage) { + // Treat this as a USSD string + isPendingUSSD = true; + + // Note that unlike most everything else, the USSD complete + // response does not complete this MMI code...we wait for + // an unsolicited USSD "Notify" or "Request". + // The matching up of this is done in GSMPhone. + + phone.mCM.sendUSSD(ussdMessage, + obtainMessage(EVENT_USSD_COMPLETE, this)); + } + + /** Called from GSMPhone.handleMessage; not a Handler subclass */ + public void + handleMessage (Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_SET_COMPLETE: + ar = (AsyncResult) (msg.obj); + + onSetComplete(ar); + break; + + case EVENT_SET_CFF_COMPLETE: + ar = (AsyncResult) (msg.obj); + + /* + * msg.arg1 = 1 means to set unconditional voice call forwarding + * msg.arg2 = 1 means to enable voice call forwarding + */ + if ((ar.exception == null) && (msg.arg1 == 1)) { + boolean cffEnabled = (msg.arg2 == 1); + phone.mIccRecords.setVoiceCallForwardingFlag(1, cffEnabled); + } + + onSetComplete(ar); + break; + + case EVENT_GET_CLIR_COMPLETE: + ar = (AsyncResult) (msg.obj); + onGetClirComplete(ar); + break; + + case EVENT_QUERY_CF_COMPLETE: + ar = (AsyncResult) (msg.obj); + onQueryCfComplete(ar); + break; + + case EVENT_QUERY_COMPLETE: + ar = (AsyncResult) (msg.obj); + onQueryComplete(ar); + break; + + case EVENT_USSD_COMPLETE: + ar = (AsyncResult) (msg.obj); + + if (ar.exception != null) { + state = State.FAILED; + message = getErrorMessage(ar); + + phone.onMMIDone(this); + } + + // Note that unlike most everything else, the USSD complete + // response does not complete this MMI code...we wait for + // an unsolicited USSD "Notify" or "Request". + // The matching up of this is done in GSMPhone. + + break; + + case EVENT_USSD_CANCEL_COMPLETE: + phone.onMMIDone(this); + break; + } + } + //***** Private instance methods + + private CharSequence getErrorMessage(AsyncResult ar) { + + if (ar.exception instanceof CommandException) { + CommandException.Error err = ((CommandException)(ar.exception)).getCommandError(); + if (err == CommandException.Error.FDN_CHECK_FAILURE) { + Log.i(LOG_TAG, "FDN_CHECK_FAILURE"); + return context.getText(com.android.internal.R.string.mmiFdnError); + } + } + + return context.getText(com.android.internal.R.string.mmiError); + } + + private CharSequence getScString() { + if (sc != null) { + if (isServiceCodeCallBarring(sc)) { + return context.getText(com.android.internal.R.string.BaMmi); + } else if (isServiceCodeCallForwarding(sc)) { + return context.getText(com.android.internal.R.string.CfMmi); + } else if (sc.equals(SC_CLIP)) { + return context.getText(com.android.internal.R.string.ClipMmi); + } else if (sc.equals(SC_CLIR)) { + return context.getText(com.android.internal.R.string.ClirMmi); + } else if (sc.equals(SC_PWD)) { + return context.getText(com.android.internal.R.string.PwdMmi); + } else if (sc.equals(SC_WAIT)) { + return context.getText(com.android.internal.R.string.CwMmi); + } else if (isPinCommand()) { + return context.getText(com.android.internal.R.string.PinMmi); + } + } + + return ""; + } + + private void + onSetComplete(AsyncResult ar){ + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + + if (ar.exception != null) { + state = State.FAILED; + if (ar.exception instanceof CommandException) { + CommandException.Error err = ((CommandException)(ar.exception)).getCommandError(); + if (err == CommandException.Error.PASSWORD_INCORRECT) { + if (isPinCommand()) { + // look specifically for the PUK commands and adjust + // the message accordingly. + if (sc.equals(SC_PUK) || sc.equals(SC_PUK2)) { + sb.append(context.getText( + com.android.internal.R.string.badPuk)); + } else { + sb.append(context.getText( + com.android.internal.R.string.badPin)); + } + } else { + sb.append(context.getText( + com.android.internal.R.string.passwordIncorrect)); + } + } else if (err == CommandException.Error.SIM_PUK2) { + sb.append(context.getText( + com.android.internal.R.string.badPin)); + sb.append("\n"); + sb.append(context.getText( + com.android.internal.R.string.needPuk2)); + } else if (err == CommandException.Error.FDN_CHECK_FAILURE) { + Log.i(LOG_TAG, "FDN_CHECK_FAILURE"); + sb.append(context.getText(com.android.internal.R.string.mmiFdnError)); + } else { + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + } else { + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + } else if (isActivate()) { + state = State.COMPLETE; + sb.append(context.getText( + com.android.internal.R.string.serviceEnabled)); + // Record CLIR setting + if (sc.equals(SC_CLIR)) { + phone.saveClirSetting(CommandsInterface.CLIR_INVOCATION); + } + } else if (isDeactivate()) { + state = State.COMPLETE; + sb.append(context.getText( + com.android.internal.R.string.serviceDisabled)); + // Record CLIR setting + if (sc.equals(SC_CLIR)) { + phone.saveClirSetting(CommandsInterface.CLIR_SUPPRESSION); + } + } else if (isRegister()) { + state = State.COMPLETE; + sb.append(context.getText( + com.android.internal.R.string.serviceRegistered)); + } else if (isErasure()) { + state = State.COMPLETE; + sb.append(context.getText( + com.android.internal.R.string.serviceErased)); + } else { + state = State.FAILED; + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + } + + message = sb; + phone.onMMIDone(this); + } + + private void + onGetClirComplete(AsyncResult ar) { + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + + if (ar.exception != null) { + state = State.FAILED; + sb.append(getErrorMessage(ar)); + } else { + int clirArgs[]; + + clirArgs = (int[])ar.result; + + // the 'm' parameter from TS 27.007 7.7 + switch (clirArgs[1]) { + case 0: // CLIR not provisioned + sb.append(context.getText( + com.android.internal.R.string.serviceNotProvisioned)); + state = State.COMPLETE; + break; + + case 1: // CLIR provisioned in permanent mode + sb.append(context.getText( + com.android.internal.R.string.CLIRPermanent)); + state = State.COMPLETE; + break; + + case 2: // unknown (e.g. no network, etc.) + sb.append(context.getText( + com.android.internal.R.string.mmiError)); + state = State.FAILED; + break; + + case 3: // CLIR temporary mode presentation restricted + + // the 'n' parameter from TS 27.007 7.7 + switch (clirArgs[0]) { + default: + case 0: // Default + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOnNextCallOn)); + break; + case 1: // CLIR invocation + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOnNextCallOn)); + break; + case 2: // CLIR suppression + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOnNextCallOff)); + break; + } + state = State.COMPLETE; + break; + + case 4: // CLIR temporary mode presentation allowed + // the 'n' parameter from TS 27.007 7.7 + switch (clirArgs[0]) { + default: + case 0: // Default + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOffNextCallOff)); + break; + case 1: // CLIR invocation + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOffNextCallOn)); + break; + case 2: // CLIR suppression + sb.append(context.getText( + com.android.internal.R.string.CLIRDefaultOffNextCallOff)); + break; + } + + state = State.COMPLETE; + break; + } + } + + message = sb; + phone.onMMIDone(this); + } + + /** + * @param serviceClass 1 bit of the service class bit vectory + * @return String to be used for call forward query MMI response text. + * Returns null if unrecognized + */ + + private CharSequence + serviceClassToCFString (int serviceClass) { + switch (serviceClass) { + case SERVICE_CLASS_VOICE: + return context.getText(com.android.internal.R.string.serviceClassVoice); + case SERVICE_CLASS_DATA: + return context.getText(com.android.internal.R.string.serviceClassData); + case SERVICE_CLASS_FAX: + return context.getText(com.android.internal.R.string.serviceClassFAX); + case SERVICE_CLASS_SMS: + return context.getText(com.android.internal.R.string.serviceClassSMS); + case SERVICE_CLASS_DATA_SYNC: + return context.getText(com.android.internal.R.string.serviceClassDataSync); + case SERVICE_CLASS_DATA_ASYNC: + return context.getText(com.android.internal.R.string.serviceClassDataAsync); + case SERVICE_CLASS_PACKET: + return context.getText(com.android.internal.R.string.serviceClassPacket); + case SERVICE_CLASS_PAD: + return context.getText(com.android.internal.R.string.serviceClassPAD); + default: + return null; + } + } + + + /** one CallForwardInfo + serviceClassMask -> one line of text */ + private CharSequence + makeCFQueryResultMessage(CallForwardInfo info, int serviceClassMask) { + CharSequence template; + String sources[] = {"{0}", "{1}", "{2}"}; + CharSequence destinations[] = new CharSequence[3]; + boolean needTimeTemplate; + + // CF_REASON_NO_REPLY also has a time value associated with + // it. All others don't. + + needTimeTemplate = + (info.reason == CommandsInterface.CF_REASON_NO_REPLY); + + if (info.status == 1) { + if (needTimeTemplate) { + template = context.getText( + com.android.internal.R.string.cfTemplateForwardedTime); + } else { + template = context.getText( + com.android.internal.R.string.cfTemplateForwarded); + } + } else if (info.status == 0 && isEmptyOrNull(info.number)) { + template = context.getText( + com.android.internal.R.string.cfTemplateNotForwarded); + } else { /* (info.status == 0) && !isEmptyOrNull(info.number) */ + // A call forward record that is not active but contains + // a phone number is considered "registered" + + if (needTimeTemplate) { + template = context.getText( + com.android.internal.R.string.cfTemplateRegisteredTime); + } else { + template = context.getText( + com.android.internal.R.string.cfTemplateRegistered); + } + } + + // In the template (from strings.xmls) + // {0} is one of "bearerServiceCode*" + // {1} is dialing number + // {2} is time in seconds + + destinations[0] = serviceClassToCFString(info.serviceClass & serviceClassMask); + destinations[1] = PhoneNumberUtils.stringFromStringAndTOA(info.number, info.toa); + destinations[2] = Integer.toString(info.timeSeconds); + + if (info.reason == CommandsInterface.CF_REASON_UNCONDITIONAL && + (info.serviceClass & serviceClassMask) + == CommandsInterface.SERVICE_CLASS_VOICE) { + boolean cffEnabled = (info.status == 1); + phone.mIccRecords.setVoiceCallForwardingFlag(1, cffEnabled); + } + + return TextUtils.replace(template, sources, destinations); + } + + + private void + onQueryCfComplete(AsyncResult ar) { + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + + if (ar.exception != null) { + state = State.FAILED; + sb.append(getErrorMessage(ar)); + } else { + CallForwardInfo infos[]; + + infos = (CallForwardInfo[]) ar.result; + + if (infos.length == 0) { + // Assume the default is not active + sb.append(context.getText(com.android.internal.R.string.serviceDisabled)); + + // Set unconditional CFF in SIM to false + phone.mIccRecords.setVoiceCallForwardingFlag(1, false); + } else { + + SpannableStringBuilder tb = new SpannableStringBuilder(); + + // Each bit in the service class gets its own result line + // The service classes may be split up over multiple + // CallForwardInfos. So, for each service class, find out + // which CallForwardInfo represents it and then build + // the response text based on that + + for (int serviceClassMask = 1 + ; serviceClassMask <= SERVICE_CLASS_MAX + ; serviceClassMask <<= 1 + ) { + for (int i = 0, s = infos.length; i < s ; i++) { + if ((serviceClassMask & infos[i].serviceClass) != 0) { + tb.append(makeCFQueryResultMessage(infos[i], + serviceClassMask)); + tb.append("\n"); + } + } + } + sb.append(tb); + } + + state = State.COMPLETE; + } + + message = sb; + phone.onMMIDone(this); + + } + + private void + onQueryComplete(AsyncResult ar) { + StringBuilder sb = new StringBuilder(getScString()); + sb.append("\n"); + + if (ar.exception != null) { + state = State.FAILED; + sb.append(getErrorMessage(ar)); + } else { + int[] ints = (int[])ar.result; + + if (ints.length != 0) { + if (ints[0] == 0) { + sb.append(context.getText(com.android.internal.R.string.serviceDisabled)); + } else if (sc.equals(SC_WAIT)) { + // Call Waiting includes additional data in the response. + sb.append(createQueryCallWaitingResultMessage(ints[1])); + } else if (isServiceCodeCallBarring(sc)) { + // ints[0] for Call Barring is a bit vector of services + sb.append(createQueryCallBarringResultMessage(ints[0])); + } else if (ints[0] == 1) { + // for all other services, treat it as a boolean + sb.append(context.getText(com.android.internal.R.string.serviceEnabled)); + } else { + sb.append(context.getText(com.android.internal.R.string.mmiError)); + } + } else { + sb.append(context.getText(com.android.internal.R.string.mmiError)); + } + state = State.COMPLETE; + } + + message = sb; + phone.onMMIDone(this); + } + + private CharSequence + createQueryCallWaitingResultMessage(int serviceClass) { + StringBuilder sb = + new StringBuilder(context.getText(com.android.internal.R.string.serviceEnabledFor)); + + for (int classMask = 1 + ; classMask <= SERVICE_CLASS_MAX + ; classMask <<= 1 + ) { + if ((classMask & serviceClass) != 0) { + sb.append("\n"); + sb.append(serviceClassToCFString(classMask & serviceClass)); + } + } + return sb; + } + private CharSequence + createQueryCallBarringResultMessage(int serviceClass) + { + StringBuilder sb = new StringBuilder(context.getText(com.android.internal.R.string.serviceEnabledFor)); + + for (int classMask = 1 + ; classMask <= SERVICE_CLASS_MAX + ; classMask <<= 1 + ) { + if ((classMask & serviceClass) != 0) { + sb.append("\n"); + sb.append(serviceClassToCFString(classMask & serviceClass)); + } + } + return sb; + } + + /*** + * TODO: It would be nice to have a method here that can take in a dialstring and + * figure out if there is an MMI code embedded within it. This code would replace + * some of the string parsing functionality in the Phone App's + * SpecialCharSequenceMgr class. + */ + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("GsmMmiCode {"); + + sb.append("State=" + getState()); + if (action != null) sb.append(" action=" + action); + if (sc != null) sb.append(" sc=" + sc); + if (sia != null) sb.append(" sia=" + sia); + if (sib != null) sb.append(" sib=" + sib); + if (sic != null) sb.append(" sic=" + sic); + if (poundString != null) sb.append(" poundString=" + poundString); + if (dialingNumber != null) sb.append(" dialingNumber=" + dialingNumber); + if (pwd != null) sb.append(" pwd=" + pwd); + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java new file mode 100644 index 0000000..d6c2a20 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Intent; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemProperties; +import android.provider.Telephony.Sms; +import android.provider.Telephony.Sms.Intents; +import android.telephony.PhoneNumberUtils; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.telephony.SmsManager; +import android.telephony.gsm.GsmCellLocation; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.SMSDispatcher; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.SmsStorageMonitor; +import com.android.internal.telephony.SmsUsageMonitor; +import com.android.internal.telephony.TelephonyProperties; + +import java.util.HashMap; +import java.util.Iterator; + +public final class GsmSMSDispatcher extends SMSDispatcher { + private static final String TAG = "GSM"; + + /** Status report received */ + private static final int EVENT_NEW_SMS_STATUS_REPORT = 100; + + /** New broadcast SMS */ + private static final int EVENT_NEW_BROADCAST_SMS = 101; + + /** Result of writing SM to UICC (when SMS-PP service is not available). */ + private static final int EVENT_WRITE_SMS_COMPLETE = 102; + + /** Handler for SMS-PP data download messages to UICC. */ + private final UsimDataDownloadHandler mDataDownloadHandler; + + public GsmSMSDispatcher(PhoneBase phone, SmsStorageMonitor storageMonitor, + SmsUsageMonitor usageMonitor) { + super(phone, storageMonitor, usageMonitor); + mDataDownloadHandler = new UsimDataDownloadHandler(mCm); + mCm.setOnNewGsmSms(this, EVENT_NEW_SMS, null); + mCm.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null); + mCm.setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null); + } + + @Override + public void dispose() { + mCm.unSetOnNewGsmSms(this); + mCm.unSetOnSmsStatus(this); + mCm.unSetOnNewGsmBroadcastSms(this); + } + + @Override + protected String getFormat() { + return SmsConstants.FORMAT_3GPP; + } + + /** + * Handles 3GPP format-specific events coming from the phone stack. + * Other events are handled by {@link SMSDispatcher#handleMessage}. + * + * @param msg the message to handle + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_NEW_SMS_STATUS_REPORT: + handleStatusReport((AsyncResult) msg.obj); + break; + + case EVENT_NEW_BROADCAST_SMS: + handleBroadcastSms((AsyncResult)msg.obj); + break; + + case EVENT_WRITE_SMS_COMPLETE: + AsyncResult ar = (AsyncResult) msg.obj; + if (ar.exception == null) { + Log.d(TAG, "Successfully wrote SMS-PP message to UICC"); + mCm.acknowledgeLastIncomingGsmSms(true, 0, null); + } else { + Log.d(TAG, "Failed to write SMS-PP message to UICC", ar.exception); + mCm.acknowledgeLastIncomingGsmSms(false, + CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR, null); + } + break; + + default: + super.handleMessage(msg); + } + } + + /** + * Called when a status report is received. This should correspond to + * a previously successful SEND. + * + * @param ar AsyncResult passed into the message handler. ar.result should + * be a String representing the status report PDU, as ASCII hex. + */ + private void handleStatusReport(AsyncResult ar) { + String pduString = (String) ar.result; + SmsMessage sms = SmsMessage.newFromCDS(pduString); + + if (sms != null) { + int tpStatus = sms.getStatus(); + int messageRef = sms.messageRef; + for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { + SmsTracker tracker = deliveryPendingList.get(i); + if (tracker.mMessageRef == messageRef) { + // Found it. Remove from list and broadcast. + if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) { + deliveryPendingList.remove(i); + } + PendingIntent intent = tracker.mDeliveryIntent; + Intent fillIn = new Intent(); + fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString)); + fillIn.putExtra("format", SmsConstants.FORMAT_3GPP); + try { + intent.send(mContext, Activity.RESULT_OK, fillIn); + } catch (CanceledException ex) {} + + // Only expect to see one tracker matching this messageref + break; + } + } + } + acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null); + } + + /** {@inheritDoc} */ + @Override + public int dispatchMessage(SmsMessageBase smsb) { + + // If sms is null, means there was a parsing error. + if (smsb == null) { + Log.e(TAG, "dispatchMessage: message is null"); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + + SmsMessage sms = (SmsMessage) smsb; + + if (sms.isTypeZero()) { + // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be + // Displayed/Stored/Notified. They should only be acknowledged. + Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack"); + return Intents.RESULT_SMS_HANDLED; + } + + // Send SMS-PP data download messages to UICC. See 3GPP TS 31.111 section 7.1.1. + if (sms.isUsimDataDownload()) { + UsimServiceTable ust = mPhone.getUsimServiceTable(); + // If we receive an SMS-PP message before the UsimServiceTable has been loaded, + // assume that the data download service is not present. This is very unlikely to + // happen because the IMS connection will not be established until after the ISIM + // records have been loaded, after the USIM service table has been loaded. + if (ust != null && ust.isAvailable( + UsimServiceTable.UsimService.DATA_DL_VIA_SMS_PP)) { + Log.d(TAG, "Received SMS-PP data download, sending to UICC."); + return mDataDownloadHandler.startDataDownload(sms); + } else { + Log.d(TAG, "DATA_DL_VIA_SMS_PP service not available, storing message to UICC."); + String smsc = IccUtils.bytesToHexString( + PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength( + sms.getServiceCenterAddress())); + mCm.writeSmsToSim(SmsManager.STATUS_ON_ICC_UNREAD, smsc, + IccUtils.bytesToHexString(sms.getPdu()), + obtainMessage(EVENT_WRITE_SMS_COMPLETE)); + return Activity.RESULT_OK; // acknowledge after response from write to USIM + } + } + + if (mSmsReceiveDisabled) { + // Device doesn't support SMS service, + Log.d(TAG, "Received short message on device which doesn't support " + + "SMS service. Ignored."); + return Intents.RESULT_SMS_HANDLED; + } + + // Special case the message waiting indicator messages + boolean handled = false; + if (sms.isMWISetMessage()) { + mPhone.setVoiceMessageWaiting(1, -1); // line 1: unknown number of msgs waiting + handled = sms.isMwiDontStore(); + if (false) { + Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); + } + } else if (sms.isMWIClearMessage()) { + mPhone.setVoiceMessageWaiting(1, 0); // line 1: no msgs waiting + handled = sms.isMwiDontStore(); + if (false) { + Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); + } + } + + if (handled) { + return Intents.RESULT_SMS_HANDLED; + } + + if (!mStorageMonitor.isStorageAvailable() && + sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) { + // It's a storable message and there's no storage available. Bail. + // (See TS 23.038 for a description of class 0 messages.) + return Intents.RESULT_SMS_OUT_OF_MEMORY; + } + + return dispatchNormalMessage(smsb); + } + + /** {@inheritDoc} */ + @Override + protected void sendData(String destAddr, String scAddr, int destPort, + byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { + SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( + scAddr, destAddr, destPort, data, (deliveryIntent != null)); + if (pdu != null) { + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destAddr); + } else { + Log.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null"); + } + } + + /** {@inheritDoc} */ + @Override + protected void sendText(String destAddr, String scAddr, String text, + PendingIntent sentIntent, PendingIntent deliveryIntent) { + SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( + scAddr, destAddr, text, (deliveryIntent != null)); + if (pdu != null) { + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destAddr); + } else { + Log.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null"); + } + } + + /** {@inheritDoc} */ + @Override + protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody, + boolean use7bitOnly) { + return SmsMessage.calculateLength(messageBody, use7bitOnly); + } + + /** {@inheritDoc} */ + @Override + protected void sendNewSubmitPdu(String destinationAddress, String scAddress, + String message, SmsHeader smsHeader, int encoding, + PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart) { + SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress, + message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader), + encoding, smsHeader.languageTable, smsHeader.languageShiftTable); + if (pdu != null) { + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destinationAddress); + } else { + Log.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null"); + } + } + + /** {@inheritDoc} */ + @Override + protected void sendSms(SmsTracker tracker) { + HashMap<String, Object> map = tracker.mData; + + byte smsc[] = (byte[]) map.get("smsc"); + byte pdu[] = (byte[]) map.get("pdu"); + + Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); + mCm.sendSMS(IccUtils.bytesToHexString(smsc), IccUtils.bytesToHexString(pdu), reply); + } + + /** {@inheritDoc} */ + @Override + protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) { + mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response); + } + + private static int resultToCause(int rc) { + switch (rc) { + case Activity.RESULT_OK: + case Intents.RESULT_SMS_HANDLED: + // Cause code is ignored on success. + return 0; + case Intents.RESULT_SMS_OUT_OF_MEMORY: + return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED; + case Intents.RESULT_SMS_GENERIC_ERROR: + default: + return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR; + } + } + + /** + * Holds all info about a message page needed to assemble a complete + * concatenated message + */ + private static final class SmsCbConcatInfo { + + private final SmsCbHeader mHeader; + private final SmsCbLocation mLocation; + + public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) { + mHeader = header; + mLocation = location; + } + + @Override + public int hashCode() { + return (mHeader.getSerialNumber() * 31) + mLocation.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SmsCbConcatInfo) { + SmsCbConcatInfo other = (SmsCbConcatInfo)obj; + + // Two pages match if they have the same serial number (which includes the + // geographical scope and update number), and both pages belong to the same + // location (PLMN, plus LAC and CID if these are part of the geographical scope). + return mHeader.getSerialNumber() == other.mHeader.getSerialNumber() + && mLocation.equals(other.mLocation); + } + + return false; + } + + /** + * Compare the location code for this message to the current location code. The match is + * relative to the geographical scope of the message, which determines whether the LAC + * and Cell ID are saved in mLocation or set to -1 to match all values. + * + * @param plmn the current PLMN + * @param lac the current Location Area (GSM) or Service Area (UMTS) + * @param cid the current Cell ID + * @return true if this message is valid for the current location; false otherwise + */ + public boolean matchesLocation(String plmn, int lac, int cid) { + return mLocation.isInLocationArea(plmn, lac, cid); + } + } + + // This map holds incomplete concatenated messages waiting for assembly + private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = + new HashMap<SmsCbConcatInfo, byte[][]>(); + + /** + * Handle 3GPP format SMS-CB message. + * @param ar the AsyncResult containing the received PDUs + */ + private void handleBroadcastSms(AsyncResult ar) { + try { + byte[] receivedPdu = (byte[])ar.result; + + if (false) { + for (int i = 0; i < receivedPdu.length; i += 8) { + StringBuilder sb = new StringBuilder("SMS CB pdu data: "); + for (int j = i; j < i + 8 && j < receivedPdu.length; j++) { + int b = receivedPdu[j] & 0xff; + if (b < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString(b)).append(' '); + } + Log.d(TAG, sb.toString()); + } + } + + SmsCbHeader header = new SmsCbHeader(receivedPdu); + String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC); + GsmCellLocation cellLocation = (GsmCellLocation) mPhone.getCellLocation(); + int lac = cellLocation.getLac(); + int cid = cellLocation.getCid(); + + SmsCbLocation location; + switch (header.getGeographicalScope()) { + case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: + location = new SmsCbLocation(plmn, lac, -1); + break; + + case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: + case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: + location = new SmsCbLocation(plmn, lac, cid); + break; + + case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: + default: + location = new SmsCbLocation(plmn); + break; + } + + byte[][] pdus; + int pageCount = header.getNumberOfPages(); + if (pageCount > 1) { + // Multi-page message + SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location); + + // Try to find other pages of the same message + pdus = mSmsCbPageMap.get(concatInfo); + + if (pdus == null) { + // This is the first page of this message, make room for all + // pages and keep until complete + pdus = new byte[pageCount][]; + + mSmsCbPageMap.put(concatInfo, pdus); + } + + // Page parameter is one-based + pdus[header.getPageIndex() - 1] = receivedPdu; + + for (int i = 0; i < pdus.length; i++) { + if (pdus[i] == null) { + // Still missing pages, exit + return; + } + } + + // Message complete, remove and dispatch + mSmsCbPageMap.remove(concatInfo); + } else { + // Single page message + pdus = new byte[1][]; + pdus[0] = receivedPdu; + } + + SmsCbMessage message = GsmSmsCbMessage.createSmsCbMessage(header, location, pdus); + dispatchBroadcastMessage(message); + + // Remove messages that are out of scope to prevent the map from + // growing indefinitely, containing incomplete messages that were + // never assembled + Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); + + while (iter.hasNext()) { + SmsCbConcatInfo info = iter.next(); + + if (!info.matchesLocation(plmn, lac, cid)) { + iter.remove(); + } + } + } catch (RuntimeException e) { + Log.e(TAG, "Error in decoding SMS CB pdu", e); + } + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java b/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java new file mode 100644 index 0000000..56cf53a --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java @@ -0,0 +1,1727 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import com.android.internal.telephony.CommandException; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.DataConnectionTracker; +import com.android.internal.telephony.EventLogTags; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccCardStatus; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.RestrictedState; +import com.android.internal.telephony.RILConstants; +import com.android.internal.telephony.ServiceStateTracker; +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.telephony.TelephonyProperties; + +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.database.ContentObserver; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.PowerManager; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.telephony.gsm.GsmCellLocation; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; +import android.util.TimeUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.TimeZone; + +/** + * {@hide} + */ +final class GsmServiceStateTracker extends ServiceStateTracker { + static final String LOG_TAG = "GSM"; + static final boolean DBG = true; + + GSMPhone phone; + GsmCellLocation cellLoc; + GsmCellLocation newCellLoc; + int mPreferredNetworkType; + + private int gprsState = ServiceState.STATE_OUT_OF_SERVICE; + private int newGPRSState = ServiceState.STATE_OUT_OF_SERVICE; + private int mMaxDataCalls = 1; + private int mNewMaxDataCalls = 1; + private int mReasonDataDenied = -1; + private int mNewReasonDataDenied = -1; + + /** + * GSM roaming status solely based on TS 27.007 7.2 CREG. Only used by + * handlePollStateResult to store CREG roaming result. + */ + private boolean mGsmRoaming = false; + + /** + * Data roaming status solely based on TS 27.007 10.1.19 CGREG. Only used by + * handlePollStateResult to store CGREG roaming result. + */ + private boolean mDataRoaming = false; + + /** + * Mark when service state is in emergency call only mode + */ + private boolean mEmergencyOnly = false; + + /** + * Sometimes we get the NITZ time before we know what country we + * are in. Keep the time zone information from the NITZ string so + * we can fix the time zone once know the country. + */ + private boolean mNeedFixZoneAfterNitz = false; + private int mZoneOffset; + private boolean mZoneDst; + private long mZoneTime; + private boolean mGotCountryCode = false; + private ContentResolver cr; + + /** Boolean is true is setTimeFromNITZString was called */ + private boolean mNitzUpdatedTime = false; + + String mSavedTimeZone; + long mSavedTime; + long mSavedAtTime; + + /** + * We can't register for SIM_RECORDS_LOADED immediately because the + * SIMRecords object may not be instantiated yet. + */ + private boolean mNeedToRegForSimLoaded; + + /** Started the recheck process after finding gprs should registered but not. */ + private boolean mStartedGprsRegCheck = false; + + /** Already sent the event-log for no gprs register. */ + private boolean mReportedGprsNoReg = false; + + /** + * The Notification object given to the NotificationManager. + */ + private Notification mNotification; + + /** Wake lock used while setting time of day. */ + private PowerManager.WakeLock mWakeLock; + private static final String WAKELOCK_TAG = "ServiceStateTracker"; + + /** Keep track of SPN display rules, so we only broadcast intent if something changes. */ + private String curSpn = null; + private String curPlmn = null; + private int curSpnRule = 0; + + /** waiting period before recheck gprs and voice registration. */ + static final int DEFAULT_GPRS_CHECK_PERIOD_MILLIS = 60 * 1000; + + /** Notification type. */ + static final int PS_ENABLED = 1001; // Access Control blocks data service + static final int PS_DISABLED = 1002; // Access Control enables data service + static final int CS_ENABLED = 1003; // Access Control blocks all voice/sms service + static final int CS_DISABLED = 1004; // Access Control enables all voice/sms service + static final int CS_NORMAL_ENABLED = 1005; // Access Control blocks normal voice/sms service + static final int CS_EMERGENCY_ENABLED = 1006; // Access Control blocks emergency call service + + /** Notification id. */ + static final int PS_NOTIFICATION = 888; // Id to update and cancel PS restricted + static final int CS_NOTIFICATION = 999; // Id to update and cancel CS restricted + + private BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_LOCALE_CHANGED)) { + // update emergency string whenever locale changed + updateSpnDisplay(); + } + } + }; + + private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + Log.i("GsmServiceStateTracker", "Auto time state changed"); + revertToNitzTime(); + } + }; + + private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + Log.i("GsmServiceStateTracker", "Auto time zone state changed"); + revertToNitzTimeZone(); + } + }; + + public GsmServiceStateTracker(GSMPhone phone) { + super(); + + this.phone = phone; + cm = phone.mCM; + ss = new ServiceState(); + newSS = new ServiceState(); + cellLoc = new GsmCellLocation(); + newCellLoc = new GsmCellLocation(); + mSignalStrength = new SignalStrength(); + + PowerManager powerManager = + (PowerManager)phone.getContext().getSystemService(Context.POWER_SERVICE); + mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG); + + cm.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null); + cm.registerForRadioStateChanged(this, EVENT_RADIO_STATE_CHANGED, null); + + cm.registerForVoiceNetworkStateChanged(this, EVENT_NETWORK_STATE_CHANGED, null); + cm.setOnNITZTime(this, EVENT_NITZ_TIME, null); + cm.setOnSignalStrengthUpdate(this, EVENT_SIGNAL_STRENGTH_UPDATE, null); + cm.setOnRestrictedStateChanged(this, EVENT_RESTRICTED_STATE_CHANGED, null); + phone.getIccCard().registerForReady(this, EVENT_SIM_READY, null); + + // system setting property AIRPLANE_MODE_ON is set in Settings. + int airplaneMode = Settings.System.getInt( + phone.getContext().getContentResolver(), + Settings.System.AIRPLANE_MODE_ON, 0); + mDesiredPowerState = ! (airplaneMode > 0); + + cr = phone.getContext().getContentResolver(); + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.AUTO_TIME), true, + mAutoTimeObserver); + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.AUTO_TIME_ZONE), true, + mAutoTimeZoneObserver); + + setSignalStrengthDefaultValues(); + mNeedToRegForSimLoaded = true; + + // Monitor locale change + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_LOCALE_CHANGED); + phone.getContext().registerReceiver(mIntentReceiver, filter); + + // Gsm doesn't support OTASP so its not needed + phone.notifyOtaspChanged(OTASP_NOT_NEEDED); + } + + public void dispose() { + // Unregister for all events. + cm.unregisterForAvailable(this); + cm.unregisterForRadioStateChanged(this); + cm.unregisterForVoiceNetworkStateChanged(this); + phone.getIccCard().unregisterForReady(this); + phone.mIccRecords.unregisterForRecordsLoaded(this); + cm.unSetOnSignalStrengthUpdate(this); + cm.unSetOnRestrictedStateChanged(this); + cm.unSetOnNITZTime(this); + cr.unregisterContentObserver(this.mAutoTimeObserver); + cr.unregisterContentObserver(this.mAutoTimeZoneObserver); + } + + protected void finalize() { + if(DBG) log("finalize"); + } + + @Override + protected Phone getPhone() { + return phone; + } + + public void handleMessage (Message msg) { + AsyncResult ar; + int[] ints; + String[] strings; + Message message; + + if (!phone.mIsTheCurrentActivePhone) { + Log.e(LOG_TAG, "Received message " + msg + + "[" + msg.what + "] while being destroyed. Ignoring."); + return; + } + switch (msg.what) { + case EVENT_RADIO_AVAILABLE: + //this is unnecessary + //setPowerStateToDesired(); + break; + + case EVENT_SIM_READY: + // Set the network type, in case the radio does not restore it. + cm.setCurrentPreferredNetworkType(); + + // The SIM is now ready i.e if it was locked + // it has been unlocked. At this stage, the radio is already + // powered on. + if (mNeedToRegForSimLoaded) { + phone.mIccRecords.registerForRecordsLoaded(this, + EVENT_SIM_RECORDS_LOADED, null); + mNeedToRegForSimLoaded = false; + } + + boolean skipRestoringSelection = phone.getContext().getResources().getBoolean( + com.android.internal.R.bool.skip_restoring_network_selection); + + if (!skipRestoringSelection) { + // restore the previous network selection. + phone.restoreSavedNetworkSelection(null); + } + pollState(); + // Signal strength polling stops when radio is off + queueNextSignalStrengthPoll(); + break; + + case EVENT_RADIO_STATE_CHANGED: + // This will do nothing in the radio not + // available case + setPowerStateToDesired(); + pollState(); + break; + + case EVENT_NETWORK_STATE_CHANGED: + pollState(); + break; + + case EVENT_GET_SIGNAL_STRENGTH: + // This callback is called when signal strength is polled + // all by itself + + if (!(cm.getRadioState().isOn())) { + // Polling will continue when radio turns back on + return; + } + ar = (AsyncResult) msg.obj; + onSignalStrengthResult(ar); + queueNextSignalStrengthPoll(); + + break; + + case EVENT_GET_LOC_DONE: + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + String states[] = (String[])ar.result; + int lac = -1; + int cid = -1; + if (states.length >= 3) { + try { + if (states[1] != null && states[1].length() > 0) { + lac = Integer.parseInt(states[1], 16); + } + if (states[2] != null && states[2].length() > 0) { + cid = Integer.parseInt(states[2], 16); + } + } catch (NumberFormatException ex) { + Log.w(LOG_TAG, "error parsing location: " + ex); + } + } + cellLoc.setLacAndCid(lac, cid); + phone.notifyLocationChanged(); + } + + // Release any temporary cell lock, which could have been + // acquired to allow a single-shot location update. + disableSingleLocationUpdate(); + break; + + case EVENT_POLL_STATE_REGISTRATION: + case EVENT_POLL_STATE_GPRS: + case EVENT_POLL_STATE_OPERATOR: + case EVENT_POLL_STATE_NETWORK_SELECTION_MODE: + ar = (AsyncResult) msg.obj; + + handlePollStateResult(msg.what, ar); + break; + + case EVENT_POLL_SIGNAL_STRENGTH: + // Just poll signal strength...not part of pollState() + + cm.getSignalStrength(obtainMessage(EVENT_GET_SIGNAL_STRENGTH)); + break; + + case EVENT_NITZ_TIME: + ar = (AsyncResult) msg.obj; + + String nitzString = (String)((Object[])ar.result)[0]; + long nitzReceiveTime = ((Long)((Object[])ar.result)[1]).longValue(); + + setTimeFromNITZString(nitzString, nitzReceiveTime); + break; + + case EVENT_SIGNAL_STRENGTH_UPDATE: + // This is a notification from + // CommandsInterface.setOnSignalStrengthUpdate + + ar = (AsyncResult) msg.obj; + + // The radio is telling us about signal strength changes + // we don't have to ask it + dontPollSignalStrength = true; + + onSignalStrengthResult(ar); + break; + + case EVENT_SIM_RECORDS_LOADED: + updateSpnDisplay(); + break; + + case EVENT_LOCATION_UPDATES_ENABLED: + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + cm.getVoiceRegistrationState(obtainMessage(EVENT_GET_LOC_DONE, null)); + } + break; + + case EVENT_SET_PREFERRED_NETWORK_TYPE: + ar = (AsyncResult) msg.obj; + // Don't care the result, only use for dereg network (COPS=2) + message = obtainMessage(EVENT_RESET_PREFERRED_NETWORK_TYPE, ar.userObj); + cm.setPreferredNetworkType(mPreferredNetworkType, message); + break; + + case EVENT_RESET_PREFERRED_NETWORK_TYPE: + ar = (AsyncResult) msg.obj; + if (ar.userObj != null) { + AsyncResult.forMessage(((Message) ar.userObj)).exception + = ar.exception; + ((Message) ar.userObj).sendToTarget(); + } + break; + + case EVENT_GET_PREFERRED_NETWORK_TYPE: + ar = (AsyncResult) msg.obj; + + if (ar.exception == null) { + mPreferredNetworkType = ((int[])ar.result)[0]; + } else { + mPreferredNetworkType = RILConstants.NETWORK_MODE_GLOBAL; + } + + message = obtainMessage(EVENT_SET_PREFERRED_NETWORK_TYPE, ar.userObj); + int toggledNetworkType = RILConstants.NETWORK_MODE_GLOBAL; + + cm.setPreferredNetworkType(toggledNetworkType, message); + break; + + case EVENT_CHECK_REPORT_GPRS: + if (ss != null && !isGprsConsistent(gprsState, ss.getState())) { + + // Can't register data service while voice service is ok + // i.e. CREG is ok while CGREG is not + // possible a network or baseband side error + GsmCellLocation loc = ((GsmCellLocation)phone.getCellLocation()); + EventLog.writeEvent(EventLogTags.DATA_NETWORK_REGISTRATION_FAIL, + ss.getOperatorNumeric(), loc != null ? loc.getCid() : -1); + mReportedGprsNoReg = true; + } + mStartedGprsRegCheck = false; + break; + + case EVENT_RESTRICTED_STATE_CHANGED: + // This is a notification from + // CommandsInterface.setOnRestrictedStateChanged + + if (DBG) log("EVENT_RESTRICTED_STATE_CHANGED"); + + ar = (AsyncResult) msg.obj; + + onRestrictedStateChanged(ar); + break; + + default: + super.handleMessage(msg); + break; + } + } + + protected void setPowerStateToDesired() { + // If we want it on and it's off, turn it on + if (mDesiredPowerState + && cm.getRadioState() == CommandsInterface.RadioState.RADIO_OFF) { + cm.setRadioPower(true, null); + } else if (!mDesiredPowerState && cm.getRadioState().isOn()) { + // If it's on and available and we want it off gracefully + DataConnectionTracker dcTracker = phone.mDataConnectionTracker; + powerOffRadioSafely(dcTracker); + } // Otherwise, we're in the desired state + } + + @Override + protected void hangupAndPowerOff() { + // hang up all active voice calls + if (phone.isInCall()) { + phone.mCT.ringingCall.hangupIfAlive(); + phone.mCT.backgroundCall.hangupIfAlive(); + phone.mCT.foregroundCall.hangupIfAlive(); + } + + cm.setRadioPower(false, null); + } + + protected void updateSpnDisplay() { + int rule = phone.mIccRecords.getDisplayRule(ss.getOperatorNumeric()); + String spn = phone.mIccRecords.getServiceProviderName(); + String plmn = ss.getOperatorAlphaLong(); + + // For emergency calls only, pass the EmergencyCallsOnly string via EXTRA_PLMN + if (mEmergencyOnly && cm.getRadioState().isOn()) { + plmn = Resources.getSystem(). + getText(com.android.internal.R.string.emergency_calls_only).toString(); + if (DBG) log("updateSpnDisplay: emergency only and radio is on plmn='" + plmn + "'"); + } + + if (rule != curSpnRule + || !TextUtils.equals(spn, curSpn) + || !TextUtils.equals(plmn, curPlmn)) { + boolean showSpn = !mEmergencyOnly && !TextUtils.isEmpty(spn) + && (rule & SIMRecords.SPN_RULE_SHOW_SPN) == SIMRecords.SPN_RULE_SHOW_SPN; + boolean showPlmn = !TextUtils.isEmpty(plmn) && + (rule & SIMRecords.SPN_RULE_SHOW_PLMN) == SIMRecords.SPN_RULE_SHOW_PLMN; + + if (DBG) { + log(String.format("updateSpnDisplay: changed sending intent" + " rule=" + rule + + " showPlmn='%b' plmn='%s' showSpn='%b' spn='%s'", + showPlmn, plmn, showSpn, spn)); + } + Intent intent = new Intent(TelephonyIntents.SPN_STRINGS_UPDATED_ACTION); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra(TelephonyIntents.EXTRA_SHOW_SPN, showSpn); + intent.putExtra(TelephonyIntents.EXTRA_SPN, spn); + intent.putExtra(TelephonyIntents.EXTRA_SHOW_PLMN, showPlmn); + intent.putExtra(TelephonyIntents.EXTRA_PLMN, plmn); + phone.getContext().sendStickyBroadcast(intent); + } + + curSpnRule = rule; + curSpn = spn; + curPlmn = plmn; + } + + /** + * Handle the result of one of the pollState()-related requests + */ + protected void handlePollStateResult (int what, AsyncResult ar) { + int ints[]; + String states[]; + + // Ignore stale requests from last poll + if (ar.userObj != pollingContext) return; + + if (ar.exception != null) { + CommandException.Error err=null; + + if (ar.exception instanceof CommandException) { + err = ((CommandException)(ar.exception)).getCommandError(); + } + + if (err == CommandException.Error.RADIO_NOT_AVAILABLE) { + // Radio has crashed or turned off + cancelPollState(); + return; + } + + if (!cm.getRadioState().isOn()) { + // Radio has crashed or turned off + cancelPollState(); + return; + } + + if (err != CommandException.Error.OP_NOT_ALLOWED_BEFORE_REG_NW) { + loge("RIL implementation has returned an error where it must succeed" + + ar.exception); + } + } else try { + switch (what) { + case EVENT_POLL_STATE_REGISTRATION: + states = (String[])ar.result; + int lac = -1; + int cid = -1; + int regState = -1; + int reasonRegStateDenied = -1; + int psc = -1; + if (states.length > 0) { + try { + regState = Integer.parseInt(states[0]); + if (states.length >= 3) { + if (states[1] != null && states[1].length() > 0) { + lac = Integer.parseInt(states[1], 16); + } + if (states[2] != null && states[2].length() > 0) { + cid = Integer.parseInt(states[2], 16); + } + } + if (states.length > 14) { + if (states[14] != null && states[14].length() > 0) { + psc = Integer.parseInt(states[14], 16); + } + } + } catch (NumberFormatException ex) { + loge("error parsing RegistrationState: " + ex); + } + } + + mGsmRoaming = regCodeIsRoaming(regState); + newSS.setState (regCodeToServiceState(regState)); + + if (regState == 10 || regState == 12 || regState == 13 || regState == 14) { + mEmergencyOnly = true; + } else { + mEmergencyOnly = false; + } + + // LAC and CID are -1 if not avail + newCellLoc.setLacAndCid(lac, cid); + newCellLoc.setPsc(psc); + break; + + case EVENT_POLL_STATE_GPRS: + states = (String[])ar.result; + + int type = 0; + regState = -1; + mNewReasonDataDenied = -1; + mNewMaxDataCalls = 1; + if (states.length > 0) { + try { + regState = Integer.parseInt(states[0]); + + // states[3] (if present) is the current radio technology + if (states.length >= 4 && states[3] != null) { + type = Integer.parseInt(states[3]); + } + if ((states.length >= 5 ) && (regState == 3)) { + mNewReasonDataDenied = Integer.parseInt(states[4]); + } + if (states.length >= 6) { + mNewMaxDataCalls = Integer.parseInt(states[5]); + } + } catch (NumberFormatException ex) { + loge("error parsing GprsRegistrationState: " + ex); + } + } + newGPRSState = regCodeToServiceState(regState); + mDataRoaming = regCodeIsRoaming(regState); + mNewRilRadioTechnology = type; + newSS.setRadioTechnology(type); + break; + + case EVENT_POLL_STATE_OPERATOR: + String opNames[] = (String[])ar.result; + + if (opNames != null && opNames.length >= 3) { + newSS.setOperatorName (opNames[0], opNames[1], opNames[2]); + } + break; + + case EVENT_POLL_STATE_NETWORK_SELECTION_MODE: + ints = (int[])ar.result; + newSS.setIsManualSelection(ints[0] == 1); + break; + } + + } catch (RuntimeException ex) { + loge("Exception while polling service state. Probably malformed RIL response." + ex); + } + + pollingContext[0]--; + + if (pollingContext[0] == 0) { + /** + * Since the roaming states of gsm service (from +CREG) and + * data service (from +CGREG) could be different, the new SS + * is set roaming while either one is roaming. + * + * There is an exception for the above rule. The new SS is not set + * as roaming while gsm service reports roaming but indeed it is + * not roaming between operators. + */ + boolean roaming = (mGsmRoaming || mDataRoaming); + if (mGsmRoaming && !isRoamingBetweenOperators(mGsmRoaming, newSS)) { + roaming = false; + } + newSS.setRoaming(roaming); + newSS.setEmergencyOnly(mEmergencyOnly); + pollStateDone(); + } + } + + private void setSignalStrengthDefaultValues() { + // TODO Make a constructor only has boolean gsm as parameter + mSignalStrength = new SignalStrength(99, -1, -1, -1, -1, -1, -1, + -1, -1, -1, SignalStrength.INVALID_SNR, -1, true); + } + + /** + * A complete "service state" from our perspective is + * composed of a handful of separate requests to the radio. + * + * We make all of these requests at once, but then abandon them + * and start over again if the radio notifies us that some + * event has changed + */ + private void pollState() { + pollingContext = new int[1]; + pollingContext[0] = 0; + + switch (cm.getRadioState()) { + case RADIO_UNAVAILABLE: + newSS.setStateOutOfService(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + mNitzUpdatedTime = false; + pollStateDone(); + break; + + case RADIO_OFF: + newSS.setStateOff(); + newCellLoc.setStateInvalid(); + setSignalStrengthDefaultValues(); + mGotCountryCode = false; + mNitzUpdatedTime = false; + pollStateDone(); + break; + + default: + // Issue all poll-related commands at once + // then count down the responses, which + // are allowed to arrive out-of-order + + pollingContext[0]++; + cm.getOperator( + obtainMessage( + EVENT_POLL_STATE_OPERATOR, pollingContext)); + + pollingContext[0]++; + cm.getDataRegistrationState( + obtainMessage( + EVENT_POLL_STATE_GPRS, pollingContext)); + + pollingContext[0]++; + cm.getVoiceRegistrationState( + obtainMessage( + EVENT_POLL_STATE_REGISTRATION, pollingContext)); + + pollingContext[0]++; + cm.getNetworkSelectionMode( + obtainMessage( + EVENT_POLL_STATE_NETWORK_SELECTION_MODE, pollingContext)); + break; + } + } + + private void pollStateDone() { + if (DBG) { + log("Poll ServiceState done: " + + " oldSS=[" + ss + "] newSS=[" + newSS + + "] oldGprs=" + gprsState + " newData=" + newGPRSState + + " oldMaxDataCalls=" + mMaxDataCalls + + " mNewMaxDataCalls=" + mNewMaxDataCalls + + " oldReasonDataDenied=" + mReasonDataDenied + + " mNewReasonDataDenied=" + mNewReasonDataDenied + + " oldType=" + ServiceState.rilRadioTechnologyToString(mRilRadioTechnology) + + " newType=" + ServiceState.rilRadioTechnologyToString(mNewRilRadioTechnology)); + } + + boolean hasRegistered = + ss.getState() != ServiceState.STATE_IN_SERVICE + && newSS.getState() == ServiceState.STATE_IN_SERVICE; + + boolean hasDeregistered = + ss.getState() == ServiceState.STATE_IN_SERVICE + && newSS.getState() != ServiceState.STATE_IN_SERVICE; + + boolean hasGprsAttached = + gprsState != ServiceState.STATE_IN_SERVICE + && newGPRSState == ServiceState.STATE_IN_SERVICE; + + boolean hasGprsDetached = + gprsState == ServiceState.STATE_IN_SERVICE + && newGPRSState != ServiceState.STATE_IN_SERVICE; + + boolean hasRadioTechnologyChanged = mRilRadioTechnology != mNewRilRadioTechnology; + + boolean hasChanged = !newSS.equals(ss); + + boolean hasRoamingOn = !ss.getRoaming() && newSS.getRoaming(); + + boolean hasRoamingOff = ss.getRoaming() && !newSS.getRoaming(); + + boolean hasLocationChanged = !newCellLoc.equals(cellLoc); + + // Add an event log when connection state changes + if (ss.getState() != newSS.getState() || gprsState != newGPRSState) { + EventLog.writeEvent(EventLogTags.GSM_SERVICE_STATE_CHANGE, + ss.getState(), gprsState, newSS.getState(), newGPRSState); + } + + ServiceState tss; + tss = ss; + ss = newSS; + newSS = tss; + // clean slate for next time + newSS.setStateOutOfService(); + + GsmCellLocation tcl = cellLoc; + cellLoc = newCellLoc; + newCellLoc = tcl; + + // Add an event log when network type switched + // TODO: we may add filtering to reduce the event logged, + // i.e. check preferred network setting, only switch to 2G, etc + if (hasRadioTechnologyChanged) { + int cid = -1; + GsmCellLocation loc = ((GsmCellLocation)phone.getCellLocation()); + if (loc != null) cid = loc.getCid(); + EventLog.writeEvent(EventLogTags.GSM_RAT_SWITCHED, cid, mRilRadioTechnology, + mNewRilRadioTechnology); + if (DBG) { + log("RAT switched " + ServiceState.rilRadioTechnologyToString(mRilRadioTechnology) + + " -> " + ServiceState.rilRadioTechnologyToString(mNewRilRadioTechnology) + + " at cell " + cid); + } + } + + gprsState = newGPRSState; + mReasonDataDenied = mNewReasonDataDenied; + mMaxDataCalls = mNewMaxDataCalls; + mRilRadioTechnology = mNewRilRadioTechnology; + // this new state has been applied - forget it until we get a new new state + mNewRilRadioTechnology = 0; + + + newSS.setStateOutOfService(); // clean slate for next time + + if (hasRadioTechnologyChanged) { + phone.setSystemProperty(TelephonyProperties.PROPERTY_DATA_NETWORK_TYPE, + ServiceState.rilRadioTechnologyToString(mRilRadioTechnology)); + } + + if (hasRegistered) { + mNetworkAttachedRegistrants.notifyRegistrants(); + + if (DBG) { + log("pollStateDone: registering current mNitzUpdatedTime=" + + mNitzUpdatedTime + " changing to false"); + } + mNitzUpdatedTime = false; + } + + if (hasChanged) { + String operatorNumeric; + + updateSpnDisplay(); + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ALPHA, + ss.getOperatorAlphaLong()); + + String prevOperatorNumeric = + SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, ""); + operatorNumeric = ss.getOperatorNumeric(); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC, operatorNumeric); + + if (operatorNumeric == null) { + if (DBG) log("operatorNumeric is null"); + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, ""); + mGotCountryCode = false; + mNitzUpdatedTime = false; + } else { + String iso = ""; + String mcc = operatorNumeric.substring(0, 3); + try{ + iso = MccTable.countryCodeForMcc(Integer.parseInt(mcc)); + } catch ( NumberFormatException ex){ + loge("pollStateDone: countryCodeForMcc error" + ex); + } catch ( StringIndexOutOfBoundsException ex) { + loge("pollStateDone: countryCodeForMcc error" + ex); + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, iso); + mGotCountryCode = true; + + TimeZone zone = null; + + if (!mNitzUpdatedTime && !mcc.equals("000") && !TextUtils.isEmpty(iso) && + getAutoTimeZone()) { + + // Test both paths if ignore nitz is true + boolean testOneUniqueOffsetPath = SystemProperties.getBoolean( + TelephonyProperties.PROPERTY_IGNORE_NITZ, false) && + ((SystemClock.uptimeMillis() & 1) == 0); + + ArrayList<TimeZone> uniqueZones = TimeUtils.getTimeZonesWithUniqueOffsets(iso); + if ((uniqueZones.size() == 1) || testOneUniqueOffsetPath) { + zone = uniqueZones.get(0); + if (DBG) { + log("pollStateDone: no nitz but one TZ for iso-cc=" + iso + + " with zone.getID=" + zone.getID() + + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath); + } + setAndBroadcastNetworkSetTimeZone(zone.getID()); + } else { + if (DBG) { + log("pollStateDone: there are " + uniqueZones.size() + + " unique offsets for iso-cc='" + iso + + " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath + + "', do nothing"); + } + } + } + + if (shouldFixTimeZoneNow(phone, operatorNumeric, prevOperatorNumeric, + mNeedFixZoneAfterNitz)) { + // If the offset is (0, false) and the timezone property + // is set, use the timezone property rather than + // GMT. + String zoneName = SystemProperties.get(TIMEZONE_PROPERTY); + if (DBG) { + log("pollStateDone: fix time zone zoneName='" + zoneName + + "' mZoneOffset=" + mZoneOffset + " mZoneDst=" + mZoneDst + + " iso-cc='" + iso + + "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, iso)); + } + + // "(mZoneOffset == 0) && (mZoneDst == false) && + // (Arrays.binarySearch(GMT_COUNTRY_CODES, iso) < 0)" + // means that we received a NITZ string telling + // it is in GMT+0 w/ DST time zone + // BUT iso tells is NOT, e.g, a wrong NITZ reporting + // local time w/ 0 offset. + if ((mZoneOffset == 0) && (mZoneDst == false) && + (zoneName != null) && (zoneName.length() > 0) && + (Arrays.binarySearch(GMT_COUNTRY_CODES, iso) < 0)) { + zone = TimeZone.getDefault(); + if (mNeedFixZoneAfterNitz) { + // For wrong NITZ reporting local time w/ 0 offset, + // need adjust time to reflect default timezone setting + long ctm = System.currentTimeMillis(); + long tzOffset = zone.getOffset(ctm); + if (DBG) { + log("pollStateDone: tzOffset=" + tzOffset + " ltod=" + + TimeUtils.logTimeOfDay(ctm)); + } + if (getAutoTime()) { + long adj = ctm - tzOffset; + if (DBG) log("pollStateDone: adj ltod=" + + TimeUtils.logTimeOfDay(adj)); + setAndBroadcastNetworkSetTime(adj); + } else { + // Adjust the saved NITZ time to account for tzOffset. + mSavedTime = mSavedTime - tzOffset; + } + } + if (DBG) log("pollStateDone: using default TimeZone"); + } else if (iso.equals("")){ + // Country code not found. This is likely a test network. + // Get a TimeZone based only on the NITZ parameters (best guess). + zone = getNitzTimeZone(mZoneOffset, mZoneDst, mZoneTime); + if (DBG) log("pollStateDone: using NITZ TimeZone"); + } else { + zone = TimeUtils.getTimeZone(mZoneOffset, mZoneDst, mZoneTime, iso); + if (DBG) log("pollStateDone: using getTimeZone(off, dst, time, iso)"); + } + + mNeedFixZoneAfterNitz = false; + + if (zone != null) { + log("pollStateDone: zone != null zone.getID=" + zone.getID()); + if (getAutoTimeZone()) { + setAndBroadcastNetworkSetTimeZone(zone.getID()); + } + saveNitzTimeZone(zone.getID()); + } else { + log("pollStateDone: zone == null"); + } + } + } + + phone.setSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISROAMING, + ss.getRoaming() ? "true" : "false"); + + phone.notifyServiceStateChanged(ss); + } + + if (hasGprsAttached) { + mAttachedRegistrants.notifyRegistrants(); + } + + if (hasGprsDetached) { + mDetachedRegistrants.notifyRegistrants(); + } + + if (hasRadioTechnologyChanged) { + phone.notifyDataConnection(Phone.REASON_NW_TYPE_CHANGED); + } + + if (hasRoamingOn) { + mRoamingOnRegistrants.notifyRegistrants(); + } + + if (hasRoamingOff) { + mRoamingOffRegistrants.notifyRegistrants(); + } + + if (hasLocationChanged) { + phone.notifyLocationChanged(); + } + + if (! isGprsConsistent(gprsState, ss.getState())) { + if (!mStartedGprsRegCheck && !mReportedGprsNoReg) { + mStartedGprsRegCheck = true; + + int check_period = Settings.Secure.getInt( + phone.getContext().getContentResolver(), + Settings.Secure.GPRS_REGISTER_CHECK_PERIOD_MS, + DEFAULT_GPRS_CHECK_PERIOD_MILLIS); + sendMessageDelayed(obtainMessage(EVENT_CHECK_REPORT_GPRS), + check_period); + } + } else { + mReportedGprsNoReg = false; + } + } + + /** + * Check if GPRS got registered while voice is registered. + * + * @param gprsState for GPRS registration state, i.e. CGREG in GSM + * @param serviceState for voice registration state, i.e. CREG in GSM + * @return false if device only register to voice but not gprs + */ + private boolean isGprsConsistent(int gprsState, int serviceState) { + return !((serviceState == ServiceState.STATE_IN_SERVICE) && + (gprsState != ServiceState.STATE_IN_SERVICE)); + } + + /** + * Returns a TimeZone object based only on parameters from the NITZ string. + */ + private TimeZone getNitzTimeZone(int offset, boolean dst, long when) { + TimeZone guess = findTimeZone(offset, dst, when); + if (guess == null) { + // Couldn't find a proper timezone. Perhaps the DST data is wrong. + guess = findTimeZone(offset, !dst, when); + } + if (DBG) log("getNitzTimeZone returning " + (guess == null ? guess : guess.getID())); + return guess; + } + + private TimeZone findTimeZone(int offset, boolean dst, long when) { + int rawOffset = offset; + if (dst) { + rawOffset -= 3600000; + } + String[] zones = TimeZone.getAvailableIDs(rawOffset); + TimeZone guess = null; + Date d = new Date(when); + for (String zone : zones) { + TimeZone tz = TimeZone.getTimeZone(zone); + if (tz.getOffset(when) == offset && + tz.inDaylightTime(d) == dst) { + guess = tz; + break; + } + } + + return guess; + } + + private void queueNextSignalStrengthPoll() { + if (dontPollSignalStrength) { + // The radio is telling us about signal strength changes + // we don't have to ask it + return; + } + + Message msg; + + msg = obtainMessage(); + msg.what = EVENT_POLL_SIGNAL_STRENGTH; + + long nextTime; + + // TODO Don't poll signal strength if screen is off + sendMessageDelayed(msg, POLL_PERIOD_MILLIS); + } + + /** + * Send signal-strength-changed notification if changed. + * Called both for solicited and unsolicited signal strength updates. + */ + private void onSignalStrengthResult(AsyncResult ar) { + SignalStrength oldSignalStrength = mSignalStrength; + int rssi = 99; + int lteSignalStrength = -1; + int lteRsrp = -1; + int lteRsrq = -1; + int lteRssnr = SignalStrength.INVALID_SNR; + int lteCqi = -1; + + if (ar.exception != null) { + // -1 = unknown + // most likely radio is resetting/disconnected + setSignalStrengthDefaultValues(); + } else { + int[] ints = (int[])ar.result; + + // bug 658816 seems to be a case where the result is 0-length + if (ints.length != 0) { + rssi = ints[0]; + lteSignalStrength = ints[7]; + lteRsrp = ints[8]; + lteRsrq = ints[9]; + lteRssnr = ints[10]; + lteCqi = ints[11]; + } else { + loge("Bogus signal strength response"); + rssi = 99; + } + } + + mSignalStrength = new SignalStrength(rssi, -1, -1, -1, + -1, -1, -1, lteSignalStrength, lteRsrp, lteRsrq, lteRssnr, lteCqi, true); + + if (!mSignalStrength.equals(oldSignalStrength)) { + try { // This takes care of delayed EVENT_POLL_SIGNAL_STRENGTH (scheduled after + // POLL_PERIOD_MILLIS) during Radio Technology Change) + phone.notifySignalStrength(); + } catch (NullPointerException ex) { + log("onSignalStrengthResult() Phone already destroyed: " + ex + + "SignalStrength not notified"); + } + } + } + + /** + * Set restricted state based on the OnRestrictedStateChanged notification + * If any voice or packet restricted state changes, trigger a UI + * notification and notify registrants when sim is ready. + * + * @param ar an int value of RIL_RESTRICTED_STATE_* + */ + private void onRestrictedStateChanged(AsyncResult ar) { + RestrictedState newRs = new RestrictedState(); + + if (DBG) log("onRestrictedStateChanged: E rs "+ mRestrictedState); + + if (ar.exception == null) { + int[] ints = (int[])ar.result; + int state = ints[0]; + + newRs.setCsEmergencyRestricted( + ((state & RILConstants.RIL_RESTRICTED_STATE_CS_EMERGENCY) != 0) || + ((state & RILConstants.RIL_RESTRICTED_STATE_CS_ALL) != 0) ); + //ignore the normal call and data restricted state before SIM READY + if (phone.getIccCard().getState() == IccCardConstants.State.READY) { + newRs.setCsNormalRestricted( + ((state & RILConstants.RIL_RESTRICTED_STATE_CS_NORMAL) != 0) || + ((state & RILConstants.RIL_RESTRICTED_STATE_CS_ALL) != 0) ); + newRs.setPsRestricted( + (state & RILConstants.RIL_RESTRICTED_STATE_PS_ALL)!= 0); + } + + if (DBG) log("onRestrictedStateChanged: new rs "+ newRs); + + if (!mRestrictedState.isPsRestricted() && newRs.isPsRestricted()) { + mPsRestrictEnabledRegistrants.notifyRegistrants(); + setNotification(PS_ENABLED); + } else if (mRestrictedState.isPsRestricted() && !newRs.isPsRestricted()) { + mPsRestrictDisabledRegistrants.notifyRegistrants(); + setNotification(PS_DISABLED); + } + + /** + * There are two kind of cs restriction, normal and emergency. So + * there are 4 x 4 combinations in current and new restricted states + * and we only need to notify when state is changed. + */ + if (mRestrictedState.isCsRestricted()) { + if (!newRs.isCsRestricted()) { + // remove all restriction + setNotification(CS_DISABLED); + } else if (!newRs.isCsNormalRestricted()) { + // remove normal restriction + setNotification(CS_EMERGENCY_ENABLED); + } else if (!newRs.isCsEmergencyRestricted()) { + // remove emergency restriction + setNotification(CS_NORMAL_ENABLED); + } + } else if (mRestrictedState.isCsEmergencyRestricted() && + !mRestrictedState.isCsNormalRestricted()) { + if (!newRs.isCsRestricted()) { + // remove all restriction + setNotification(CS_DISABLED); + } else if (newRs.isCsRestricted()) { + // enable all restriction + setNotification(CS_ENABLED); + } else if (newRs.isCsNormalRestricted()) { + // remove emergency restriction and enable normal restriction + setNotification(CS_NORMAL_ENABLED); + } + } else if (!mRestrictedState.isCsEmergencyRestricted() && + mRestrictedState.isCsNormalRestricted()) { + if (!newRs.isCsRestricted()) { + // remove all restriction + setNotification(CS_DISABLED); + } else if (newRs.isCsRestricted()) { + // enable all restriction + setNotification(CS_ENABLED); + } else if (newRs.isCsEmergencyRestricted()) { + // remove normal restriction and enable emergency restriction + setNotification(CS_EMERGENCY_ENABLED); + } + } else { + if (newRs.isCsRestricted()) { + // enable all restriction + setNotification(CS_ENABLED); + } else if (newRs.isCsEmergencyRestricted()) { + // enable emergency restriction + setNotification(CS_EMERGENCY_ENABLED); + } else if (newRs.isCsNormalRestricted()) { + // enable normal restriction + setNotification(CS_NORMAL_ENABLED); + } + } + + mRestrictedState = newRs; + } + log("onRestrictedStateChanged: X rs "+ mRestrictedState); + } + + /** code is registration state 0-5 from TS 27.007 7.2 */ + private int regCodeToServiceState(int code) { + switch (code) { + case 0: + case 2: // 2 is "searching" + case 3: // 3 is "registration denied" + case 4: // 4 is "unknown" no vaild in current baseband + case 10:// same as 0, but indicates that emergency call is possible. + case 12:// same as 2, but indicates that emergency call is possible. + case 13:// same as 3, but indicates that emergency call is possible. + case 14:// same as 4, but indicates that emergency call is possible. + return ServiceState.STATE_OUT_OF_SERVICE; + + case 1: + return ServiceState.STATE_IN_SERVICE; + + case 5: + // in service, roam + return ServiceState.STATE_IN_SERVICE; + + default: + loge("regCodeToServiceState: unexpected service state " + code); + return ServiceState.STATE_OUT_OF_SERVICE; + } + } + + + /** + * code is registration state 0-5 from TS 27.007 7.2 + * returns true if registered roam, false otherwise + */ + private boolean regCodeIsRoaming (int code) { + // 5 is "in service -- roam" + return 5 == code; + } + + /** + * Set roaming state when gsmRoaming is true and, if operator mcc is the + * same as sim mcc, ons is different from spn + * @param gsmRoaming TS 27.007 7.2 CREG registered roaming + * @param s ServiceState hold current ons + * @return true for roaming state set + */ + private boolean isRoamingBetweenOperators(boolean gsmRoaming, ServiceState s) { + String spn = SystemProperties.get(TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA, "empty"); + + String onsl = s.getOperatorAlphaLong(); + String onss = s.getOperatorAlphaShort(); + + boolean equalsOnsl = onsl != null && spn.equals(onsl); + boolean equalsOnss = onss != null && spn.equals(onss); + + String simNumeric = SystemProperties.get( + TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC, ""); + String operatorNumeric = s.getOperatorNumeric(); + + boolean equalsMcc = true; + try { + equalsMcc = simNumeric.substring(0, 3). + equals(operatorNumeric.substring(0, 3)); + } catch (Exception e){ + } + + return gsmRoaming && !(equalsMcc && (equalsOnsl || equalsOnss)); + } + + private static int twoDigitsAt(String s, int offset) { + int a, b; + + a = Character.digit(s.charAt(offset), 10); + b = Character.digit(s.charAt(offset+1), 10); + + if (a < 0 || b < 0) { + + throw new RuntimeException("invalid format"); + } + + return a*10 + b; + } + + /** + * @return The current GPRS state. IN_SERVICE is the same as "attached" + * and OUT_OF_SERVICE is the same as detached. + */ + int getCurrentGprsState() { + return gprsState; + } + + public int getCurrentDataConnectionState() { + return gprsState; + } + + /** + * @return true if phone is camping on a technology (eg UMTS) + * that could support voice and data simultaneously. + */ + public boolean isConcurrentVoiceAndDataAllowed() { + return (mRilRadioTechnology >= ServiceState.RIL_RADIO_TECHNOLOGY_UMTS); + } + + /** + * Provides the name of the algorithmic time zone for the specified + * offset. Taken from TimeZone.java. + */ + private static String displayNameFor(int off) { + off = off / 1000 / 60; + + char[] buf = new char[9]; + buf[0] = 'G'; + buf[1] = 'M'; + buf[2] = 'T'; + + if (off < 0) { + buf[3] = '-'; + off = -off; + } else { + buf[3] = '+'; + } + + int hours = off / 60; + int minutes = off % 60; + + buf[4] = (char) ('0' + hours / 10); + buf[5] = (char) ('0' + hours % 10); + + buf[6] = ':'; + + buf[7] = (char) ('0' + minutes / 10); + buf[8] = (char) ('0' + minutes % 10); + + return new String(buf); + } + + /** + * nitzReceiveTime is time_t that the NITZ time was posted + */ + private void setTimeFromNITZString (String nitz, long nitzReceiveTime) { + // "yy/mm/dd,hh:mm:ss(+/-)tz" + // tz is in number of quarter-hours + + long start = SystemClock.elapsedRealtime(); + if (DBG) {log("NITZ: " + nitz + "," + nitzReceiveTime + + " start=" + start + " delay=" + (start - nitzReceiveTime)); + } + + try { + /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone + * offset as well (which we won't worry about until later) */ + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT")); + + c.clear(); + c.set(Calendar.DST_OFFSET, 0); + + String[] nitzSubs = nitz.split("[/:,+-]"); + + int year = 2000 + Integer.parseInt(nitzSubs[0]); + c.set(Calendar.YEAR, year); + + // month is 0 based! + int month = Integer.parseInt(nitzSubs[1]) - 1; + c.set(Calendar.MONTH, month); + + int date = Integer.parseInt(nitzSubs[2]); + c.set(Calendar.DATE, date); + + int hour = Integer.parseInt(nitzSubs[3]); + c.set(Calendar.HOUR, hour); + + int minute = Integer.parseInt(nitzSubs[4]); + c.set(Calendar.MINUTE, minute); + + int second = Integer.parseInt(nitzSubs[5]); + c.set(Calendar.SECOND, second); + + boolean sign = (nitz.indexOf('-') == -1); + + int tzOffset = Integer.parseInt(nitzSubs[6]); + + int dst = (nitzSubs.length >= 8 ) ? Integer.parseInt(nitzSubs[7]) + : 0; + + // The zone offset received from NITZ is for current local time, + // so DST correction is already applied. Don't add it again. + // + // tzOffset += dst * 4; + // + // We could unapply it if we wanted the raw offset. + + tzOffset = (sign ? 1 : -1) * tzOffset * 15 * 60 * 1000; + + TimeZone zone = null; + + // As a special extension, the Android emulator appends the name of + // the host computer's timezone to the nitz string. this is zoneinfo + // timezone name of the form Area!Location or Area!Location!SubLocation + // so we need to convert the ! into / + if (nitzSubs.length >= 9) { + String tzname = nitzSubs[8].replace('!','/'); + zone = TimeZone.getTimeZone( tzname ); + } + + String iso = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY); + + if (zone == null) { + + if (mGotCountryCode) { + if (iso != null && iso.length() > 0) { + zone = TimeUtils.getTimeZone(tzOffset, dst != 0, + c.getTimeInMillis(), + iso); + } else { + // We don't have a valid iso country code. This is + // most likely because we're on a test network that's + // using a bogus MCC (eg, "001"), so get a TimeZone + // based only on the NITZ parameters. + zone = getNitzTimeZone(tzOffset, (dst != 0), c.getTimeInMillis()); + } + } + } + + if ((zone == null) || (mZoneOffset != tzOffset) || (mZoneDst != (dst != 0))){ + // We got the time before the country or the zone has changed + // so we don't know how to identify the DST rules yet. Save + // the information and hope to fix it up later. + + mNeedFixZoneAfterNitz = true; + mZoneOffset = tzOffset; + mZoneDst = dst != 0; + mZoneTime = c.getTimeInMillis(); + } + + if (zone != null) { + if (getAutoTimeZone()) { + setAndBroadcastNetworkSetTimeZone(zone.getID()); + } + saveNitzTimeZone(zone.getID()); + } + + String ignore = SystemProperties.get("gsm.ignore-nitz"); + if (ignore != null && ignore.equals("yes")) { + log("NITZ: Not setting clock because gsm.ignore-nitz is set"); + return; + } + + try { + mWakeLock.acquire(); + + if (getAutoTime()) { + long millisSinceNitzReceived + = SystemClock.elapsedRealtime() - nitzReceiveTime; + + if (millisSinceNitzReceived < 0) { + // Sanity check: something is wrong + if (DBG) { + log("NITZ: not setting time, clock has rolled " + + "backwards since NITZ time was received, " + + nitz); + } + return; + } + + if (millisSinceNitzReceived > Integer.MAX_VALUE) { + // If the time is this far off, something is wrong > 24 days! + if (DBG) { + log("NITZ: not setting time, processing has taken " + + (millisSinceNitzReceived / (1000 * 60 * 60 * 24)) + + " days"); + } + return; + } + + // Note: with range checks above, cast to int is safe + c.add(Calendar.MILLISECOND, (int)millisSinceNitzReceived); + + if (DBG) { + log("NITZ: Setting time of day to " + c.getTime() + + " NITZ receive delay(ms): " + millisSinceNitzReceived + + " gained(ms): " + + (c.getTimeInMillis() - System.currentTimeMillis()) + + " from " + nitz); + } + + setAndBroadcastNetworkSetTime(c.getTimeInMillis()); + Log.i(LOG_TAG, "NITZ: after Setting time of day"); + } + SystemProperties.set("gsm.nitz.time", String.valueOf(c.getTimeInMillis())); + saveNitzTime(c.getTimeInMillis()); + if (false) { + long end = SystemClock.elapsedRealtime(); + log("NITZ: end=" + end + " dur=" + (end - start)); + } + mNitzUpdatedTime = true; + } finally { + mWakeLock.release(); + } + } catch (RuntimeException ex) { + loge("NITZ: Parsing NITZ time " + nitz + " ex=" + ex); + } + } + + private boolean getAutoTime() { + try { + return Settings.System.getInt(phone.getContext().getContentResolver(), + Settings.System.AUTO_TIME) > 0; + } catch (SettingNotFoundException snfe) { + return true; + } + } + + private boolean getAutoTimeZone() { + try { + return Settings.System.getInt(phone.getContext().getContentResolver(), + Settings.System.AUTO_TIME_ZONE) > 0; + } catch (SettingNotFoundException snfe) { + return true; + } + } + + private void saveNitzTimeZone(String zoneId) { + mSavedTimeZone = zoneId; + } + + private void saveNitzTime(long time) { + mSavedTime = time; + mSavedAtTime = SystemClock.elapsedRealtime(); + } + + /** + * Set the timezone and send out a sticky broadcast so the system can + * determine if the timezone was set by the carrier. + * + * @param zoneId timezone set by carrier + */ + private void setAndBroadcastNetworkSetTimeZone(String zoneId) { + if (DBG) log("setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId); + AlarmManager alarm = + (AlarmManager) phone.getContext().getSystemService(Context.ALARM_SERVICE); + alarm.setTimeZone(zoneId); + Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time-zone", zoneId); + phone.getContext().sendStickyBroadcast(intent); + if (DBG) { + log("setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast zoneId=" + + zoneId); + } + } + + /** + * Set the time and Send out a sticky broadcast so the system can determine + * if the time was set by the carrier. + * + * @param time time set by network + */ + private void setAndBroadcastNetworkSetTime(long time) { + if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms"); + SystemClock.setCurrentTimeMillis(time); + Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + intent.putExtra("time", time); + phone.getContext().sendStickyBroadcast(intent); + } + + private void revertToNitzTime() { + if (Settings.System.getInt(phone.getContext().getContentResolver(), + Settings.System.AUTO_TIME, 0) == 0) { + return; + } + if (DBG) { + log("Reverting to NITZ Time: mSavedTime=" + mSavedTime + + " mSavedAtTime=" + mSavedAtTime); + } + if (mSavedTime != 0 && mSavedAtTime != 0) { + setAndBroadcastNetworkSetTime(mSavedTime + + (SystemClock.elapsedRealtime() - mSavedAtTime)); + } + } + + private void revertToNitzTimeZone() { + if (Settings.System.getInt(phone.getContext().getContentResolver(), + Settings.System.AUTO_TIME_ZONE, 0) == 0) { + return; + } + if (DBG) log("Reverting to NITZ TimeZone: tz='" + mSavedTimeZone); + if (mSavedTimeZone != null) { + setAndBroadcastNetworkSetTimeZone(mSavedTimeZone); + } + } + + /** + * Post a notification to NotificationManager for restricted state + * + * @param notifyType is one state of PS/CS_*_ENABLE/DISABLE + */ + private void setNotification(int notifyType) { + + if (DBG) log("setNotification: create notification " + notifyType); + Context context = phone.getContext(); + + mNotification = new Notification(); + mNotification.when = System.currentTimeMillis(); + mNotification.flags = Notification.FLAG_AUTO_CANCEL; + mNotification.icon = com.android.internal.R.drawable.stat_sys_warning; + Intent intent = new Intent(); + mNotification.contentIntent = PendingIntent + .getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + + CharSequence details = ""; + CharSequence title = context.getText(com.android.internal.R.string.RestrictedChangedTitle); + int notificationId = CS_NOTIFICATION; + + switch (notifyType) { + case PS_ENABLED: + notificationId = PS_NOTIFICATION; + details = context.getText(com.android.internal.R.string.RestrictedOnData);; + break; + case PS_DISABLED: + notificationId = PS_NOTIFICATION; + break; + case CS_ENABLED: + details = context.getText(com.android.internal.R.string.RestrictedOnAllVoice);; + break; + case CS_NORMAL_ENABLED: + details = context.getText(com.android.internal.R.string.RestrictedOnNormal);; + break; + case CS_EMERGENCY_ENABLED: + details = context.getText(com.android.internal.R.string.RestrictedOnEmergency);; + break; + case CS_DISABLED: + // do nothing and cancel the notification later + break; + } + + if (DBG) log("setNotification: put notification " + title + " / " +details); + mNotification.tickerText = title; + mNotification.setLatestEventInfo(context, title, details, + mNotification.contentIntent); + + NotificationManager notificationManager = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (notifyType == PS_DISABLED || notifyType == CS_DISABLED) { + // cancel previous post notification + notificationManager.cancel(notificationId); + } else { + // update restricted state notification + notificationManager.notify(notificationId, mNotification); + } + } + + @Override + protected void log(String s) { + Log.d(LOG_TAG, "[GsmSST] " + s); + } + + @Override + protected void loge(String s) { + Log.e(LOG_TAG, "[GsmSST] " + s); + } + + private static void sloge(String s) { + Log.e(LOG_TAG, "[GsmSST] " + s); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("GsmServiceStateTracker extends:"); + super.dump(fd, pw, args); + pw.println(" phone=" + phone); + pw.println(" cellLoc=" + cellLoc); + pw.println(" newCellLoc=" + newCellLoc); + pw.println(" mPreferredNetworkType=" + mPreferredNetworkType); + pw.println(" gprsState=" + gprsState); + pw.println(" newGPRSState=" + newGPRSState); + pw.println(" mMaxDataCalls=" + mMaxDataCalls); + pw.println(" mNewMaxDataCalls=" + mNewMaxDataCalls); + pw.println(" mReasonDataDenied=" + mReasonDataDenied); + pw.println(" mNewReasonDataDenied=" + mNewReasonDataDenied); + pw.println(" mGsmRoaming=" + mGsmRoaming); + pw.println(" mDataRoaming=" + mDataRoaming); + pw.println(" mEmergencyOnly=" + mEmergencyOnly); + pw.println(" mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz); + pw.println(" mZoneOffset=" + mZoneOffset); + pw.println(" mZoneDst=" + mZoneDst); + pw.println(" mZoneTime=" + mZoneTime); + pw.println(" mGotCountryCode=" + mGotCountryCode); + pw.println(" mNitzUpdatedTime=" + mNitzUpdatedTime); + pw.println(" mSavedTimeZone=" + mSavedTimeZone); + pw.println(" mSavedTime=" + mSavedTime); + pw.println(" mSavedAtTime=" + mSavedAtTime); + pw.println(" mNeedToRegForSimLoaded=" + mNeedToRegForSimLoaded); + pw.println(" mStartedGprsRegCheck=" + mStartedGprsRegCheck); + pw.println(" mReportedGprsNoReg=" + mReportedGprsNoReg); + pw.println(" mNotification=" + mNotification); + pw.println(" mWakeLock=" + mWakeLock); + pw.println(" curSpn=" + curSpn); + pw.println(" curPlmn=" + curPlmn); + pw.println(" curSpnRule=" + curSpnRule); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmSmsAddress.java b/src/java/com/android/internal/telephony/gsm/GsmSmsAddress.java new file mode 100644 index 0000000..c163803 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmSmsAddress.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.telephony.PhoneNumberUtils; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsAddress; + +public class GsmSmsAddress extends SmsAddress { + + static final int OFFSET_ADDRESS_LENGTH = 0; + + static final int OFFSET_TOA = 1; + + static final int OFFSET_ADDRESS_VALUE = 2; + + /** + * New GsmSmsAddress from TS 23.040 9.1.2.5 Address Field + * + * @param offset the offset of the Address-Length byte + * @param length the length in bytes rounded up, e.g. "2 + + * (addressLength + 1) / 2" + */ + + public GsmSmsAddress(byte[] data, int offset, int length) { + origBytes = new byte[length]; + System.arraycopy(data, offset, origBytes, 0, length); + + // addressLength is the count of semi-octets, not bytes + int addressLength = origBytes[OFFSET_ADDRESS_LENGTH] & 0xff; + + int toa = origBytes[OFFSET_TOA] & 0xff; + ton = 0x7 & (toa >> 4); + + // TOA must have its high bit set + if ((toa & 0x80) != 0x80) { + throw new RuntimeException("Invalid TOA - high bit must be set"); + } + + if (isAlphanumeric()) { + // An alphanumeric address + int countSeptets = addressLength * 4 / 7; + + address = GsmAlphabet.gsm7BitPackedToString(origBytes, + OFFSET_ADDRESS_VALUE, countSeptets); + } else { + // TS 23.040 9.1.2.5 says + // that "the MS shall interpret reserved values as 'Unknown' + // but shall store them exactly as received" + + byte lastByte = origBytes[length - 1]; + + if ((addressLength & 1) == 1) { + // Make sure the final unused BCD digit is 0xf + origBytes[length - 1] |= 0xf0; + } + address = PhoneNumberUtils.calledPartyBCDToString(origBytes, + OFFSET_TOA, length - OFFSET_TOA); + + // And restore origBytes + origBytes[length - 1] = lastByte; + } + } + + public String getAddressString() { + return address; + } + + /** + * Returns true if this is an alphanumeric address + */ + public boolean isAlphanumeric() { + return ton == TON_ALPHANUMERIC; + } + + public boolean isNetworkSpecific() { + return ton == TON_NETWORK; + } + + /** + * Returns true of this is a valid CPHS voice message waiting indicator + * address + */ + public boolean isCphsVoiceMessageIndicatorAddress() { + // CPHS-style MWI message + // See CPHS 4.7 B.4.2.1 + // + // Basically: + // + // - Originating address should be 4 bytes long and alphanumeric + // - Decode will result with two chars: + // - Char 1 + // 76543210 + // ^ set/clear indicator (0 = clear) + // ^^^ type of indicator (000 = voice) + // ^^^^ must be equal to 0001 + // - Char 2: + // 76543210 + // ^ line number (0 = line 1) + // ^^^^^^^ set to 0 + // + // Remember, since the alpha address is stored in 7-bit compact form, + // the "line number" is really the top bit of the first address value + // byte + + return (origBytes[OFFSET_ADDRESS_LENGTH] & 0xff) == 4 + && isAlphanumeric() && (origBytes[OFFSET_TOA] & 0x0f) == 0; + } + + /** + * Returns true if this is a valid CPHS voice message waiting indicator + * address indicating a "set" of "indicator 1" of type "voice message + * waiting" + */ + public boolean isCphsVoiceMessageSet() { + // 0x11 means "set" "voice message waiting" "indicator 1" + return isCphsVoiceMessageIndicatorAddress() + && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x11; + + } + + /** + * Returns true if this is a valid CPHS voice message waiting indicator + * address indicating a "clear" of "indicator 1" of type "voice message + * waiting" + */ + public boolean isCphsVoiceMessageClear() { + // 0x10 means "clear" "voice message waiting" "indicator 1" + return isCphsVoiceMessageIndicatorAddress() + && (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x10; + + } +} diff --git a/src/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/src/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java new file mode 100644 index 0000000..78590fb --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2012 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.internal.telephony.gsm; + +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.util.Pair; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsConstants; + +import java.io.UnsupportedEncodingException; + +/** + * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is + * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. + */ +public class GsmSmsCbMessage { + + /** + * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_0 = { + "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu", + "pl", null + }; + + /** + * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_2 = { + "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null, + null, null + }; + + private static final char CARRIAGE_RETURN = 0x0d; + + private static final int PDU_BODY_PAGE_LENGTH = 82; + + /** Utility class with only static methods. */ + private GsmSmsCbMessage() { } + + /** + * Create a new SmsCbMessage object from a header object plus one or more received PDUs. + * + * @param pdus PDU bytes + */ + static SmsCbMessage createSmsCbMessage(SmsCbHeader header, SmsCbLocation location, + byte[][] pdus) throws IllegalArgumentException { + if (header.isEtwsPrimaryNotification()) { + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, + header.getGeographicalScope(), header.getSerialNumber(), + location, header.getServiceCategory(), + null, "ETWS", SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, + header.getEtwsInfo(), header.getCmasInfo()); + } else { + String language = null; + StringBuilder sb = new StringBuilder(); + for (byte[] pdu : pdus) { + Pair<String, String> p = parseBody(header, pdu); + language = p.first; + sb.append(p.second); + } + int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY + : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; + + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, + header.getGeographicalScope(), header.getSerialNumber(), location, + header.getServiceCategory(), language, sb.toString(), priority, + header.getEtwsInfo(), header.getCmasInfo()); + } + } + + /** + * Create a new SmsCbMessage object from one or more received PDUs. This is used by some + * CellBroadcastReceiver test cases, because SmsCbHeader is now package local. + * + * @param location the location (geographical scope) for the message + * @param pdus PDU bytes + */ + public static SmsCbMessage createSmsCbMessage(SmsCbLocation location, byte[][] pdus) + throws IllegalArgumentException { + SmsCbHeader header = new SmsCbHeader(pdus[0]); + return createSmsCbMessage(header, location, pdus); + } + + /** + * Parse and unpack the body text according to the encoding in the DCS. + * After completing successfully this method will have assigned the body + * text into mBody, and optionally the language code into mLanguage + * + * @param header the message header to use + * @param pdu the PDU to decode + * @return a Pair of Strings containing the language and body of the message + */ + private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) { + int encoding; + String language = null; + boolean hasLanguageIndicator = false; + int dataCodingScheme = header.getDataCodingScheme(); + + // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, + // section 5. + switch ((dataCodingScheme & 0xf0) >> 4) { + case 0x00: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; + break; + + case 0x01: + hasLanguageIndicator = true; + if ((dataCodingScheme & 0x0f) == 0x01) { + encoding = SmsConstants.ENCODING_16BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + case 0x02: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; + break; + + case 0x03: + encoding = SmsConstants.ENCODING_7BIT; + break; + + case 0x04: + case 0x05: + switch ((dataCodingScheme & 0x0c) >> 2) { + case 0x01: + encoding = SmsConstants.ENCODING_8BIT; + break; + + case 0x02: + encoding = SmsConstants.ENCODING_16BIT; + break; + + case 0x00: + default: + encoding = SmsConstants.ENCODING_7BIT; + break; + } + break; + + case 0x06: + case 0x07: + // Compression not supported + case 0x09: + // UDH structure not supported + case 0x0e: + // Defined by the WAP forum not supported + throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " + + dataCodingScheme); + + case 0x0f: + if (((dataCodingScheme & 0x04) >> 2) == 0x01) { + encoding = SmsConstants.ENCODING_8BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + default: + // Reserved values are to be treated as 7-bit + encoding = SmsConstants.ENCODING_7BIT; + break; + } + + if (header.isUmtsFormat()) { + // Payload may contain multiple pages + int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; + + if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) + * nrPages) { + throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " + + nrPages + " pages"); + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < nrPages; i++) { + // Each page is 82 bytes followed by a length octet indicating + // the number of useful octets within those 82 + int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; + int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; + + if (length > PDU_BODY_PAGE_LENGTH) { + throw new IllegalArgumentException("Page length " + length + + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); + } + + Pair<String, String> p = unpackBody(pdu, encoding, offset, length, + hasLanguageIndicator, language); + language = p.first; + sb.append(p.second); + } + return new Pair<String, String>(language, sb.toString()); + } else { + // Payload is one single page + int offset = SmsCbHeader.PDU_HEADER_LENGTH; + int length = pdu.length - offset; + + return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language); + } + } + + /** + * Unpack body text from the pdu using the given encoding, position and + * length within the pdu + * + * @param pdu The pdu + * @param encoding The encoding, as derived from the DCS + * @param offset Position of the first byte to unpack + * @param length Number of bytes to unpack + * @param hasLanguageIndicator true if the body text is preceded by a + * language indicator. If so, this method will as a side-effect + * assign the extracted language code into mLanguage + * @param language the language to return if hasLanguageIndicator is false + * @return a Pair of Strings containing the language and body of the message + */ + private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length, + boolean hasLanguageIndicator, String language) { + String body = null; + + switch (encoding) { + case SmsConstants.ENCODING_7BIT: + body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); + + if (hasLanguageIndicator && body != null && body.length() > 2) { + // Language is two GSM characters followed by a CR. + // The actual body text is offset by 3 characters. + language = body.substring(0, 2); + body = body.substring(3); + } + break; + + case SmsConstants.ENCODING_16BIT: + if (hasLanguageIndicator && pdu.length >= offset + 2) { + // Language is two GSM characters. + // The actual body text is offset by 2 bytes. + language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); + offset += 2; + length -= 2; + } + + try { + body = new String(pdu, offset, (length & 0xfffe), "utf-16"); + } catch (UnsupportedEncodingException e) { + // Apparently it wasn't valid UTF-16. + throw new IllegalArgumentException("Error decoding UTF-16 message", e); + } + break; + + default: + break; + } + + if (body != null) { + // Remove trailing carriage return + for (int i = body.length() - 1; i >= 0; i--) { + if (body.charAt(i) != CARRIAGE_RETURN) { + body = body.substring(0, i + 1); + break; + } + } + } else { + body = ""; + } + + return new Pair<String, String>(language, body); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SIMFileHandler.java b/src/java/com/android/internal/telephony/gsm/SIMFileHandler.java new file mode 100644 index 0000000..dcc9cfd --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SIMFileHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccCardApplication; +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.PhoneBase; + +/** + * {@hide} + */ +public final class SIMFileHandler extends IccFileHandler implements IccConstants { + static final String LOG_TAG = "GSM"; + + //***** Instance Variables + + //***** Constructor + + public SIMFileHandler(IccCard card, String aid, CommandsInterface ci) { + super(card, aid, ci); + } + + protected void finalize() { + Log.d(LOG_TAG, "SIMFileHandler finalized"); + } + + //***** Overridden from IccFileHandler + + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + } + + protected String getEFPath(int efid) { + // TODO(): DF_GSM can be 7F20 or 7F21 to handle backward compatibility. + // Implement this after discussion with OEMs. + switch(efid) { + case EF_SMS: + return MF_SIM + DF_TELECOM; + + case EF_EXT6: + case EF_MWIS: + case EF_MBI: + case EF_SPN: + case EF_AD: + case EF_MBDN: + case EF_PNN: + case EF_SPDI: + case EF_SST: + case EF_CFIS: + return MF_SIM + DF_GSM; + + case EF_MAILBOX_CPHS: + case EF_VOICE_MAIL_INDICATOR_CPHS: + case EF_CFF_CPHS: + case EF_SPN_CPHS: + case EF_SPN_SHORT_CPHS: + case EF_INFO_CPHS: + case EF_CSP_CPHS: + return MF_SIM + DF_GSM; + + case EF_PBR: + // we only support global phonebook. + return MF_SIM + DF_TELECOM + DF_PHONEBOOK; + } + String path = getCommonIccEFPath(efid); + if (path == null) { + // The EFids in USIM phone book entries are decided by the card manufacturer. + // So if we don't match any of the cases above and if its a USIM return + // the phone book path. + if (mParentCard != null + && mParentCard.isApplicationOnIcc(IccCardApplication.AppType.APPTYPE_USIM)) { + return MF_SIM + DF_TELECOM + DF_PHONEBOOK; + } + Log.e(LOG_TAG, "Error: EF Path being returned in null"); + } + return path; + } + + protected void logd(String msg) { + Log.d(LOG_TAG, "[SIMFileHandler] " + msg); + } + + protected void loge(String msg) { + Log.e(LOG_TAG, "[SIMFileHandler] " + msg); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SIMRecords.java b/src/java/com/android/internal/telephony/gsm/SIMRecords.java new file mode 100755 index 0000000..1ec3ea7 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SIMRecords.java @@ -0,0 +1,1675 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ALPHA; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_ISO_COUNTRY; +import static com.android.internal.telephony.TelephonyProperties.PROPERTY_ICC_OPERATOR_NUMERIC; +import android.content.Context; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.SystemProperties; +import android.util.Log; + +import com.android.internal.telephony.AdnRecord; +import com.android.internal.telephony.AdnRecordCache; +import com.android.internal.telephony.AdnRecordLoader; +import com.android.internal.telephony.BaseCommands; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccCardConstants; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccRecords; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.IccVmFixedException; +import com.android.internal.telephony.IccVmNotSupportedException; +import com.android.internal.telephony.MccTable; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.SmsMessageBase; +import com.android.internal.telephony.IccRefreshResponse; + +import java.util.ArrayList; + + +/** + * {@hide} + */ +public class SIMRecords extends IccRecords { + protected static final String LOG_TAG = "GSM"; + + private static final boolean CRASH_RIL = false; + + protected static final boolean DBG = true; + + // ***** Instance Variables + + VoiceMailConstants mVmConfig; + + + SpnOverride mSpnOverride; + + // ***** Cached SIM State; cleared on channel close + + private String imsi; + private boolean callForwardingEnabled; + + + /** + * States only used by getSpnFsm FSM + */ + private Get_Spn_Fsm_State spnState; + + /** CPHS service information (See CPHS 4.2 B.3.1.1) + * It will be set in onSimReady if reading GET_CPHS_INFO successfully + * mCphsInfo[0] is CPHS Phase + * mCphsInfo[1] and mCphsInfo[2] is CPHS Service Table + */ + private byte[] mCphsInfo = null; + boolean mCspPlmnEnabled = true; + + byte[] efMWIS = null; + byte[] efCPHS_MWI =null; + byte[] mEfCff = null; + byte[] mEfCfis = null; + + + int spnDisplayCondition; + // Numeric network codes listed in TS 51.011 EF[SPDI] + ArrayList<String> spdiNetworks = null; + + String pnnHomeName = null; + + UsimServiceTable mUsimServiceTable; + + // ***** Constants + + // Bitmasks for SPN display rules. + static final int SPN_RULE_SHOW_SPN = 0x01; + static final int SPN_RULE_SHOW_PLMN = 0x02; + + // From TS 51.011 EF[SPDI] section + static final int TAG_SPDI = 0xA3; + static final int TAG_SPDI_PLMN_LIST = 0x80; + + // Full Name IEI from TS 24.008 + static final int TAG_FULL_NETWORK_NAME = 0x43; + + // Short Name IEI from TS 24.008 + static final int TAG_SHORT_NETWORK_NAME = 0x45; + + // active CFF from CPHS 4.2 B.4.5 + static final int CFF_UNCONDITIONAL_ACTIVE = 0x0a; + static final int CFF_UNCONDITIONAL_DEACTIVE = 0x05; + static final int CFF_LINE1_MASK = 0x0f; + static final int CFF_LINE1_RESET = 0xf0; + + // CPHS Service Table (See CPHS 4.2 B.3.1) + private static final int CPHS_SST_MBN_MASK = 0x30; + private static final int CPHS_SST_MBN_ENABLED = 0x30; + + // ***** Event Constants + + private static final int EVENT_RADIO_OFF_OR_NOT_AVAILABLE = 2; + protected static final int EVENT_GET_IMSI_DONE = 3; + protected static final int EVENT_GET_ICCID_DONE = 4; + private static final int EVENT_GET_MBI_DONE = 5; + private static final int EVENT_GET_MBDN_DONE = 6; + private static final int EVENT_GET_MWIS_DONE = 7; + private static final int EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE = 8; + protected static final int EVENT_GET_AD_DONE = 9; // Admin data on SIM + protected static final int EVENT_GET_MSISDN_DONE = 10; + private static final int EVENT_GET_CPHS_MAILBOX_DONE = 11; + private static final int EVENT_GET_SPN_DONE = 12; + private static final int EVENT_GET_SPDI_DONE = 13; + private static final int EVENT_UPDATE_DONE = 14; + private static final int EVENT_GET_PNN_DONE = 15; + protected static final int EVENT_GET_SST_DONE = 17; + private static final int EVENT_GET_ALL_SMS_DONE = 18; + private static final int EVENT_MARK_SMS_READ_DONE = 19; + private static final int EVENT_SET_MBDN_DONE = 20; + private static final int EVENT_SMS_ON_SIM = 21; + private static final int EVENT_GET_SMS_DONE = 22; + private static final int EVENT_GET_CFF_DONE = 24; + private static final int EVENT_SET_CPHS_MAILBOX_DONE = 25; + private static final int EVENT_GET_INFO_CPHS_DONE = 26; + private static final int EVENT_SET_MSISDN_DONE = 30; + private static final int EVENT_SIM_REFRESH = 31; + private static final int EVENT_GET_CFIS_DONE = 32; + private static final int EVENT_GET_CSP_CPHS_DONE = 33; + + // Lookup table for carriers known to produce SIMs which incorrectly indicate MNC length. + + private static final String[] MCCMNC_CODES_HAVING_3DIGITS_MNC = { + "405025", "405026", "405027", "405028", "405029", "405030", "405031", "405032", + "405033", "405034", "405035", "405036", "405037", "405038", "405039", "405040", + "405041", "405042", "405043", "405044", "405045", "405046", "405047", "405750", + "405751", "405752", "405753", "405754", "405755", "405756", "405799", "405800", + "405801", "405802", "405803", "405804", "405805", "405806", "405807", "405808", + "405809", "405810", "405811", "405812", "405813", "405814", "405815", "405816", + "405817", "405818", "405819", "405820", "405821", "405822", "405823", "405824", + "405825", "405826", "405827", "405828", "405829", "405830", "405831", "405832", + "405833", "405834", "405835", "405836", "405837", "405838", "405839", "405840", + "405841", "405842", "405843", "405844", "405845", "405846", "405847", "405848", + "405849", "405850", "405851", "405852", "405853", "405875", "405876", "405877", + "405878", "405879", "405880", "405881", "405882", "405883", "405884", "405885", + "405886", "405908", "405909", "405910", "405911", "405912", "405913", "405914", + "405915", "405916", "405917", "405918", "405919", "405920", "405921", "405922", + "405923", "405924", "405925", "405926", "405927", "405928", "405929", "405930", + "405931", "405932" + }; + + // ***** Constructor + + public SIMRecords(IccCard card, Context c, CommandsInterface ci) { + super(card, c, ci); + + adnCache = new AdnRecordCache(mFh); + + mVmConfig = new VoiceMailConstants(); + mSpnOverride = new SpnOverride(); + + recordsRequested = false; // No load request is made till SIM ready + + // recordsToLoad is set to 0 because no requests are made yet + recordsToLoad = 0; + + mCi.registerForOffOrNotAvailable( + this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null); + mCi.setOnSmsOnSim(this, EVENT_SMS_ON_SIM, null); + mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null); + + // Start off by setting empty state + onRadioOffOrNotAvailable(); + + } + + @Override + public void dispose() { + if (DBG) log("Disposing SIMRecords " + this); + //Unregister for all events + mCi.unregisterForOffOrNotAvailable( this); + mCi.unregisterForIccRefresh(this); + mCi.unSetOnSmsOnSim(this); + super.dispose(); + } + + protected void finalize() { + if(DBG) log("finalized"); + } + + protected void onRadioOffOrNotAvailable() { + imsi = null; + msisdn = null; + voiceMailNum = null; + countVoiceMessages = 0; + mncLength = UNINITIALIZED; + iccid = null; + // -1 means no EF_SPN found; treat accordingly. + spnDisplayCondition = -1; + efMWIS = null; + efCPHS_MWI = null; + spdiNetworks = null; + pnnHomeName = null; + + adnCache.reset(); + + log("SIMRecords: onRadioOffOrNotAvailable set 'gsm.sim.operator.numeric' to operator=null"); + SystemProperties.set(PROPERTY_ICC_OPERATOR_NUMERIC, null); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ALPHA, null); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ISO_COUNTRY, null); + + // recordsRequested is set to false indicating that the SIM + // read requests made so far are not valid. This is set to + // true only when fresh set of read requests are made. + recordsRequested = false; + } + + + //***** Public Methods + + /** + * {@inheritDoc} + */ + @Override + public String getIMSI() { + return imsi; + } + + public String getMsisdnNumber() { + return msisdn; + } + + @Override + public UsimServiceTable getUsimServiceTable() { + return mUsimServiceTable; + } + + /** + * Set subscriber number to SIM record + * + * The subscriber number is stored in EF_MSISDN (TS 51.011) + * + * When the operation is complete, onComplete will be sent to its handler + * + * @param alphaTag alpha-tagging of the dailing nubmer (up to 10 characters) + * @param number dailing nubmer (up to 20 digits) + * if the number starts with '+', then set to international TOA + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void setMsisdnNumber(String alphaTag, String number, + Message onComplete) { + + msisdn = number; + msisdnTag = alphaTag; + + if(DBG) log("Set MSISDN: " + msisdnTag + " " + /*msisdn*/ "xxxxxxx"); + + + AdnRecord adn = new AdnRecord(msisdnTag, msisdn); + + new AdnRecordLoader(mFh).updateEF(adn, EF_MSISDN, EF_EXT1, 1, null, + obtainMessage(EVENT_SET_MSISDN_DONE, onComplete)); + } + + public String getMsisdnAlphaTag() { + return msisdnTag; + } + + public String getVoiceMailNumber() { + return voiceMailNum; + } + + /** + * Set voice mail number to SIM record + * + * The voice mail number can be stored either in EF_MBDN (TS 51.011) or + * EF_MAILBOX_CPHS (CPHS 4.2) + * + * If EF_MBDN is available, store the voice mail number to EF_MBDN + * + * If EF_MAILBOX_CPHS is enabled, store the voice mail number to EF_CHPS + * + * So the voice mail number will be stored in both EFs if both are available + * + * Return error only if both EF_MBDN and EF_MAILBOX_CPHS fail. + * + * When the operation is complete, onComplete will be sent to its handler + * + * @param alphaTag alpha-tagging of the dailing nubmer (upto 10 characters) + * @param voiceNumber dailing nubmer (upto 20 digits) + * if the number is start with '+', then set to international TOA + * @param onComplete + * onComplete.obj will be an AsyncResult + * ((AsyncResult)onComplete.obj).exception == null on success + * ((AsyncResult)onComplete.obj).exception != null on fail + */ + public void setVoiceMailNumber(String alphaTag, String voiceNumber, + Message onComplete) { + if (isVoiceMailFixed) { + AsyncResult.forMessage((onComplete)).exception = + new IccVmFixedException("Voicemail number is fixed by operator"); + onComplete.sendToTarget(); + return; + } + + newVoiceMailNum = voiceNumber; + newVoiceMailTag = alphaTag; + + AdnRecord adn = new AdnRecord(newVoiceMailTag, newVoiceMailNum); + + if (mailboxIndex != 0 && mailboxIndex != 0xff) { + + new AdnRecordLoader(mFh).updateEF(adn, EF_MBDN, EF_EXT6, + mailboxIndex, null, + obtainMessage(EVENT_SET_MBDN_DONE, onComplete)); + + } else if (isCphsMailboxEnabled()) { + + new AdnRecordLoader(mFh).updateEF(adn, EF_MAILBOX_CPHS, + EF_EXT1, 1, null, + obtainMessage(EVENT_SET_CPHS_MAILBOX_DONE, onComplete)); + + } else { + AsyncResult.forMessage((onComplete)).exception = + new IccVmNotSupportedException("Update SIM voice mailbox error"); + onComplete.sendToTarget(); + } + } + + public String getVoiceMailAlphaTag() + { + return voiceMailTag; + } + + /** + * Sets the SIM voice message waiting indicator records + * @param line GSM Subscriber Profile Number, one-based. Only '1' is supported + * @param countWaiting The number of messages waiting, if known. Use + * -1 to indicate that an unknown number of + * messages are waiting + */ + public void + setVoiceMessageWaiting(int line, int countWaiting) { + if (line != 1) { + // only profile 1 is supported + return; + } + + // range check + if (countWaiting < 0) { + countWaiting = -1; + } else if (countWaiting > 0xff) { + // TS 23.040 9.2.3.24.2 + // "The value 255 shall be taken to mean 255 or greater" + countWaiting = 0xff; + } + + countVoiceMessages = countWaiting; + + mRecordsEventsRegistrants.notifyResult(EVENT_MWI); + + try { + if (efMWIS != null) { + // TS 51.011 10.3.45 + + // lsb of byte 0 is 'voicemail' status + efMWIS[0] = (byte)((efMWIS[0] & 0xfe) + | (countVoiceMessages == 0 ? 0 : 1)); + + // byte 1 is the number of voice messages waiting + if (countWaiting < 0) { + // The spec does not define what this should be + // if we don't know the count + efMWIS[1] = 0; + } else { + efMWIS[1] = (byte) countWaiting; + } + + mFh.updateEFLinearFixed( + EF_MWIS, 1, efMWIS, null, + obtainMessage (EVENT_UPDATE_DONE, EF_MWIS)); + } + + if (efCPHS_MWI != null) { + // Refer CPHS4_2.WW6 B4.2.3 + efCPHS_MWI[0] = (byte)((efCPHS_MWI[0] & 0xf0) + | (countVoiceMessages == 0 ? 0x5 : 0xa)); + + mFh.updateEFTransparent( + EF_VOICE_MAIL_INDICATOR_CPHS, efCPHS_MWI, + obtainMessage (EVENT_UPDATE_DONE, EF_VOICE_MAIL_INDICATOR_CPHS)); + } + } catch (ArrayIndexOutOfBoundsException ex) { + logw("Error saving voice mail state to SIM. Probably malformed SIM record", ex); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getVoiceCallForwardingFlag() { + return callForwardingEnabled; + } + + /** + * {@inheritDoc} + */ + @Override + public void setVoiceCallForwardingFlag(int line, boolean enable) { + + if (line != 1) return; // only line 1 is supported + + callForwardingEnabled = enable; + + mRecordsEventsRegistrants.notifyResult(EVENT_CFI); + + try { + if (mEfCfis != null) { + // lsb is of byte 1 is voice status + if (enable) { + mEfCfis[1] |= 1; + } else { + mEfCfis[1] &= 0xfe; + } + + // TODO: Should really update other fields in EF_CFIS, eg, + // dialing number. We don't read or use it right now. + + mFh.updateEFLinearFixed( + EF_CFIS, 1, mEfCfis, null, + obtainMessage (EVENT_UPDATE_DONE, EF_CFIS)); + } + + if (mEfCff != null) { + if (enable) { + mEfCff[0] = (byte) ((mEfCff[0] & CFF_LINE1_RESET) + | CFF_UNCONDITIONAL_ACTIVE); + } else { + mEfCff[0] = (byte) ((mEfCff[0] & CFF_LINE1_RESET) + | CFF_UNCONDITIONAL_DEACTIVE); + } + + mFh.updateEFTransparent( + EF_CFF_CPHS, mEfCff, + obtainMessage (EVENT_UPDATE_DONE, EF_CFF_CPHS)); + } + } catch (ArrayIndexOutOfBoundsException ex) { + logw("Error saving call fowarding flag to SIM. " + + "Probably malformed SIM record", ex); + + } + } + + /** + * Called by STK Service when REFRESH is received. + * @param fileChanged indicates whether any files changed + * @param fileList if non-null, a list of EF files that changed + */ + public void onRefresh(boolean fileChanged, int[] fileList) { + if (fileChanged) { + // A future optimization would be to inspect fileList and + // only reload those files that we care about. For now, + // just re-fetch all SIM records that we cache. + fetchSimRecords(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getOperatorNumeric() { + if (imsi == null) { + log("getOperatorNumeric: IMSI == null"); + return null; + } + if (mncLength == UNINITIALIZED || mncLength == UNKNOWN) { + log("getSIMOperatorNumeric: bad mncLength"); + return null; + } + + // Length = length of MCC + length of MNC + // length of mcc = 3 (TS 23.003 Section 2.2) + return imsi.substring(0, 3 + mncLength); + } + + // ***** Overridden from Handler + public void handleMessage(Message msg) { + AsyncResult ar; + AdnRecord adn; + + byte data[]; + + boolean isRecordLoadResponse = false; + + if (mDestroyed) { + loge("Received message " + msg + "[" + msg.what + "] " + + " while being destroyed. Ignoring."); + return; + } + + try { switch (msg.what) { + case EVENT_RADIO_OFF_OR_NOT_AVAILABLE: + onRadioOffOrNotAvailable(); + break; + + /* IO events */ + case EVENT_GET_IMSI_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + loge("Exception querying IMSI, Exception:" + ar.exception); + break; + } + + imsi = (String) ar.result; + + // IMSI (MCC+MNC+MSIN) is at least 6 digits, but not more + // than 15 (and usually 15). + if (imsi != null && (imsi.length() < 6 || imsi.length() > 15)) { + loge("invalid IMSI " + imsi); + imsi = null; + } + + log("IMSI: " + /* imsi.substring(0, 6) +*/ "xxxxxxx"); + + if (((mncLength == UNKNOWN) || (mncLength == 2)) && + ((imsi != null) && (imsi.length() >= 6))) { + String mccmncCode = imsi.substring(0, 6); + for (String mccmnc : MCCMNC_CODES_HAVING_3DIGITS_MNC) { + if (mccmnc.equals(mccmncCode)) { + mncLength = 3; + break; + } + } + } + + if (mncLength == UNKNOWN) { + // the SIM has told us all it knows, but it didn't know the mnc length. + // guess using the mcc + try { + int mcc = Integer.parseInt(imsi.substring(0,3)); + mncLength = MccTable.smallestDigitsMccForMnc(mcc); + } catch (NumberFormatException e) { + mncLength = UNKNOWN; + loge("Corrupt IMSI!"); + } + } + + if (mncLength != UNKNOWN && mncLength != UNINITIALIZED) { + // finally have both the imsi and the mncLength and can parse the imsi properly + MccTable.updateMccMncConfiguration(mContext, imsi.substring(0, 3 + mncLength)); + } + mParentCard.broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_IMSI, null); + break; + + case EVENT_GET_MBI_DONE: + boolean isValidMbdn; + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[]) ar.result; + + isValidMbdn = false; + if (ar.exception == null) { + // Refer TS 51.011 Section 10.3.44 for content details + log("EF_MBI: " + IccUtils.bytesToHexString(data)); + + // Voice mail record number stored first + mailboxIndex = (int)data[0] & 0xff; + + // check if dailing numbe id valid + if (mailboxIndex != 0 && mailboxIndex != 0xff) { + log("Got valid mailbox number for MBDN"); + isValidMbdn = true; + } + } + + // one more record to load + recordsToLoad += 1; + + if (isValidMbdn) { + // Note: MBDN was not included in NUM_OF_SIM_RECORDS_LOADED + new AdnRecordLoader(mFh).loadFromEF(EF_MBDN, EF_EXT6, + mailboxIndex, obtainMessage(EVENT_GET_MBDN_DONE)); + } else { + // If this EF not present, try mailbox as in CPHS standard + // CPHS (CPHS4_2.WW6) is a european standard. + new AdnRecordLoader(mFh).loadFromEF(EF_MAILBOX_CPHS, + EF_EXT1, 1, + obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE)); + } + + break; + case EVENT_GET_CPHS_MAILBOX_DONE: + case EVENT_GET_MBDN_DONE: + //Resetting the voice mail number and voice mail tag to null + //as these should be updated from the data read from EF_MBDN. + //If they are not reset, incase of invalid data/exception these + //variables are retaining their previous values and are + //causing invalid voice mailbox info display to user. + voiceMailNum = null; + voiceMailTag = null; + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + + log("Invalid or missing EF" + + ((msg.what == EVENT_GET_CPHS_MAILBOX_DONE) ? "[MAILBOX]" : "[MBDN]")); + + // Bug #645770 fall back to CPHS + // FIXME should use SST to decide + + if (msg.what == EVENT_GET_MBDN_DONE) { + //load CPHS on fail... + // FIXME right now, only load line1's CPHS voice mail entry + + recordsToLoad += 1; + new AdnRecordLoader(mFh).loadFromEF( + EF_MAILBOX_CPHS, EF_EXT1, 1, + obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE)); + } + break; + } + + adn = (AdnRecord)ar.result; + + log("VM: " + adn + + ((msg.what == EVENT_GET_CPHS_MAILBOX_DONE) ? " EF[MAILBOX]" : " EF[MBDN]")); + + if (adn.isEmpty() && msg.what == EVENT_GET_MBDN_DONE) { + // Bug #645770 fall back to CPHS + // FIXME should use SST to decide + // FIXME right now, only load line1's CPHS voice mail entry + recordsToLoad += 1; + new AdnRecordLoader(mFh).loadFromEF( + EF_MAILBOX_CPHS, EF_EXT1, 1, + obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE)); + + break; + } + + voiceMailNum = adn.getNumber(); + voiceMailTag = adn.getAlphaTag(); + break; + + case EVENT_GET_MSISDN_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + log("Invalid or missing EF[MSISDN]"); + break; + } + + adn = (AdnRecord)ar.result; + + msisdn = adn.getNumber(); + msisdnTag = adn.getAlphaTag(); + + log("MSISDN: " + /*msisdn*/ "xxxxxxx"); + break; + + case EVENT_SET_MSISDN_DONE: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + + if (ar.userObj != null) { + AsyncResult.forMessage(((Message) ar.userObj)).exception + = ar.exception; + ((Message) ar.userObj).sendToTarget(); + } + break; + + case EVENT_GET_MWIS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + log("EF_MWIS: " + IccUtils.bytesToHexString(data)); + + efMWIS = data; + + if ((data[0] & 0xff) == 0xff) { + log("Uninitialized record MWIS"); + break; + } + + // Refer TS 51.011 Section 10.3.45 for the content description + boolean voiceMailWaiting = ((data[0] & 0x01) != 0); + countVoiceMessages = data[1] & 0xff; + + if (voiceMailWaiting && countVoiceMessages == 0) { + // Unknown count = -1 + countVoiceMessages = -1; + } + + mRecordsEventsRegistrants.notifyResult(EVENT_MWI); + break; + + case EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + efCPHS_MWI = data; + + // Use this data if the EF[MWIS] exists and + // has been loaded + + if (efMWIS == null) { + int indicator = (int)(data[0] & 0xf); + + // Refer CPHS4_2.WW6 B4.2.3 + if (indicator == 0xA) { + // Unknown count = -1 + countVoiceMessages = -1; + } else if (indicator == 0x5) { + countVoiceMessages = 0; + } + + mRecordsEventsRegistrants.notifyResult(EVENT_MWI); + } + break; + + case EVENT_GET_ICCID_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + iccid = IccUtils.bcdToString(data, 0, data.length); + + log("iccid: " + iccid); + + break; + + + case EVENT_GET_AD_DONE: + try { + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + log("EF_AD: " + IccUtils.bytesToHexString(data)); + + if (data.length < 3) { + log("Corrupt AD data on SIM"); + break; + } + + if (data.length == 3) { + log("MNC length not present in EF_AD"); + break; + } + + mncLength = (int)data[3] & 0xf; + + if (mncLength == 0xf) { + mncLength = UNKNOWN; + } + } finally { + if (((mncLength == UNINITIALIZED) || (mncLength == UNKNOWN) || + (mncLength == 2)) && ((imsi != null) && (imsi.length() >= 6))) { + String mccmncCode = imsi.substring(0, 6); + for (String mccmnc : MCCMNC_CODES_HAVING_3DIGITS_MNC) { + if (mccmnc.equals(mccmncCode)) { + mncLength = 3; + break; + } + } + } + + if (mncLength == UNKNOWN || mncLength == UNINITIALIZED) { + if (imsi != null) { + try { + int mcc = Integer.parseInt(imsi.substring(0,3)); + + mncLength = MccTable.smallestDigitsMccForMnc(mcc); + } catch (NumberFormatException e) { + mncLength = UNKNOWN; + loge("Corrupt IMSI!"); + } + } else { + // Indicate we got this info, but it didn't contain the length. + mncLength = UNKNOWN; + + log("MNC length not present in EF_AD"); + } + } + if (imsi != null && mncLength != UNKNOWN) { + // finally have both imsi and the length of the mnc and can parse + // the imsi properly + MccTable.updateMccMncConfiguration(mContext, + imsi.substring(0, 3 + mncLength)); + } + } + break; + + case EVENT_GET_SPN_DONE: + isRecordLoadResponse = true; + ar = (AsyncResult) msg.obj; + getSpnFsm(false, ar); + break; + + case EVENT_GET_CFF_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult) msg.obj; + data = (byte[]) ar.result; + + if (ar.exception != null) { + break; + } + + log("EF_CFF_CPHS: " + IccUtils.bytesToHexString(data)); + mEfCff = data; + + if (mEfCfis == null) { + callForwardingEnabled = + ((data[0] & CFF_LINE1_MASK) == CFF_UNCONDITIONAL_ACTIVE); + + mRecordsEventsRegistrants.notifyResult(EVENT_CFI); + } + break; + + case EVENT_GET_SPDI_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + parseEfSpdi(data); + break; + + case EVENT_UPDATE_DONE: + ar = (AsyncResult)msg.obj; + if (ar.exception != null) { + logw("update failed. ", ar.exception); + } + break; + + case EVENT_GET_PNN_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + SimTlv tlv = new SimTlv(data, 0, data.length); + + for ( ; tlv.isValidObject() ; tlv.nextObject()) { + if (tlv.getTag() == TAG_FULL_NETWORK_NAME) { + pnnHomeName + = IccUtils.networkNameToString( + tlv.getData(), 0, tlv.getData().length); + break; + } + } + break; + + case EVENT_GET_ALL_SMS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + if (ar.exception != null) + break; + + handleSmses((ArrayList) ar.result); + break; + + case EVENT_MARK_SMS_READ_DONE: + Log.i("ENF", "marked read: sms " + msg.arg1); + break; + + + case EVENT_SMS_ON_SIM: + isRecordLoadResponse = false; + + ar = (AsyncResult)msg.obj; + + int[] index = (int[])ar.result; + + if (ar.exception != null || index.length != 1) { + loge("Error on SMS_ON_SIM with exp " + + ar.exception + " length " + index.length); + } else { + log("READ EF_SMS RECORD index=" + index[0]); + mFh.loadEFLinearFixed(EF_SMS,index[0], + obtainMessage(EVENT_GET_SMS_DONE)); + } + break; + + case EVENT_GET_SMS_DONE: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + if (ar.exception == null) { + handleSms((byte[])ar.result); + } else { + loge("Error on GET_SMS with exp " + ar.exception); + } + break; + case EVENT_GET_SST_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + mUsimServiceTable = new UsimServiceTable(data); + if (DBG) log("SST: " + mUsimServiceTable); + break; + + case EVENT_GET_INFO_CPHS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + break; + } + + mCphsInfo = (byte[])ar.result; + + if (DBG) log("iCPHS: " + IccUtils.bytesToHexString(mCphsInfo)); + break; + + case EVENT_SET_MBDN_DONE: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + + if (ar.exception == null) { + voiceMailNum = newVoiceMailNum; + voiceMailTag = newVoiceMailTag; + } + + if (isCphsMailboxEnabled()) { + adn = new AdnRecord(voiceMailTag, voiceMailNum); + Message onCphsCompleted = (Message) ar.userObj; + + /* write to cphs mailbox whenever it is available but + * we only need notify caller once if both updating are + * successful. + * + * so if set_mbdn successful, notify caller here and set + * onCphsCompleted to null + */ + if (ar.exception == null && ar.userObj != null) { + AsyncResult.forMessage(((Message) ar.userObj)).exception + = null; + ((Message) ar.userObj).sendToTarget(); + + if (DBG) log("Callback with MBDN successful."); + + onCphsCompleted = null; + } + + new AdnRecordLoader(mFh). + updateEF(adn, EF_MAILBOX_CPHS, EF_EXT1, 1, null, + obtainMessage(EVENT_SET_CPHS_MAILBOX_DONE, + onCphsCompleted)); + } else { + if (ar.userObj != null) { + AsyncResult.forMessage(((Message) ar.userObj)).exception + = ar.exception; + ((Message) ar.userObj).sendToTarget(); + } + } + break; + case EVENT_SET_CPHS_MAILBOX_DONE: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + if(ar.exception == null) { + voiceMailNum = newVoiceMailNum; + voiceMailTag = newVoiceMailTag; + } else { + if (DBG) log("Set CPHS MailBox with exception: " + + ar.exception); + } + if (ar.userObj != null) { + if (DBG) log("Callback with CPHS MB successful."); + AsyncResult.forMessage(((Message) ar.userObj)).exception + = ar.exception; + ((Message) ar.userObj).sendToTarget(); + } + break; + case EVENT_SIM_REFRESH: + isRecordLoadResponse = false; + ar = (AsyncResult)msg.obj; + if (DBG) log("Sim REFRESH with exception: " + ar.exception); + if (ar.exception == null) { + handleSimRefresh((IccRefreshResponse)ar.result); + } + break; + case EVENT_GET_CFIS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + data = (byte[])ar.result; + + if (ar.exception != null) { + break; + } + + log("EF_CFIS: " + IccUtils.bytesToHexString(data)); + + mEfCfis = data; + + // Refer TS 51.011 Section 10.3.46 for the content description + callForwardingEnabled = ((data[1] & 0x01) != 0); + + mRecordsEventsRegistrants.notifyResult(EVENT_CFI); + break; + + case EVENT_GET_CSP_CPHS_DONE: + isRecordLoadResponse = true; + + ar = (AsyncResult)msg.obj; + + if (ar.exception != null) { + loge("Exception in fetching EF_CSP data " + ar.exception); + break; + } + + data = (byte[])ar.result; + + log("EF_CSP: " + IccUtils.bytesToHexString(data)); + handleEfCspData(data); + break; + + default: + super.handleMessage(msg); // IccRecords handles generic record load responses + + }}catch (RuntimeException exc) { + // I don't want these exceptions to be fatal + logw("Exception parsing SIM record", exc); + } finally { + // Count up record load responses even if they are fails + if (isRecordLoadResponse) { + onRecordLoaded(); + } + } + } + + private void handleFileUpdate(int efid) { + switch(efid) { + case EF_MBDN: + recordsToLoad++; + new AdnRecordLoader(mFh).loadFromEF(EF_MBDN, EF_EXT6, + mailboxIndex, obtainMessage(EVENT_GET_MBDN_DONE)); + break; + case EF_MAILBOX_CPHS: + recordsToLoad++; + new AdnRecordLoader(mFh).loadFromEF(EF_MAILBOX_CPHS, EF_EXT1, + 1, obtainMessage(EVENT_GET_CPHS_MAILBOX_DONE)); + break; + case EF_CSP_CPHS: + recordsToLoad++; + log("[CSP] SIM Refresh for EF_CSP_CPHS"); + mFh.loadEFTransparent(EF_CSP_CPHS, + obtainMessage(EVENT_GET_CSP_CPHS_DONE)); + break; + default: + // For now, fetch all records if this is not a + // voicemail number. + // TODO: Handle other cases, instead of fetching all. + adnCache.reset(); + fetchSimRecords(); + break; + } + } + + private void handleSimRefresh(IccRefreshResponse refreshResponse){ + if (refreshResponse == null) { + if (DBG) log("handleSimRefresh received without input"); + return; + } + + if (refreshResponse.aid != null && + !refreshResponse.aid.equals(mParentCard.getAid())) { + // This is for different app. Ignore. + return; + } + + switch (refreshResponse.refreshResult) { + case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE: + if (DBG) log("handleSimRefresh with SIM_FILE_UPDATED"); + handleFileUpdate(refreshResponse.efId); + break; + case IccRefreshResponse.REFRESH_RESULT_INIT: + if (DBG) log("handleSimRefresh with SIM_REFRESH_INIT"); + // need to reload all files (that we care about) + adnCache.reset(); + fetchSimRecords(); + break; + case IccRefreshResponse.REFRESH_RESULT_RESET: + if (DBG) log("handleSimRefresh with SIM_REFRESH_RESET"); + mCi.setRadioPower(false, null); + /* Note: no need to call setRadioPower(true). Assuming the desired + * radio power state is still ON (as tracked by ServiceStateTracker), + * ServiceStateTracker will call setRadioPower when it receives the + * RADIO_STATE_CHANGED notification for the power off. And if the + * desired power state has changed in the interim, we don't want to + * override it with an unconditional power on. + */ + break; + default: + // unknown refresh operation + if (DBG) log("handleSimRefresh with unknown operation"); + break; + } + } + + /** + * Dispatch 3GPP format message. Overridden for CDMA/LTE phones by + * {@link com.android.internal.telephony.cdma.CdmaLteUiccRecords} + * to send messages to the secondary 3GPP format SMS dispatcher. + */ + protected int dispatchGsmMessage(SmsMessageBase message) { + mNewSmsRegistrants.notifyResult(message); + return 0; + } + + private void handleSms(byte[] ba) { + if (ba[0] != 0) + Log.d("ENF", "status : " + ba[0]); + + // 3GPP TS 51.011 v5.0.0 (20011-12) 10.5.3 + // 3 == "received by MS from network; message to be read" + if (ba[0] == 3) { + int n = ba.length; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[n - 1]; + System.arraycopy(ba, 1, pdu, 0, n - 1); + SmsMessage message = SmsMessage.createFromPdu(pdu); + + dispatchGsmMessage(message); + } + } + + + private void handleSmses(ArrayList messages) { + int count = messages.size(); + + for (int i = 0; i < count; i++) { + byte[] ba = (byte[]) messages.get(i); + + if (ba[0] != 0) + Log.i("ENF", "status " + i + ": " + ba[0]); + + // 3GPP TS 51.011 v5.0.0 (20011-12) 10.5.3 + // 3 == "received by MS from network; message to be read" + + if (ba[0] == 3) { + int n = ba.length; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[n - 1]; + System.arraycopy(ba, 1, pdu, 0, n - 1); + SmsMessage message = SmsMessage.createFromPdu(pdu); + + dispatchGsmMessage(message); + + // 3GPP TS 51.011 v5.0.0 (20011-12) 10.5.3 + // 1 == "received by MS from network; message read" + + ba[0] = 1; + + if (false) { // XXX writing seems to crash RdoServD + mFh.updateEFLinearFixed(EF_SMS, + i, ba, null, obtainMessage(EVENT_MARK_SMS_READ_DONE, i)); + } + } + } + } + + protected void onRecordLoaded() { + // One record loaded successfully or failed, In either case + // we need to update the recordsToLoad count + recordsToLoad -= 1; + if (DBG) log("onRecordLoaded " + recordsToLoad + " requested: " + recordsRequested); + + if (recordsToLoad == 0 && recordsRequested == true) { + onAllRecordsLoaded(); + } else if (recordsToLoad < 0) { + loge("recordsToLoad <0, programmer error suspected"); + recordsToLoad = 0; + } + } + + protected void onAllRecordsLoaded() { + String operator = getOperatorNumeric(); + + // Some fields require more than one SIM record to set + + log("SIMRecords: onAllRecordsLoaded set 'gsm.sim.operator.numeric' to operator='" + + operator + "'"); + SystemProperties.set(PROPERTY_ICC_OPERATOR_NUMERIC, operator); + + if (imsi != null) { + SystemProperties.set(PROPERTY_ICC_OPERATOR_ISO_COUNTRY, + MccTable.countryCodeForMcc(Integer.parseInt(imsi.substring(0,3)))); + } + else { + loge("onAllRecordsLoaded: imsi is NULL!"); + } + + setVoiceMailByCountry(operator); + setSpnFromConfig(operator); + + recordsLoadedRegistrants.notifyRegistrants( + new AsyncResult(null, null, null)); + mParentCard.broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_LOADED, null); + } + + //***** Private methods + + private void setSpnFromConfig(String carrier) { + if (mSpnOverride.containsCarrier(carrier)) { + spn = mSpnOverride.getSpn(carrier); + } + } + + + private void setVoiceMailByCountry (String spn) { + if (mVmConfig.containsCarrier(spn)) { + isVoiceMailFixed = true; + voiceMailNum = mVmConfig.getVoiceMailNumber(spn); + voiceMailTag = mVmConfig.getVoiceMailTag(spn); + } + } + + @Override + public void onReady() { + /* broadcast intent SIM_READY here so that we can make sure + READY is sent before IMSI ready + */ + mParentCard.broadcastIccStateChangedIntent( + IccCardConstants.INTENT_VALUE_ICC_READY, null); + + fetchSimRecords(); + } + + protected void fetchSimRecords() { + recordsRequested = true; + + if (DBG) log("fetchSimRecords " + recordsToLoad); + + mCi.getIMSIForApp(mParentCard.getAid(), obtainMessage(EVENT_GET_IMSI_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE)); + recordsToLoad++; + + // FIXME should examine EF[MSISDN]'s capability configuration + // to determine which is the voice/data/fax line + new AdnRecordLoader(mFh).loadFromEF(EF_MSISDN, EF_EXT1, 1, + obtainMessage(EVENT_GET_MSISDN_DONE)); + recordsToLoad++; + + // Record number is subscriber profile + mFh.loadEFLinearFixed(EF_MBI, 1, obtainMessage(EVENT_GET_MBI_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_AD, obtainMessage(EVENT_GET_AD_DONE)); + recordsToLoad++; + + // Record number is subscriber profile + mFh.loadEFLinearFixed(EF_MWIS, 1, obtainMessage(EVENT_GET_MWIS_DONE)); + recordsToLoad++; + + + // Also load CPHS-style voice mail indicator, which stores + // the same info as EF[MWIS]. If both exist, both are updated + // but the EF[MWIS] data is preferred + // Please note this must be loaded after EF[MWIS] + mFh.loadEFTransparent( + EF_VOICE_MAIL_INDICATOR_CPHS, + obtainMessage(EVENT_GET_VOICE_MAIL_INDICATOR_CPHS_DONE)); + recordsToLoad++; + + // Same goes for Call Forward Status indicator: fetch both + // EF[CFIS] and CPHS-EF, with EF[CFIS] preferred. + mFh.loadEFLinearFixed(EF_CFIS, 1, obtainMessage(EVENT_GET_CFIS_DONE)); + recordsToLoad++; + mFh.loadEFTransparent(EF_CFF_CPHS, obtainMessage(EVENT_GET_CFF_DONE)); + recordsToLoad++; + + + getSpnFsm(true, null); + + mFh.loadEFTransparent(EF_SPDI, obtainMessage(EVENT_GET_SPDI_DONE)); + recordsToLoad++; + + mFh.loadEFLinearFixed(EF_PNN, 1, obtainMessage(EVENT_GET_PNN_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_SST, obtainMessage(EVENT_GET_SST_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_INFO_CPHS, obtainMessage(EVENT_GET_INFO_CPHS_DONE)); + recordsToLoad++; + + mFh.loadEFTransparent(EF_CSP_CPHS,obtainMessage(EVENT_GET_CSP_CPHS_DONE)); + recordsToLoad++; + + // XXX should seek instead of examining them all + if (false) { // XXX + mFh.loadEFLinearFixedAll(EF_SMS, obtainMessage(EVENT_GET_ALL_SMS_DONE)); + recordsToLoad++; + } + + if (CRASH_RIL) { + String sms = "0107912160130310f20404d0110041007030208054832b0120" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + + "ffffffffffffffffffffffffffffff"; + byte[] ba = IccUtils.hexStringToBytes(sms); + + mFh.updateEFLinearFixed(EF_SMS, 1, ba, null, + obtainMessage(EVENT_MARK_SMS_READ_DONE, 1)); + } + if (DBG) log("fetchSimRecords " + recordsToLoad + " requested: " + recordsRequested); + } + + /** + * Returns the SpnDisplayRule based on settings on the SIM and the + * specified plmn (currently-registered PLMN). See TS 22.101 Annex A + * and TS 51.011 10.3.11 for details. + * + * If the SPN is not found on the SIM, the rule is always PLMN_ONLY. + */ + @Override + public int getDisplayRule(String plmn) { + int rule; + if (spn == null || spnDisplayCondition == -1) { + // EF_SPN was not found on the SIM, or not yet loaded. Just show ONS. + rule = SPN_RULE_SHOW_PLMN; + } else if (isOnMatchingPlmn(plmn)) { + rule = SPN_RULE_SHOW_SPN; + if ((spnDisplayCondition & 0x01) == 0x01) { + // ONS required when registered to HPLMN or PLMN in EF_SPDI + rule |= SPN_RULE_SHOW_PLMN; + } + } else { + rule = SPN_RULE_SHOW_PLMN; + if ((spnDisplayCondition & 0x02) == 0x00) { + // SPN required if not registered to HPLMN or PLMN in EF_SPDI + rule |= SPN_RULE_SHOW_SPN; + } + } + return rule; + } + + /** + * Checks if plmn is HPLMN or on the spdiNetworks list. + */ + private boolean isOnMatchingPlmn(String plmn) { + if (plmn == null) return false; + + if (plmn.equals(getOperatorNumeric())) { + return true; + } + + if (spdiNetworks != null) { + for (String spdiNet : spdiNetworks) { + if (plmn.equals(spdiNet)) { + return true; + } + } + } + return false; + } + + /** + * States of Get SPN Finite State Machine which only used by getSpnFsm() + */ + private enum Get_Spn_Fsm_State { + IDLE, // No initialized + INIT, // Start FSM + READ_SPN_3GPP, // Load EF_SPN firstly + READ_SPN_CPHS, // Load EF_SPN_CPHS secondly + READ_SPN_SHORT_CPHS // Load EF_SPN_SHORT_CPHS last + } + + /** + * Finite State Machine to load Service Provider Name , which can be stored + * in either EF_SPN (3GPP), EF_SPN_CPHS, or EF_SPN_SHORT_CPHS (CPHS4.2) + * + * After starting, FSM will search SPN EFs in order and stop after finding + * the first valid SPN + * + * If the FSM gets restart while waiting for one of + * SPN EFs results (i.e. a SIM refresh occurs after issuing + * read EF_CPHS_SPN), it will re-initialize only after + * receiving and discarding the unfinished SPN EF result. + * + * @param start set true only for initialize loading + * @param ar the AsyncResult from loadEFTransparent + * ar.exception holds exception in error + * ar.result is byte[] for data in success + */ + private void getSpnFsm(boolean start, AsyncResult ar) { + byte[] data; + + if (start) { + // Check previous state to see if there is outstanding + // SPN read + if(spnState == Get_Spn_Fsm_State.READ_SPN_3GPP || + spnState == Get_Spn_Fsm_State.READ_SPN_CPHS || + spnState == Get_Spn_Fsm_State.READ_SPN_SHORT_CPHS || + spnState == Get_Spn_Fsm_State.INIT) { + // Set INIT then return so the INIT code + // will run when the outstanding read done. + spnState = Get_Spn_Fsm_State.INIT; + return; + } else { + spnState = Get_Spn_Fsm_State.INIT; + } + } + + switch(spnState){ + case INIT: + spn = null; + + mFh.loadEFTransparent(EF_SPN, + obtainMessage(EVENT_GET_SPN_DONE)); + recordsToLoad++; + + spnState = Get_Spn_Fsm_State.READ_SPN_3GPP; + break; + case READ_SPN_3GPP: + if (ar != null && ar.exception == null) { + data = (byte[]) ar.result; + spnDisplayCondition = 0xff & data[0]; + spn = IccUtils.adnStringFieldToString(data, 1, data.length - 1); + + if (DBG) log("Load EF_SPN: " + spn + + " spnDisplayCondition: " + spnDisplayCondition); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ALPHA, spn); + + spnState = Get_Spn_Fsm_State.IDLE; + } else { + mFh.loadEFTransparent( EF_SPN_CPHS, + obtainMessage(EVENT_GET_SPN_DONE)); + recordsToLoad++; + + spnState = Get_Spn_Fsm_State.READ_SPN_CPHS; + + // See TS 51.011 10.3.11. Basically, default to + // show PLMN always, and SPN also if roaming. + spnDisplayCondition = -1; + } + break; + case READ_SPN_CPHS: + if (ar != null && ar.exception == null) { + data = (byte[]) ar.result; + spn = IccUtils.adnStringFieldToString( + data, 0, data.length - 1 ); + + if (DBG) log("Load EF_SPN_CPHS: " + spn); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ALPHA, spn); + + spnState = Get_Spn_Fsm_State.IDLE; + } else { + mFh.loadEFTransparent( + EF_SPN_SHORT_CPHS, obtainMessage(EVENT_GET_SPN_DONE)); + recordsToLoad++; + + spnState = Get_Spn_Fsm_State.READ_SPN_SHORT_CPHS; + } + break; + case READ_SPN_SHORT_CPHS: + if (ar != null && ar.exception == null) { + data = (byte[]) ar.result; + spn = IccUtils.adnStringFieldToString( + data, 0, data.length - 1); + + if (DBG) log("Load EF_SPN_SHORT_CPHS: " + spn); + SystemProperties.set(PROPERTY_ICC_OPERATOR_ALPHA, spn); + }else { + if (DBG) log("No SPN loaded in either CHPS or 3GPP"); + } + + spnState = Get_Spn_Fsm_State.IDLE; + break; + default: + spnState = Get_Spn_Fsm_State.IDLE; + } + } + + /** + * Parse TS 51.011 EF[SPDI] record + * This record contains the list of numeric network IDs that + * are treated specially when determining SPN display + */ + private void + parseEfSpdi(byte[] data) { + SimTlv tlv = new SimTlv(data, 0, data.length); + + byte[] plmnEntries = null; + + for ( ; tlv.isValidObject() ; tlv.nextObject()) { + // Skip SPDI tag, if existant + if (tlv.getTag() == TAG_SPDI) { + tlv = new SimTlv(tlv.getData(), 0, tlv.getData().length); + } + // There should only be one TAG_SPDI_PLMN_LIST + if (tlv.getTag() == TAG_SPDI_PLMN_LIST) { + plmnEntries = tlv.getData(); + break; + } + } + + if (plmnEntries == null) { + return; + } + + spdiNetworks = new ArrayList<String>(plmnEntries.length / 3); + + for (int i = 0 ; i + 2 < plmnEntries.length ; i += 3) { + String plmnCode; + plmnCode = IccUtils.bcdToString(plmnEntries, i, 3); + + // Valid operator codes are 5 or 6 digits + if (plmnCode.length() >= 5) { + log("EF_SPDI network: " + plmnCode); + spdiNetworks.add(plmnCode); + } + } + } + + /** + * check to see if Mailbox Number is allocated and activated in CPHS SST + */ + private boolean isCphsMailboxEnabled() { + if (mCphsInfo == null) return false; + return ((mCphsInfo[1] & CPHS_SST_MBN_MASK) == CPHS_SST_MBN_ENABLED ); + } + + protected void log(String s) { + Log.d(LOG_TAG, "[SIMRecords] " + s); + } + + protected void loge(String s) { + Log.e(LOG_TAG, "[SIMRecords] " + s); + } + + protected void logw(String s, Throwable tr) { + Log.w(LOG_TAG, "[SIMRecords] " + s, tr); + } + + protected void logv(String s) { + Log.v(LOG_TAG, "[SIMRecords] " + s); + } + + /** + * Return true if "Restriction of menu options for manual PLMN selection" + * bit is set or EF_CSP data is unavailable, return false otherwise. + */ + public boolean isCspPlmnEnabled() { + return mCspPlmnEnabled; + } + + /** + * Parse EF_CSP data and check if + * "Restriction of menu options for manual PLMN selection" is + * Enabled/Disabled + * + * @param data EF_CSP hex data. + */ + private void handleEfCspData(byte[] data) { + // As per spec CPHS4_2.WW6, CPHS B.4.7.1, EF_CSP contains CPHS defined + // 18 bytes (i.e 9 service groups info) and additional data specific to + // operator. The valueAddedServicesGroup is not part of standard + // services. This is operator specific and can be programmed any where. + // Normally this is programmed as 10th service after the standard + // services. + int usedCspGroups = data.length / 2; + // This is the "Servive Group Number" of "Value Added Services Group". + byte valueAddedServicesGroup = (byte)0xC0; + + mCspPlmnEnabled = true; + for (int i = 0; i < usedCspGroups; i++) { + if (data[2 * i] == valueAddedServicesGroup) { + log("[CSP] found ValueAddedServicesGroup, value " + data[(2 * i) + 1]); + if ((data[(2 * i) + 1] & 0x80) == 0x80) { + // Bit 8 is for + // "Restriction of menu options for manual PLMN selection". + // Operator Selection menu should be enabled. + mCspPlmnEnabled = true; + } else { + mCspPlmnEnabled = false; + // Operator Selection menu should be disabled. + // Operator Selection Mode should be set to Automatic. + log("[CSP] Set Automatic Network Selection"); + mNetworkSelectionModeAutomaticRegistrants.notifyRegistrants(); + } + return; + } + } + + log("[CSP] Value Added Service Group (0xC0), not found!"); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SimPhoneBookInterfaceManager.java b/src/java/com/android/internal/telephony/gsm/SimPhoneBookInterfaceManager.java new file mode 100644 index 0000000..35ba0d1 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SimPhoneBookInterfaceManager.java @@ -0,0 +1,79 @@ +/* +** Copyright 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 com.android.internal.telephony.gsm; + +import java.util.concurrent.atomic.AtomicBoolean; + +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.IccPhoneBookInterfaceManager; + +/** + * SimPhoneBookInterfaceManager to provide an inter-process communication to + * access ADN-like SIM records. + */ + + +public class SimPhoneBookInterfaceManager extends IccPhoneBookInterfaceManager { + static final String LOG_TAG = "GSM"; + + public SimPhoneBookInterfaceManager(GSMPhone phone) { + super(phone); + adnCache = phone.mIccRecords.getAdnCache(); + //NOTE service "simphonebook" added by IccSmsInterfaceManagerProxy + } + + public void dispose() { + super.dispose(); + } + + protected void finalize() { + try { + super.finalize(); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Error while finalizing:", throwable); + } + if(DBG) Log.d(LOG_TAG, "SimPhoneBookInterfaceManager finalized"); + } + + public int[] getAdnRecordsSize(int efid) { + if (DBG) logd("getAdnRecordsSize: efid=" + efid); + synchronized(mLock) { + checkThread(); + recordSize = new int[3]; + + //Using mBaseHandler, no difference in EVENT_GET_SIZE_DONE handling + AtomicBoolean status = new AtomicBoolean(false); + Message response = mBaseHandler.obtainMessage(EVENT_GET_SIZE_DONE, status); + + phone.getIccFileHandler().getEFLinearRecordSize(efid, response); + waitForResult(status); + } + + return recordSize; + } + + protected void logd(String msg) { + Log.d(LOG_TAG, "[SimPbInterfaceManager] " + msg); + } + + protected void loge(String msg) { + Log.e(LOG_TAG, "[SimPbInterfaceManager] " + msg); + } +} + diff --git a/src/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java b/src/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java new file mode 100644 index 0000000..92bf390 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java @@ -0,0 +1,365 @@ +/* +** Copyright 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 com.android.internal.telephony.gsm; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.AsyncResult; +import android.os.Binder; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccSmsInterfaceManager; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.IntRangeManager; +import com.android.internal.telephony.SMSDispatcher; +import com.android.internal.telephony.SmsRawData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static android.telephony.SmsManager.STATUS_ON_ICC_FREE; + +/** + * SimSmsInterfaceManager to provide an inter-process communication to + * access Sms in Sim. + */ +public class SimSmsInterfaceManager extends IccSmsInterfaceManager { + static final String LOG_TAG = "GSM"; + static final boolean DBG = true; + + private final Object mLock = new Object(); + private boolean mSuccess; + private List<SmsRawData> mSms; + private HashMap<Integer, HashSet<String>> mCellBroadcastSubscriptions = + new HashMap<Integer, HashSet<String>>(); + + private CellBroadcastRangeManager mCellBroadcastRangeManager = + new CellBroadcastRangeManager(); + + private static final int EVENT_LOAD_DONE = 1; + private static final int EVENT_UPDATE_DONE = 2; + private static final int EVENT_SET_BROADCAST_ACTIVATION_DONE = 3; + private static final int EVENT_SET_BROADCAST_CONFIG_DONE = 4; + private static final int SMS_CB_CODE_SCHEME_MIN = 0; + private static final int SMS_CB_CODE_SCHEME_MAX = 255; + + Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch (msg.what) { + case EVENT_UPDATE_DONE: + ar = (AsyncResult) msg.obj; + synchronized (mLock) { + mSuccess = (ar.exception == null); + mLock.notifyAll(); + } + break; + case EVENT_LOAD_DONE: + ar = (AsyncResult)msg.obj; + synchronized (mLock) { + if (ar.exception == null) { + mSms = buildValidRawData((ArrayList<byte[]>) ar.result); + } else { + if(DBG) log("Cannot load Sms records"); + if (mSms != null) + mSms.clear(); + } + mLock.notifyAll(); + } + break; + case EVENT_SET_BROADCAST_ACTIVATION_DONE: + case EVENT_SET_BROADCAST_CONFIG_DONE: + ar = (AsyncResult) msg.obj; + synchronized (mLock) { + mSuccess = (ar.exception == null); + mLock.notifyAll(); + } + break; + } + } + }; + + public SimSmsInterfaceManager(GSMPhone phone, SMSDispatcher dispatcher) { + super(phone); + mDispatcher = dispatcher; + } + + public void dispose() { + } + + @Override + protected void finalize() { + try { + super.finalize(); + } catch (Throwable throwable) { + Log.e(LOG_TAG, "Error while finalizing:", throwable); + } + if(DBG) Log.d(LOG_TAG, "SimSmsInterfaceManager finalized"); + } + + /** + * Update the specified message on the SIM. + * + * @param index record index of message to update + * @param status new message status (STATUS_ON_ICC_READ, + * STATUS_ON_ICC_UNREAD, STATUS_ON_ICC_SENT, + * STATUS_ON_ICC_UNSENT, STATUS_ON_ICC_FREE) + * @param pdu the raw PDU to store + * @return success or not + * + */ + public boolean + updateMessageOnIccEf(int index, int status, byte[] pdu) { + if (DBG) log("updateMessageOnIccEf: index=" + index + + " status=" + status + " ==> " + + "("+ Arrays.toString(pdu) + ")"); + enforceReceiveAndSend("Updating message on SIM"); + synchronized(mLock) { + mSuccess = false; + Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE); + + if (status == STATUS_ON_ICC_FREE) { + // Special case FREE: call deleteSmsOnSim instead of + // manipulating the SIM record + mPhone.mCM.deleteSmsOnSim(index, response); + } else { + byte[] record = makeSmsRecordData(status, pdu); + mPhone.getIccFileHandler().updateEFLinearFixed( + IccConstants.EF_SMS, + index, record, null, response); + } + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to update by index"); + } + } + return mSuccess; + } + + /** + * Copy a raw SMS PDU to the SIM. + * + * @param pdu the raw PDU to store + * @param status message status (STATUS_ON_ICC_READ, STATUS_ON_ICC_UNREAD, + * STATUS_ON_ICC_SENT, STATUS_ON_ICC_UNSENT) + * @return success or not + * + */ + public boolean copyMessageToIccEf(int status, byte[] pdu, byte[] smsc) { + if (DBG) log("copyMessageToIccEf: status=" + status + " ==> " + + "pdu=("+ Arrays.toString(pdu) + + "), smsm=(" + Arrays.toString(smsc) +")"); + enforceReceiveAndSend("Copying message to SIM"); + synchronized(mLock) { + mSuccess = false; + Message response = mHandler.obtainMessage(EVENT_UPDATE_DONE); + + mPhone.mCM.writeSmsToSim(status, IccUtils.bytesToHexString(smsc), + IccUtils.bytesToHexString(pdu), response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to update by index"); + } + } + return mSuccess; + } + + /** + * Retrieves all messages currently stored on ICC. + * + * @return list of SmsRawData of all sms on ICC + */ + public List<SmsRawData> getAllMessagesFromIccEf() { + if (DBG) log("getAllMessagesFromEF"); + + Context context = mPhone.getContext(); + + context.enforceCallingPermission( + "android.permission.RECEIVE_SMS", + "Reading messages from SIM"); + synchronized(mLock) { + Message response = mHandler.obtainMessage(EVENT_LOAD_DONE); + mPhone.getIccFileHandler().loadEFLinearFixedAll(IccConstants.EF_SMS, response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to load from the SIM"); + } + } + return mSms; + } + + public boolean enableCellBroadcast(int messageIdentifier) { + return enableCellBroadcastRange(messageIdentifier, messageIdentifier); + } + + public boolean disableCellBroadcast(int messageIdentifier) { + return disableCellBroadcastRange(messageIdentifier, messageIdentifier); + } + + public boolean enableCellBroadcastRange(int startMessageId, int endMessageId) { + if (DBG) log("enableCellBroadcastRange"); + + Context context = mPhone.getContext(); + + context.enforceCallingPermission( + "android.permission.RECEIVE_SMS", + "Enabling cell broadcast SMS"); + + String client = context.getPackageManager().getNameForUid( + Binder.getCallingUid()); + + if (!mCellBroadcastRangeManager.enableRange(startMessageId, endMessageId, client)) { + log("Failed to add cell broadcast subscription for MID range " + startMessageId + + " to " + endMessageId + " from client " + client); + return false; + } + + if (DBG) + log("Added cell broadcast subscription for MID range " + startMessageId + + " to " + endMessageId + " from client " + client); + + setCellBroadcastActivation(!mCellBroadcastRangeManager.isEmpty()); + + return true; + } + + public boolean disableCellBroadcastRange(int startMessageId, int endMessageId) { + if (DBG) log("disableCellBroadcastRange"); + + Context context = mPhone.getContext(); + + context.enforceCallingPermission( + "android.permission.RECEIVE_SMS", + "Disabling cell broadcast SMS"); + + String client = context.getPackageManager().getNameForUid( + Binder.getCallingUid()); + + if (!mCellBroadcastRangeManager.disableRange(startMessageId, endMessageId, client)) { + log("Failed to remove cell broadcast subscription for MID range " + startMessageId + + " to " + endMessageId + " from client " + client); + return false; + } + + if (DBG) + log("Removed cell broadcast subscription for MID range " + startMessageId + + " to " + endMessageId + " from client " + client); + + setCellBroadcastActivation(!mCellBroadcastRangeManager.isEmpty()); + + return true; + } + + class CellBroadcastRangeManager extends IntRangeManager { + private ArrayList<SmsBroadcastConfigInfo> mConfigList = + new ArrayList<SmsBroadcastConfigInfo>(); + + /** + * Called when the list of enabled ranges has changed. This will be + * followed by zero or more calls to {@link #addRange} followed by + * a call to {@link #finishUpdate}. + */ + protected void startUpdate() { + mConfigList.clear(); + } + + /** + * Called after {@link #startUpdate} to indicate a range of enabled + * values. + * @param startId the first id included in the range + * @param endId the last id included in the range + */ + protected void addRange(int startId, int endId, boolean selected) { + mConfigList.add(new SmsBroadcastConfigInfo(startId, endId, + SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, selected)); + } + + /** + * Called to indicate the end of a range update started by the + * previous call to {@link #startUpdate}. + * @return true if successful, false otherwise + */ + protected boolean finishUpdate() { + if (mConfigList.isEmpty()) { + return true; + } else { + SmsBroadcastConfigInfo[] configs = + mConfigList.toArray(new SmsBroadcastConfigInfo[mConfigList.size()]); + return setCellBroadcastConfig(configs); + } + } + } + + private boolean setCellBroadcastConfig(SmsBroadcastConfigInfo[] configs) { + if (DBG) + log("Calling setGsmBroadcastConfig with " + configs.length + " configurations"); + + synchronized (mLock) { + Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_CONFIG_DONE); + + mSuccess = false; + mPhone.mCM.setGsmBroadcastConfig(configs, response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to set cell broadcast config"); + } + } + + return mSuccess; + } + + private boolean setCellBroadcastActivation(boolean activate) { + if (DBG) + log("Calling setCellBroadcastActivation(" + activate + ')'); + + synchronized (mLock) { + Message response = mHandler.obtainMessage(EVENT_SET_BROADCAST_ACTIVATION_DONE); + + mSuccess = false; + mPhone.mCM.setGsmBroadcastActivation(activate, response); + + try { + mLock.wait(); + } catch (InterruptedException e) { + log("interrupted while trying to set cell broadcast activation"); + } + } + + return mSuccess; + } + + @Override + protected void log(String msg) { + Log.d(LOG_TAG, "[SimSmsInterfaceManager] " + msg); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SimTlv.java b/src/java/com/android/internal/telephony/gsm/SimTlv.java new file mode 100644 index 0000000..497cf5f --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SimTlv.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +/** + * SIM Tag-Length-Value record + * TS 102 223 Annex C + * + * {@hide} + * + */ +public class SimTlv +{ + //***** Private Instance Variables + + byte record[]; + int tlvOffset; + int tlvLength; + int curOffset; + int curDataOffset; + int curDataLength; + boolean hasValidTlvObject; + + public SimTlv(byte[] record, int offset, int length) { + this.record = record; + + this.tlvOffset = offset; + this.tlvLength = length; + curOffset = offset; + + hasValidTlvObject = parseCurrentTlvObject(); + } + + public boolean nextObject() { + if (!hasValidTlvObject) return false; + curOffset = curDataOffset + curDataLength; + hasValidTlvObject = parseCurrentTlvObject(); + return hasValidTlvObject; + } + + public boolean isValidObject() { + return hasValidTlvObject; + } + + /** + * Returns the tag for the current TLV object + * Return 0 if !isValidObject() + * 0 and 0xff are invalid tag values + * valid tags range from 1 - 0xfe + */ + public int getTag() { + if (!hasValidTlvObject) return 0; + return record[curOffset] & 0xff; + } + + /** + * Returns data associated with current TLV object + * returns null if !isValidObject() + */ + + public byte[] getData() { + if (!hasValidTlvObject) return null; + + byte[] ret = new byte[curDataLength]; + System.arraycopy(record, curDataOffset, ret, 0, curDataLength); + return ret; + } + + /** + * Updates curDataLength and curDataOffset + * @return false on invalid record, true on valid record + */ + + private boolean parseCurrentTlvObject() { + // 0x00 and 0xff are invalid tag values + + try { + if (record[curOffset] == 0 || (record[curOffset] & 0xff) == 0xff) { + return false; + } + + if ((record[curOffset + 1] & 0xff) < 0x80) { + // one byte length 0 - 0x7f + curDataLength = record[curOffset + 1] & 0xff; + curDataOffset = curOffset + 2; + } else if ((record[curOffset + 1] & 0xff) == 0x81) { + // two byte length 0x80 - 0xff + curDataLength = record[curOffset + 2] & 0xff; + curDataOffset = curOffset + 3; + } else { + return false; + } + } catch (ArrayIndexOutOfBoundsException ex) { + return false; + } + + if (curDataLength + curDataOffset > tlvOffset + tlvLength) { + return false; + } + + return true; + } + +} diff --git a/src/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java b/src/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java new file mode 100644 index 0000000..66e7ce0 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SmsBroadcastConfigInfo.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2009 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.internal.telephony.gsm; + +/** + * SmsBroadcastConfigInfo defines one configuration of Cell Broadcast + * Message (CBM) to be received by the ME + * + * fromServiceId - toServiceId defines a range of CBM message identifiers + * whose value is 0x0000 - 0xFFFF as defined in TS 23.041 9.4.1.2.2 for GMS + * and 9.4.4.2.2 for UMTS. All other values can be treated as empty + * CBM message ID. + * + * fromCodeScheme - toCodeScheme defines a range of CBM data coding schemes + * whose value is 0x00 - 0xFF as defined in TS 23.041 9.4.1.2.3 for GMS + * and 9.4.4.2.3 for UMTS. + * All other values can be treated as empty CBM data coding scheme. + * + * selected false means message types specified in {@code <fromServiceId, toServiceId>} + * and {@code <fromCodeScheme, toCodeScheme>} are not accepted, while true means accepted. + * + */ +public final class SmsBroadcastConfigInfo { + private int fromServiceId; + private int toServiceId; + private int fromCodeScheme; + private int toCodeScheme; + private boolean selected; + + /** + * Initialize the object from rssi and cid. + */ + public SmsBroadcastConfigInfo(int fromId, int toId, int fromScheme, + int toScheme, boolean selected) { + fromServiceId = fromId; + toServiceId = toId; + fromCodeScheme = fromScheme; + toCodeScheme = toScheme; + this.selected = selected; + } + + /** + * @param fromServiceId the fromServiceId to set + */ + public void setFromServiceId(int fromServiceId) { + this.fromServiceId = fromServiceId; + } + + /** + * @return the fromServiceId + */ + public int getFromServiceId() { + return fromServiceId; + } + + /** + * @param toServiceId the toServiceId to set + */ + public void setToServiceId(int toServiceId) { + this.toServiceId = toServiceId; + } + + /** + * @return the toServiceId + */ + public int getToServiceId() { + return toServiceId; + } + + /** + * @param fromCodeScheme the fromCodeScheme to set + */ + public void setFromCodeScheme(int fromCodeScheme) { + this.fromCodeScheme = fromCodeScheme; + } + + /** + * @return the fromCodeScheme + */ + public int getFromCodeScheme() { + return fromCodeScheme; + } + + /** + * @param toCodeScheme the toCodeScheme to set + */ + public void setToCodeScheme(int toCodeScheme) { + this.toCodeScheme = toCodeScheme; + } + + /** + * @return the toCodeScheme + */ + public int getToCodeScheme() { + return toCodeScheme; + } + + /** + * @param selected the selected to set + */ + public void setSelected(boolean selected) { + this.selected = selected; + } + + /** + * @return the selected + */ + public boolean isSelected() { + return selected; + } + + @Override + public String toString() { + return "SmsBroadcastConfigInfo: Id [" + + fromServiceId + ',' + toServiceId + "] Code [" + + fromCodeScheme + ',' + toCodeScheme + "] " + + (selected ? "ENABLED" : "DISABLED"); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SmsCbConstants.java b/src/java/com/android/internal/telephony/gsm/SmsCbConstants.java new file mode 100644 index 0000000..ebb4666 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SmsCbConstants.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012 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.internal.telephony.gsm; + +/** + * Constants used in SMS Cell Broadcast messages (see 3GPP TS 23.041). This class is used by the + * boot-time broadcast channel enable and database upgrade code in CellBroadcastReceiver, so it + * is public, but should be avoided in favor of the radio technology independent constants in + * {@link android.telephony.SmsCbMessage}, {@link android.telephony.SmsCbEtwsInfo}, and + * {@link android.telephony.SmsCbCmasInfo} classes. + * + * {@hide} + */ +public class SmsCbConstants { + + /** Private constructor for utility class. */ + private SmsCbConstants() { } + + /** Start of PWS Message Identifier range (includes ETWS and CMAS). */ + public static final int MESSAGE_ID_PWS_FIRST_IDENTIFIER = 0x1100; + + /** Bitmask for messages of ETWS type (including future extensions). */ + public static final int MESSAGE_ID_ETWS_TYPE_MASK = 0xFFF8; + + /** Value for messages of ETWS type after applying {@link #MESSAGE_ID_ETWS_TYPE_MASK}. */ + public static final int MESSAGE_ID_ETWS_TYPE = 0x1100; + + /** ETWS Message Identifier for earthquake warning message. */ + public static final int MESSAGE_ID_ETWS_EARTHQUAKE_WARNING = 0x1100; + + /** ETWS Message Identifier for tsunami warning message. */ + public static final int MESSAGE_ID_ETWS_TSUNAMI_WARNING = 0x1101; + + /** ETWS Message Identifier for earthquake and tsunami combined warning message. */ + public static final int MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING = 0x1102; + + /** ETWS Message Identifier for test message. */ + public static final int MESSAGE_ID_ETWS_TEST_MESSAGE = 0x1103; + + /** ETWS Message Identifier for messages related to other emergency types. */ + public static final int MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE = 0x1104; + + /** Start of CMAS Message Identifier range. */ + public static final int MESSAGE_ID_CMAS_FIRST_IDENTIFIER = 0x1112; + + /** CMAS Message Identifier for Presidential Level alerts. */ + public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL = 0x1112; + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED = 0x1113; + + /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY = 0x1114; + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED = 0x1115; + + /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY = 0x1116; + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED = 0x1117; + + /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY = 0x1118; + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED = 0x1119; + + /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely. */ + public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY = 0x111A; + + /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert). */ + public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY = 0x111B; + + /** CMAS Message Identifier for the Required Monthly Test. */ + public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST = 0x111C; + + /** CMAS Message Identifier for CMAS Exercise. */ + public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE = 0x111D; + + /** CMAS Message Identifier for operator defined use. */ + public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE = 0x111E; + + /** End of CMAS Message Identifier range (including future extensions). */ + public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER = 0x112F; + + /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */ + public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER = 0x18FF; + + /** ETWS serial number flag to activate the popup display. */ + public static final int SERIAL_NUMBER_ETWS_ACTIVATE_POPUP = 0x1000; + + /** ETWS serial number flag to activate the emergency user alert. */ + public static final int SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT = 0x2000; + + /** ETWS warning type value for earthquake. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE = 0x00; + + /** ETWS warning type value for tsunami. */ + public static final int ETWS_WARNING_TYPE_TSUNAMI = 0x01; + + /** ETWS warning type value for earthquake and tsunami. */ + public static final int ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI = 0x02; + + /** ETWS warning type value for test broadcast. */ + public static final int ETWS_WARNING_TYPE_TEST = 0x03; + + /** ETWS warning type value for other notifications. */ + public static final int ETWS_WARNING_TYPE_OTHER = 0x04; +} diff --git a/src/java/com/android/internal/telephony/gsm/SmsCbHeader.java b/src/java/com/android/internal/telephony/gsm/SmsCbHeader.java new file mode 100644 index 0000000..5692044 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SmsCbHeader.java @@ -0,0 +1,409 @@ +/* + * Copyright (C) 2010 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.internal.telephony.gsm; + +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; + +import java.util.Arrays; + +/** + * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by + * CellBroadcastReceiver test cases, but should not be used by applications. + * + * All relevant header information is now sent as a Parcelable + * {@link android.telephony.SmsCbMessage} object in the "message" extra of the + * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or + * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent. + * The raw PDU is no longer sent to SMS CB applications. + */ +class SmsCbHeader { + + /** + * Length of SMS-CB header + */ + static final int PDU_HEADER_LENGTH = 6; + + /** + * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1 + */ + static final int FORMAT_GSM = 1; + + /** + * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2 + */ + static final int FORMAT_UMTS = 2; + + /** + * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3 + */ + static final int FORMAT_ETWS_PRIMARY = 3; + + /** + * Message type value as defined in 3gpp TS 25.324, section 11.1. + */ + private static final int MESSAGE_TYPE_CBS_MESSAGE = 1; + + /** + * Length of GSM pdus + */ + private static final int PDU_LENGTH_GSM = 88; + + /** + * Maximum length of ETWS primary message GSM pdus + */ + private static final int PDU_LENGTH_ETWS = 56; + + private final int geographicalScope; + + /** The serial number combines geographical scope, message code, and update number. */ + private final int serialNumber; + + /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */ + private final int messageIdentifier; + + private final int dataCodingScheme; + + private final int pageIndex; + + private final int nrOfPages; + + private final int format; + + /** ETWS warning notification info. */ + private final SmsCbEtwsInfo mEtwsInfo; + + /** CMAS warning notification info. */ + private final SmsCbCmasInfo mCmasInfo; + + public SmsCbHeader(byte[] pdu) throws IllegalArgumentException { + if (pdu == null || pdu.length < PDU_HEADER_LENGTH) { + throw new IllegalArgumentException("Illegal PDU"); + } + + if (pdu.length <= PDU_LENGTH_ETWS) { + format = FORMAT_ETWS_PRIMARY; + geographicalScope = (pdu[0] & 0xc0) >> 6; + serialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff); + messageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff); + dataCodingScheme = -1; + pageIndex = -1; + nrOfPages = -1; + boolean emergencyUserAlert = (pdu[4] & 0x1) != 0; + boolean activatePopup = (pdu[5] & 0x80) != 0; + int warningType = (pdu[4] & 0xfe) >> 1; + byte[] warningSecurityInfo; + // copy the Warning-Security-Information, if present + if (pdu.length > PDU_HEADER_LENGTH) { + warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length); + } else { + warningSecurityInfo = null; + } + mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, + warningSecurityInfo); + mCmasInfo = null; + return; // skip the ETWS/CMAS initialization code for regular notifications + } else if (pdu.length <= PDU_LENGTH_GSM) { + // GSM pdus are no more than 88 bytes + format = FORMAT_GSM; + geographicalScope = (pdu[0] & 0xc0) >> 6; + serialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff); + messageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff); + dataCodingScheme = pdu[4] & 0xff; + + // Check for invalid page parameter + int pageIndex = (pdu[5] & 0xf0) >> 4; + int nrOfPages = pdu[5] & 0x0f; + + if (pageIndex == 0 || nrOfPages == 0 || pageIndex > nrOfPages) { + pageIndex = 1; + nrOfPages = 1; + } + + this.pageIndex = pageIndex; + this.nrOfPages = nrOfPages; + } else { + // UMTS pdus are always at least 90 bytes since the payload includes + // a number-of-pages octet and also one length octet per page + format = FORMAT_UMTS; + + int messageType = pdu[0]; + + if (messageType != MESSAGE_TYPE_CBS_MESSAGE) { + throw new IllegalArgumentException("Unsupported message type " + messageType); + } + + messageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff; + geographicalScope = (pdu[3] & 0xc0) >> 6; + serialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff); + dataCodingScheme = pdu[5] & 0xff; + + // We will always consider a UMTS message as having one single page + // since there's only one instance of the header, even though the + // actual payload may contain several pages. + pageIndex = 1; + nrOfPages = 1; + } + + if (isEtwsMessage()) { + boolean emergencyUserAlert = isEtwsEmergencyUserAlert(); + boolean activatePopup = isEtwsPopupAlert(); + int warningType = getEtwsWarningType(); + mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, null); + mCmasInfo = null; + } else if (isCmasMessage()) { + int messageClass = getCmasMessageClass(); + int severity = getCmasSeverity(); + int urgency = getCmasUrgency(); + int certainty = getCmasCertainty(); + mEtwsInfo = null; + mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty); + } else { + mEtwsInfo = null; + mCmasInfo = null; + } + } + + int getGeographicalScope() { + return geographicalScope; + } + + int getSerialNumber() { + return serialNumber; + } + + int getServiceCategory() { + return messageIdentifier; + } + + int getDataCodingScheme() { + return dataCodingScheme; + } + + int getPageIndex() { + return pageIndex; + } + + int getNumberOfPages() { + return nrOfPages; + } + + SmsCbEtwsInfo getEtwsInfo() { + return mEtwsInfo; + } + + SmsCbCmasInfo getCmasInfo() { + return mCmasInfo; + } + + /** + * Return whether this broadcast is an emergency (PWS) message type. + * @return true if this message is emergency type; false otherwise + */ + boolean isEmergencyMessage() { + return messageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER + && messageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER; + } + + /** + * Return whether this broadcast is an ETWS emergency message type. + * @return true if this message is ETWS emergency type; false otherwise + */ + private boolean isEtwsMessage() { + return (messageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK) + == SmsCbConstants.MESSAGE_ID_ETWS_TYPE; + } + + /** + * Return whether this broadcast is an ETWS primary notification. + * @return true if this message is an ETWS primary notification; false otherwise + */ + boolean isEtwsPrimaryNotification() { + return format == FORMAT_ETWS_PRIMARY; + } + + /** + * Return whether this broadcast is in UMTS format. + * @return true if this message is in UMTS format; false otherwise + */ + boolean isUmtsFormat() { + return format == FORMAT_UMTS; + } + + /** + * Return whether this message is a CMAS emergency message type. + * @return true if this message is CMAS emergency type; false otherwise + */ + private boolean isCmasMessage() { + return messageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER + && messageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER; + } + + /** + * Return whether the popup alert flag is set for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return true if the message code indicates a popup alert should be displayed + */ + private boolean isEtwsPopupAlert() { + return (serialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0; + } + + /** + * Return whether the emergency user alert flag is set for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return true if the message code indicates an emergency user alert + */ + private boolean isEtwsEmergencyUserAlert() { + return (serialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0; + } + + /** + * Returns the warning type for an ETWS warning notification. + * This method assumes that the message ID has already been checked for ETWS type. + * + * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24 + */ + private int getEtwsWarningType() { + return messageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING; + } + + /** + * Returns the message class for a CMAS warning notification. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS message class as defined in {@link SmsCbCmasInfo} + */ + private int getCmasMessageClass() { + switch (messageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL: + return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: + return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: + return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE: + return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE: + return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE; + + default: + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Returns the severity for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS severity as defined in {@link SmsCbCmasInfo} + */ + private int getCmasSeverity() { + switch (messageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + + default: + return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + } + } + + /** + * Returns the urgency for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS urgency as defined in {@link SmsCbCmasInfo} + */ + private int getCmasUrgency() { + switch (messageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + + default: + return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + } + } + + /** + * Returns the certainty for a CMAS warning notification. This is only available for extreme + * and severe alerts, not for other types such as Presidential Level and AMBER alerts. + * This method assumes that the message ID has already been checked for CMAS type. + * @return the CMAS certainty as defined in {@link SmsCbCmasInfo} + */ + private int getCmasCertainty() { + switch (messageIdentifier) { + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + + default: + return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + } + } + + @Override + public String toString() { + return "SmsCbHeader{GS=" + geographicalScope + ", serialNumber=0x" + + Integer.toHexString(serialNumber) + + ", messageIdentifier=0x" + Integer.toHexString(messageIdentifier) + + ", DCS=0x" + Integer.toHexString(dataCodingScheme) + + ", page " + pageIndex + " of " + nrOfPages + '}'; + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SmsMessage.java b/src/java/com/android/internal/telephony/gsm/SmsMessage.java new file mode 100644 index 0000000..1ed1478 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SmsMessage.java @@ -0,0 +1,1180 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.telephony.PhoneNumberUtils; +import android.text.format.Time; +import android.util.Log; + +import com.android.internal.telephony.EncodeException; +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.SmsMessageBase; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; + +import static com.android.internal.telephony.SmsConstants.MessageClass; +import static com.android.internal.telephony.SmsConstants.ENCODING_UNKNOWN; +import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_8BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_16BIT; +import static com.android.internal.telephony.SmsConstants.ENCODING_KSC5601; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_SEPTETS; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES; +import static com.android.internal.telephony.SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + +/** + * A Short Message Service message. + * + */ +public class SmsMessage extends SmsMessageBase { + static final String LOG_TAG = "GSM"; + + private MessageClass messageClass; + + /** + * TP-Message-Type-Indicator + * 9.2.3 + */ + private int mti; + + /** TP-Protocol-Identifier (TP-PID) */ + private int protocolIdentifier; + + // TP-Data-Coding-Scheme + // see TS 23.038 + private int dataCodingScheme; + + // TP-Reply-Path + // e.g. 23.040 9.2.2.1 + private boolean replyPathPresent = false; + + // "Message Marked for Automatic Deletion Group" + // 23.038 Section 4 + private boolean automaticDeletion; + + /** True if Status Report is for SMS-SUBMIT; false for SMS-COMMAND. */ + private boolean forSubmit; + + /** The address of the receiver. */ + private GsmSmsAddress recipientAddress; + + /** Time when SMS-SUBMIT was delivered from SC to MSE. */ + private long dischargeTimeMillis; + + /** + * TP-Status - status of a previously submitted SMS. + * This field applies to SMS-STATUS-REPORT messages. 0 indicates success; + * see TS 23.040, 9.2.3.15 for description of other possible values. + */ + private int status; + + /** + * TP-Status - status of a previously submitted SMS. + * This field is true iff the message is a SMS-STATUS-REPORT message. + */ + private boolean isStatusReportMessage = false; + + public static class SubmitPdu extends SubmitPduBase { + } + + /** + * Create an SmsMessage from a raw PDU. + */ + public static SmsMessage createFromPdu(byte[] pdu) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * 3GPP TS 23.040 9.2.3.9 specifies that Type Zero messages are indicated + * by TP_PID field set to value 0x40 + */ + public boolean isTypeZero() { + return (protocolIdentifier == 0x40); + } + + /** + * TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the + * +CMT unsolicited response (PDU mode, of course) + * +CMT: [<alpha>],<length><CR><LF><pdu> + * + * Only public for debugging + * + * {@hide} + */ + public static SmsMessage newFromCMT(String[] lines) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(IccUtils.hexStringToBytes(lines[1])); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** @hide */ + public static SmsMessage newFromCDS(String line) { + try { + SmsMessage msg = new SmsMessage(); + msg.parsePdu(IccUtils.hexStringToBytes(line)); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "CDS SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * Create an SmsMessage from an SMS EF record. + * + * @param index Index of SMS record. This should be index in ArrayList + * returned by SmsManager.getAllMessagesFromSim + 1. + * @param data Record data. + * @return An SmsMessage representing the record. + * + * @hide + */ + public static SmsMessage createFromEfRecord(int index, byte[] data) { + try { + SmsMessage msg = new SmsMessage(); + + msg.indexOnIcc = index; + + // First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT, + // or STORED_UNSENT + // See TS 51.011 10.5.3 + if ((data[0] & 1) == 0) { + Log.w(LOG_TAG, + "SMS parsing failed: Trying to parse a free record"); + return null; + } else { + msg.statusOnIcc = data[0] & 0x07; + } + + int size = data.length - 1; + + // Note: Data may include trailing FF's. That's OK; message + // should still parse correctly. + byte[] pdu = new byte[size]; + System.arraycopy(data, 1, pdu, 0, size); + msg.parsePdu(pdu); + return msg; + } catch (RuntimeException ex) { + Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex); + return null; + } + } + + /** + * Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the + * length in bytes (not hex chars) less the SMSC header + */ + public static int getTPLayerLengthForPDU(String pdu) { + int len = pdu.length() / 2; + int smscLen = Integer.parseInt(pdu.substring(0, 2), 16); + + return len - smscLen - 1; + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested, byte[] header) { + return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, header, + ENCODING_UNKNOWN, 0, 0); + } + + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message using the + * specified encoding. + * + * @param scAddress Service Centre address. Null means use default. + * @param encoding Encoding defined by constants in + * com.android.internal.telephony.SmsConstants.ENCODING_* + * @param languageTable + * @param languageShiftTable + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + * @hide + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested, byte[] header, int encoding, + int languageTable, int languageShiftTable) { + + // Perform null parameter checks. + if (message == null || destinationAddress == null) { + return null; + } + + if (encoding == ENCODING_UNKNOWN) { + // Find the best encoding to use + TextEncodingDetails ted = calculateLength(message, false); + encoding = ted.codeUnitSize; + languageTable = ted.languageTable; + languageShiftTable = ted.languageShiftTable; + + if (encoding == ENCODING_7BIT && + (languageTable != 0 || languageShiftTable != 0)) { + if (header != null) { + SmsHeader smsHeader = SmsHeader.fromByteArray(header); + if (smsHeader.languageTable != languageTable + || smsHeader.languageShiftTable != languageShiftTable) { + Log.w(LOG_TAG, "Updating language table in SMS header: " + + smsHeader.languageTable + " -> " + languageTable + ", " + + smsHeader.languageShiftTable + " -> " + languageShiftTable); + smsHeader.languageTable = languageTable; + smsHeader.languageShiftTable = languageShiftTable; + header = SmsHeader.toByteArray(smsHeader); + } + } else { + SmsHeader smsHeader = new SmsHeader(); + smsHeader.languageTable = languageTable; + smsHeader.languageShiftTable = languageShiftTable; + header = SmsHeader.toByteArray(smsHeader); + } + } + } + + SubmitPdu ret = new SubmitPdu(); + // MTI = SMS-SUBMIT, UDHI = header != null + byte mtiByte = (byte)(0x01 | (header != null ? 0x40 : 0x00)); + ByteArrayOutputStream bo = getSubmitPduHead( + scAddress, destinationAddress, mtiByte, + statusReportRequested, ret); + + // User Data (and length) + byte[] userData; + try { + if (encoding == ENCODING_7BIT) { + userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, header, + languageTable, languageShiftTable); + } else { //assume UCS-2 + try { + userData = encodeUCS2(message, header); + } catch(UnsupportedEncodingException uex) { + Log.e(LOG_TAG, + "Implausible UnsupportedEncodingException ", + uex); + return null; + } + } + } catch (EncodeException ex) { + // Encoding to the 7-bit alphabet failed. Let's see if we can + // send it as a UCS-2 encoded message + try { + userData = encodeUCS2(message, header); + encoding = ENCODING_16BIT; + } catch(UnsupportedEncodingException uex) { + Log.e(LOG_TAG, + "Implausible UnsupportedEncodingException ", + uex); + return null; + } + } + + if (encoding == ENCODING_7BIT) { + if ((0xff & userData[0]) > MAX_USER_DATA_SEPTETS) { + // Message too long + Log.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " septets)"); + return null; + } + // TP-Data-Coding-Scheme + // Default encoding, uncompressed + // To test writing messages to the SIM card, change this value 0x00 + // to 0x12, which means "bits 1 and 0 contain message class, and the + // class is 2". Note that this takes effect for the sender. In other + // words, messages sent by the phone with this change will end up on + // the receiver's SIM card. You can then send messages to yourself + // (on a phone with this change) and they'll end up on the SIM card. + bo.write(0x00); + } else { // assume UCS-2 + if ((0xff & userData[0]) > MAX_USER_DATA_BYTES) { + // Message too long + Log.e(LOG_TAG, "Message too long (" + (0xff & userData[0]) + " bytes)"); + return null; + } + // TP-Data-Coding-Scheme + // UCS-2 encoding, uncompressed + bo.write(0x08); + } + + // (no TP-Validity-Period) + bo.write(userData, 0, userData.length); + ret.encodedMessage = bo.toByteArray(); + return ret; + } + + /** + * Packs header and UCS-2 encoded message. Includes TP-UDL & TP-UDHL if necessary + * + * @return + * @throws UnsupportedEncodingException + */ + private static byte[] encodeUCS2(String message, byte[] header) + throws UnsupportedEncodingException { + byte[] userData, textPart; + textPart = message.getBytes("utf-16be"); + + if (header != null) { + // Need 1 byte for UDHL + userData = new byte[header.length + textPart.length + 1]; + + userData[0] = (byte)header.length; + System.arraycopy(header, 0, userData, 1, header.length); + System.arraycopy(textPart, 0, userData, header.length + 1, textPart.length); + } + else { + userData = textPart; + } + byte[] ret = new byte[userData.length+1]; + ret[0] = (byte) (userData.length & 0xff ); + System.arraycopy(userData, 0, ret, 1, userData.length); + return ret; + } + + /** + * Get an SMS-SUBMIT PDU for a destination address and a message + * + * @param scAddress Service Centre address. Null means use default. + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, String message, + boolean statusReportRequested) { + + return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, null); + } + + /** + * Get an SMS-SUBMIT PDU for a data message to a destination address & port + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param destinationPort the port to deliver the message to at the + * destination + * @param data the data for the message + * @return a <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message. + * Returns null on encode error. + */ + public static SubmitPdu getSubmitPdu(String scAddress, + String destinationAddress, int destinationPort, byte[] data, + boolean statusReportRequested) { + + SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs(); + portAddrs.destPort = destinationPort; + portAddrs.origPort = 0; + portAddrs.areEightBits = false; + + SmsHeader smsHeader = new SmsHeader(); + smsHeader.portAddrs = portAddrs; + + byte[] smsHeaderData = SmsHeader.toByteArray(smsHeader); + + if ((data.length + smsHeaderData.length + 1) > MAX_USER_DATA_BYTES) { + Log.e(LOG_TAG, "SMS data message may only contain " + + (MAX_USER_DATA_BYTES - smsHeaderData.length - 1) + " bytes"); + return null; + } + + SubmitPdu ret = new SubmitPdu(); + ByteArrayOutputStream bo = getSubmitPduHead( + scAddress, destinationAddress, (byte) 0x41, // MTI = SMS-SUBMIT, + // TP-UDHI = true + statusReportRequested, ret); + + // TP-Data-Coding-Scheme + // No class, 8 bit data + bo.write(0x04); + + // (no TP-Validity-Period) + + // Total size + bo.write(data.length + smsHeaderData.length + 1); + + // User data header + bo.write(smsHeaderData.length); + bo.write(smsHeaderData, 0, smsHeaderData.length); + + // User data + bo.write(data, 0, data.length); + + ret.encodedMessage = bo.toByteArray(); + return ret; + } + + /** + * Create the beginning of a SUBMIT PDU. This is the part of the + * SUBMIT PDU that is common to the two versions of {@link #getSubmitPdu}, + * one of which takes a byte array and the other of which takes a + * <code>String</code>. + * + * @param scAddress Service Centre address. null == use default + * @param destinationAddress the address of the destination for the message + * @param mtiByte + * @param ret <code>SubmitPdu</code> containing the encoded SC + * address, if applicable, and the encoded message + */ + private static ByteArrayOutputStream getSubmitPduHead( + String scAddress, String destinationAddress, byte mtiByte, + boolean statusReportRequested, SubmitPdu ret) { + ByteArrayOutputStream bo = new ByteArrayOutputStream( + MAX_USER_DATA_BYTES + 40); + + // SMSC address with length octet, or 0 + if (scAddress == null) { + ret.encodedScAddress = null; + } else { + ret.encodedScAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength( + scAddress); + } + + // TP-Message-Type-Indicator (and friends) + if (statusReportRequested) { + // Set TP-Status-Report-Request bit. + mtiByte |= 0x20; + if (false) Log.d(LOG_TAG, "SMS status report requested"); + } + bo.write(mtiByte); + + // space for TP-Message-Reference + bo.write(0); + + byte[] daBytes; + + daBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(destinationAddress); + + // destination address length in BCD digits, ignoring TON byte and pad + // TODO Should be better. + bo.write((daBytes.length - 1) * 2 + - ((daBytes[daBytes.length - 1] & 0xf0) == 0xf0 ? 1 : 0)); + + // destination address + bo.write(daBytes, 0, daBytes.length); + + // TP-Protocol-Identifier + bo.write(0); + return bo; + } + + private static class PduParser { + byte pdu[]; + int cur; + SmsHeader userDataHeader; + byte[] userData; + int mUserDataSeptetPadding; + int mUserDataSize; + + PduParser(byte[] pdu) { + this.pdu = pdu; + cur = 0; + mUserDataSeptetPadding = 0; + } + + /** + * Parse and return the SC address prepended to SMS messages coming via + * the TS 27.005 / AT interface. Returns null on invalid address + */ + String getSCAddress() { + int len; + String ret; + + // length of SC Address + len = getByte(); + + if (len == 0) { + // no SC address + ret = null; + } else { + // SC address + try { + ret = PhoneNumberUtils + .calledPartyBCDToString(pdu, cur, len); + } catch (RuntimeException tr) { + Log.d(LOG_TAG, "invalid SC address: ", tr); + ret = null; + } + } + + cur += len; + + return ret; + } + + /** + * returns non-sign-extended byte value + */ + int getByte() { + return pdu[cur++] & 0xff; + } + + /** + * Any address except the SC address (eg, originating address) See TS + * 23.040 9.1.2.5 + */ + GsmSmsAddress getAddress() { + GsmSmsAddress ret; + + // "The Address-Length field is an integer representation of + // the number field, i.e. excludes any semi-octet containing only + // fill bits." + // The TOA field is not included as part of this + int addressLength = pdu[cur] & 0xff; + int lengthBytes = 2 + (addressLength + 1) / 2; + + ret = new GsmSmsAddress(pdu, cur, lengthBytes); + + cur += lengthBytes; + + return ret; + } + + /** + * Parses an SC timestamp and returns a currentTimeMillis()-style + * timestamp + */ + + long getSCTimestampMillis() { + // TP-Service-Centre-Time-Stamp + int year = IccUtils.gsmBcdByteToInt(pdu[cur++]); + int month = IccUtils.gsmBcdByteToInt(pdu[cur++]); + int day = IccUtils.gsmBcdByteToInt(pdu[cur++]); + int hour = IccUtils.gsmBcdByteToInt(pdu[cur++]); + int minute = IccUtils.gsmBcdByteToInt(pdu[cur++]); + int second = IccUtils.gsmBcdByteToInt(pdu[cur++]); + + // For the timezone, the most significant bit of the + // least significant nibble is the sign byte + // (meaning the max range of this field is 79 quarter-hours, + // which is more than enough) + + byte tzByte = pdu[cur++]; + + // Mask out sign bit. + int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08))); + + timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset; + + Time time = new Time(Time.TIMEZONE_UTC); + + // It's 2006. Should I really support years < 2000? + time.year = year >= 90 ? year + 1900 : year + 2000; + time.month = month - 1; + time.monthDay = day; + time.hour = hour; + time.minute = minute; + time.second = second; + + // Timezone offset is in quarter hours. + return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000); + } + + /** + * Pulls the user data out of the PDU, and separates the payload from + * the header if there is one. + * + * @param hasUserDataHeader true if there is a user data header + * @param dataInSeptets true if the data payload is in septets instead + * of octets + * @return the number of septets or octets in the user data payload + */ + int constructUserData(boolean hasUserDataHeader, boolean dataInSeptets) { + int offset = cur; + int userDataLength = pdu[offset++] & 0xff; + int headerSeptets = 0; + int userDataHeaderLength = 0; + + if (hasUserDataHeader) { + userDataHeaderLength = pdu[offset++] & 0xff; + + byte[] udh = new byte[userDataHeaderLength]; + System.arraycopy(pdu, offset, udh, 0, userDataHeaderLength); + userDataHeader = SmsHeader.fromByteArray(udh); + offset += userDataHeaderLength; + + int headerBits = (userDataHeaderLength + 1) * 8; + headerSeptets = headerBits / 7; + headerSeptets += (headerBits % 7) > 0 ? 1 : 0; + mUserDataSeptetPadding = (headerSeptets * 7) - headerBits; + } + + int bufferLen; + if (dataInSeptets) { + /* + * Here we just create the user data length to be the remainder of + * the pdu minus the user data header, since userDataLength means + * the number of uncompressed septets. + */ + bufferLen = pdu.length - offset; + } else { + /* + * userDataLength is the count of octets, so just subtract the + * user data header. + */ + bufferLen = userDataLength - (hasUserDataHeader ? (userDataHeaderLength + 1) : 0); + if (bufferLen < 0) { + bufferLen = 0; + } + } + + userData = new byte[bufferLen]; + System.arraycopy(pdu, offset, userData, 0, userData.length); + cur = offset; + + if (dataInSeptets) { + // Return the number of septets + int count = userDataLength - headerSeptets; + // If count < 0, return 0 (means UDL was probably incorrect) + return count < 0 ? 0 : count; + } else { + // Return the number of octets + return userData.length; + } + } + + /** + * Returns the user data payload, not including the headers + * + * @return the user data payload, not including the headers + */ + byte[] getUserData() { + return userData; + } + + /** + * Returns the number of padding bits at the beginning of the user data + * array before the start of the septets. + * + * @return the number of padding bits at the beginning of the user data + * array before the start of the septets + */ + int getUserDataSeptetPadding() { + return mUserDataSeptetPadding; + } + + /** + * Returns an object representing the user data headers + * + * {@hide} + */ + SmsHeader getUserDataHeader() { + return userDataHeader; + } + + /** + * Interprets the user data payload as packed GSM 7bit characters, and + * decodes them into a String. + * + * @param septetCount the number of septets in the user data payload + * @return a String with the decoded characters + */ + String getUserDataGSM7Bit(int septetCount, int languageTable, + int languageShiftTable) { + String ret; + + ret = GsmAlphabet.gsm7BitPackedToString(pdu, cur, septetCount, + mUserDataSeptetPadding, languageTable, languageShiftTable); + + cur += (septetCount * 7) / 8; + + return ret; + } + + /** + * Interprets the user data payload as UCS2 characters, and + * decodes them into a String. + * + * @param byteCount the number of bytes in the user data payload + * @return a String with the decoded characters + */ + String getUserDataUCS2(int byteCount) { + String ret; + + try { + ret = new String(pdu, cur, byteCount, "utf-16"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Log.e(LOG_TAG, "implausible UnsupportedEncodingException", ex); + } + + cur += byteCount; + return ret; + } + + /** + * Interprets the user data payload as KSC-5601 characters, and + * decodes them into a String. + * + * @param byteCount the number of bytes in the user data payload + * @return a String with the decoded characters + */ + String getUserDataKSC5601(int byteCount) { + String ret; + + try { + ret = new String(pdu, cur, byteCount, "KSC5601"); + } catch (UnsupportedEncodingException ex) { + ret = ""; + Log.e(LOG_TAG, "implausible UnsupportedEncodingException", ex); + } + + cur += byteCount; + return ret; + } + + boolean moreDataPresent() { + return (pdu.length > cur); + } + } + + /** + * Calculate the number of septets needed to encode the message. + * + * @param msgBody the message to encode + * @param use7bitOnly ignore (but still count) illegal characters if true + * @return TextEncodingDetails + */ + public static TextEncodingDetails calculateLength(CharSequence msgBody, + boolean use7bitOnly) { + TextEncodingDetails ted = GsmAlphabet.countGsmSeptets(msgBody, use7bitOnly); + if (ted == null) { + ted = new TextEncodingDetails(); + int octets = msgBody.length() * 2; + ted.codeUnitCount = msgBody.length(); + if (octets > MAX_USER_DATA_BYTES) { + ted.msgCount = (octets + (MAX_USER_DATA_BYTES_WITH_HEADER - 1)) / + MAX_USER_DATA_BYTES_WITH_HEADER; + ted.codeUnitsRemaining = ((ted.msgCount * + MAX_USER_DATA_BYTES_WITH_HEADER) - octets) / 2; + } else { + ted.msgCount = 1; + ted.codeUnitsRemaining = (MAX_USER_DATA_BYTES - octets)/2; + } + ted.codeUnitSize = ENCODING_16BIT; + } + return ted; + } + + /** {@inheritDoc} */ + @Override + public int getProtocolIdentifier() { + return protocolIdentifier; + } + + /** + * Returns the TP-Data-Coding-Scheme byte, for acknowledgement of SMS-PP download messages. + * @return the TP-DCS field of the SMS header + */ + int getDataCodingScheme() { + return dataCodingScheme; + } + + /** {@inheritDoc} */ + @Override + public boolean isReplace() { + return (protocolIdentifier & 0xc0) == 0x40 + && (protocolIdentifier & 0x3f) > 0 + && (protocolIdentifier & 0x3f) < 8; + } + + /** {@inheritDoc} */ + @Override + public boolean isCphsMwiMessage() { + return ((GsmSmsAddress) originatingAddress).isCphsVoiceMessageClear() + || ((GsmSmsAddress) originatingAddress).isCphsVoiceMessageSet(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMWIClearMessage() { + if (isMwi && !mwiSense) { + return true; + } + + return originatingAddress != null + && ((GsmSmsAddress) originatingAddress).isCphsVoiceMessageClear(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMWISetMessage() { + if (isMwi && mwiSense) { + return true; + } + + return originatingAddress != null + && ((GsmSmsAddress) originatingAddress).isCphsVoiceMessageSet(); + } + + /** {@inheritDoc} */ + @Override + public boolean isMwiDontStore() { + if (isMwi && mwiDontStore) { + return true; + } + + if (isCphsMwiMessage()) { + // See CPHS 4.2 Section B.4.2.1 + // If the user data is a single space char, do not store + // the message. Otherwise, store and display as usual + if (" ".equals(getMessageBody())) { + ; + } + return true; + } + + return false; + } + + /** {@inheritDoc} */ + @Override + public int getStatus() { + return status; + } + + /** {@inheritDoc} */ + @Override + public boolean isStatusReportMessage() { + return isStatusReportMessage; + } + + /** {@inheritDoc} */ + @Override + public boolean isReplyPathPresent() { + return replyPathPresent; + } + + /** + * TS 27.005 3.1, <pdu> definition "In the case of SMS: 3GPP TS 24.011 [6] + * SC address followed by 3GPP TS 23.040 [3] TPDU in hexadecimal format: + * ME/TA converts each octet of TP data unit into two IRA character long + * hex number (e.g. octet with integer value 42 is presented to TE as two + * characters 2A (IRA 50 and 65))" ...in the case of cell broadcast, + * something else... + */ + private void parsePdu(byte[] pdu) { + mPdu = pdu; + // Log.d(LOG_TAG, "raw sms message:"); + // Log.d(LOG_TAG, s); + + PduParser p = new PduParser(pdu); + + scAddress = p.getSCAddress(); + + if (scAddress != null) { + if (false) Log.d(LOG_TAG, "SMS SC address: " + scAddress); + } + + // TODO(mkf) support reply path, user data header indicator + + // TP-Message-Type-Indicator + // 9.2.3 + int firstByte = p.getByte(); + + mti = firstByte & 0x3; + switch (mti) { + // TP-Message-Type-Indicator + // 9.2.3 + case 0: + case 3: //GSM 03.40 9.2.3.1: MTI == 3 is Reserved. + //This should be processed in the same way as MTI == 0 (Deliver) + parseSmsDeliver(p, firstByte); + break; + case 2: + parseSmsStatusReport(p, firstByte); + break; + default: + // TODO(mkf) the rest of these + throw new RuntimeException("Unsupported message type"); + } + } + + /** + * Parses a SMS-STATUS-REPORT message. + * + * @param p A PduParser, cued past the first byte. + * @param firstByte The first byte of the PDU, which contains MTI, etc. + */ + private void parseSmsStatusReport(PduParser p, int firstByte) { + isStatusReportMessage = true; + + // TP-Status-Report-Qualifier bit == 0 for SUBMIT + forSubmit = (firstByte & 0x20) == 0x00; + // TP-Message-Reference + messageRef = p.getByte(); + // TP-Recipient-Address + recipientAddress = p.getAddress(); + // TP-Service-Centre-Time-Stamp + scTimeMillis = p.getSCTimestampMillis(); + // TP-Discharge-Time + dischargeTimeMillis = p.getSCTimestampMillis(); + // TP-Status + status = p.getByte(); + + // The following are optional fields that may or may not be present. + if (p.moreDataPresent()) { + // TP-Parameter-Indicator + int extraParams = p.getByte(); + int moreExtraParams = extraParams; + while ((moreExtraParams & 0x80) != 0) { + // We only know how to parse a few extra parameters, all + // indicated in the first TP-PI octet, so skip over any + // additional TP-PI octets. + moreExtraParams = p.getByte(); + } + // TP-Protocol-Identifier + if ((extraParams & 0x01) != 0) { + protocolIdentifier = p.getByte(); + } + // TP-Data-Coding-Scheme + if ((extraParams & 0x02) != 0) { + dataCodingScheme = p.getByte(); + } + // TP-User-Data-Length (implies existence of TP-User-Data) + if ((extraParams & 0x04) != 0) { + boolean hasUserDataHeader = (firstByte & 0x40) == 0x40; + parseUserData(p, hasUserDataHeader); + } + } + } + + private void parseSmsDeliver(PduParser p, int firstByte) { + replyPathPresent = (firstByte & 0x80) == 0x80; + + originatingAddress = p.getAddress(); + + if (originatingAddress != null) { + if (false) Log.v(LOG_TAG, "SMS originating address: " + + originatingAddress.address); + } + + // TP-Protocol-Identifier (TP-PID) + // TS 23.040 9.2.3.9 + protocolIdentifier = p.getByte(); + + // TP-Data-Coding-Scheme + // see TS 23.038 + dataCodingScheme = p.getByte(); + + if (false) { + Log.v(LOG_TAG, "SMS TP-PID:" + protocolIdentifier + + " data coding scheme: " + dataCodingScheme); + } + + scTimeMillis = p.getSCTimestampMillis(); + + if (false) Log.d(LOG_TAG, "SMS SC timestamp: " + scTimeMillis); + + boolean hasUserDataHeader = (firstByte & 0x40) == 0x40; + + parseUserData(p, hasUserDataHeader); + } + + /** + * Parses the User Data of an SMS. + * + * @param p The current PduParser. + * @param hasUserDataHeader Indicates whether a header is present in the + * User Data. + */ + private void parseUserData(PduParser p, boolean hasUserDataHeader) { + boolean hasMessageClass = false; + boolean userDataCompressed = false; + + int encodingType = ENCODING_UNKNOWN; + + // Look up the data encoding scheme + if ((dataCodingScheme & 0x80) == 0) { + // Bits 7..4 == 0xxx + automaticDeletion = (0 != (dataCodingScheme & 0x40)); + userDataCompressed = (0 != (dataCodingScheme & 0x20)); + hasMessageClass = (0 != (dataCodingScheme & 0x10)); + + if (userDataCompressed) { + Log.w(LOG_TAG, "4 - Unsupported SMS data coding scheme " + + "(compression) " + (dataCodingScheme & 0xff)); + } else { + switch ((dataCodingScheme >> 2) & 0x3) { + case 0: // GSM 7 bit default alphabet + encodingType = ENCODING_7BIT; + break; + + case 2: // UCS 2 (16bit) + encodingType = ENCODING_16BIT; + break; + + case 1: // 8 bit data + case 3: // reserved + Log.w(LOG_TAG, "1 - Unsupported SMS data coding scheme " + + (dataCodingScheme & 0xff)); + encodingType = ENCODING_8BIT; + break; + } + } + } else if ((dataCodingScheme & 0xf0) == 0xf0) { + automaticDeletion = false; + hasMessageClass = true; + userDataCompressed = false; + + if (0 == (dataCodingScheme & 0x04)) { + // GSM 7 bit default alphabet + encodingType = ENCODING_7BIT; + } else { + // 8 bit data + encodingType = ENCODING_8BIT; + } + } else if ((dataCodingScheme & 0xF0) == 0xC0 + || (dataCodingScheme & 0xF0) == 0xD0 + || (dataCodingScheme & 0xF0) == 0xE0) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + + // 0xC0 == 7 bit, don't store + // 0xD0 == 7 bit, store + // 0xE0 == UCS-2, store + + if ((dataCodingScheme & 0xF0) == 0xE0) { + encodingType = ENCODING_16BIT; + } else { + encodingType = ENCODING_7BIT; + } + + userDataCompressed = false; + boolean active = ((dataCodingScheme & 0x08) == 0x08); + + // bit 0x04 reserved + + if ((dataCodingScheme & 0x03) == 0x00) { + isMwi = true; + mwiSense = active; + mwiDontStore = ((dataCodingScheme & 0xF0) == 0xC0); + } else { + isMwi = false; + + Log.w(LOG_TAG, "MWI for fax, email, or other " + + (dataCodingScheme & 0xff)); + } + } else if ((dataCodingScheme & 0xC0) == 0x80) { + // 3GPP TS 23.038 V7.0.0 (2006-03) section 4 + // 0x80..0xBF == Reserved coding groups + if (dataCodingScheme == 0x84) { + // This value used for KSC5601 by carriers in Korea. + encodingType = ENCODING_KSC5601; + } else { + Log.w(LOG_TAG, "5 - Unsupported SMS data coding scheme " + + (dataCodingScheme & 0xff)); + } + } else { + Log.w(LOG_TAG, "3 - Unsupported SMS data coding scheme " + + (dataCodingScheme & 0xff)); + } + + // set both the user data and the user data header. + int count = p.constructUserData(hasUserDataHeader, + encodingType == ENCODING_7BIT); + this.userData = p.getUserData(); + this.userDataHeader = p.getUserDataHeader(); + + switch (encodingType) { + case ENCODING_UNKNOWN: + case ENCODING_8BIT: + messageBody = null; + break; + + case ENCODING_7BIT: + messageBody = p.getUserDataGSM7Bit(count, + hasUserDataHeader ? userDataHeader.languageTable : 0, + hasUserDataHeader ? userDataHeader.languageShiftTable : 0); + break; + + case ENCODING_16BIT: + messageBody = p.getUserDataUCS2(count); + break; + + case ENCODING_KSC5601: + messageBody = p.getUserDataKSC5601(count); + break; + } + + if (false) Log.v(LOG_TAG, "SMS message body (raw): '" + messageBody + "'"); + + if (messageBody != null) { + parseMessageBody(); + } + + if (!hasMessageClass) { + messageClass = MessageClass.UNKNOWN; + } else { + switch (dataCodingScheme & 0x3) { + case 0: + messageClass = MessageClass.CLASS_0; + break; + case 1: + messageClass = MessageClass.CLASS_1; + break; + case 2: + messageClass = MessageClass.CLASS_2; + break; + case 3: + messageClass = MessageClass.CLASS_3; + break; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public MessageClass getMessageClass() { + return messageClass; + } + + /** + * Returns true if this is a (U)SIM data download type SM. + * See 3GPP TS 31.111 section 9.1 and TS 23.040 section 9.2.3.9. + * + * @return true if this is a USIM data download message; false otherwise + */ + boolean isUsimDataDownload() { + return messageClass == MessageClass.CLASS_2 && + (protocolIdentifier == 0x7f || protocolIdentifier == 0x7c); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/SpnOverride.java b/src/java/com/android/internal/telephony/gsm/SpnOverride.java new file mode 100644 index 0000000..918c2d2 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SpnOverride.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2009 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.internal.telephony.gsm; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.os.Environment; +import android.util.Log; +import android.util.Xml; + +import com.android.internal.util.XmlUtils; + +public class SpnOverride { + private HashMap<String, String> CarrierSpnMap; + + + static final String LOG_TAG = "GSM"; + static final String PARTNER_SPN_OVERRIDE_PATH ="etc/spn-conf.xml"; + + SpnOverride () { + CarrierSpnMap = new HashMap<String, String>(); + loadSpnOverrides(); + } + + boolean containsCarrier(String carrier) { + return CarrierSpnMap.containsKey(carrier); + } + + String getSpn(String carrier) { + return CarrierSpnMap.get(carrier); + } + + private void loadSpnOverrides() { + FileReader spnReader; + + final File spnFile = new File(Environment.getRootDirectory(), + PARTNER_SPN_OVERRIDE_PATH); + + try { + spnReader = new FileReader(spnFile); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "Can't open " + + Environment.getRootDirectory() + "/" + PARTNER_SPN_OVERRIDE_PATH); + return; + } + + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(spnReader); + + XmlUtils.beginDocument(parser, "spnOverrides"); + + while (true) { + XmlUtils.nextElement(parser); + + String name = parser.getName(); + if (!"spnOverride".equals(name)) { + break; + } + + String numeric = parser.getAttributeValue(null, "numeric"); + String data = parser.getAttributeValue(null, "spn"); + + CarrierSpnMap.put(numeric, data); + } + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Exception in spn-conf parser " + e); + } catch (IOException e) { + Log.w(LOG_TAG, "Exception in spn-conf parser " + e); + } + } + +} diff --git a/src/java/com/android/internal/telephony/gsm/SuppServiceNotification.java b/src/java/com/android/internal/telephony/gsm/SuppServiceNotification.java new file mode 100644 index 0000000..e68655e --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/SuppServiceNotification.java @@ -0,0 +1,70 @@ +/* + * 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 com.android.internal.telephony.gsm; + +import android.telephony.PhoneNumberUtils; + +/** + * Represents a Supplementary Service Notification received from the network. + * + * {@hide} + */ +public class SuppServiceNotification { + /** Type of notification: 0 = MO; 1 = MT */ + public int notificationType; + /** TS 27.007 7.17 "code1" or "code2" */ + public int code; + /** TS 27.007 7.17 "index" */ + public int index; + /** TS 27.007 7.17 "type" (MT only) */ + public int type; + /** TS 27.007 7.17 "number" (MT only) */ + public String number; + + static public final int MO_CODE_UNCONDITIONAL_CF_ACTIVE = 0; + static public final int MO_CODE_SOME_CF_ACTIVE = 1; + static public final int MO_CODE_CALL_FORWARDED = 2; + static public final int MO_CODE_CALL_IS_WAITING = 3; + static public final int MO_CODE_CUG_CALL = 4; + static public final int MO_CODE_OUTGOING_CALLS_BARRED = 5; + static public final int MO_CODE_INCOMING_CALLS_BARRED = 6; + static public final int MO_CODE_CLIR_SUPPRESSION_REJECTED = 7; + static public final int MO_CODE_CALL_DEFLECTED = 8; + + static public final int MT_CODE_FORWARDED_CALL = 0; + static public final int MT_CODE_CUG_CALL = 1; + static public final int MT_CODE_CALL_ON_HOLD = 2; + static public final int MT_CODE_CALL_RETRIEVED = 3; + static public final int MT_CODE_MULTI_PARTY_CALL = 4; + static public final int MT_CODE_ON_HOLD_CALL_RELEASED = 5; + static public final int MT_CODE_FORWARD_CHECK_RECEIVED = 6; + static public final int MT_CODE_CALL_CONNECTING_ECT = 7; + static public final int MT_CODE_CALL_CONNECTED_ECT = 8; + static public final int MT_CODE_DEFLECTED_CALL = 9; + static public final int MT_CODE_ADDITIONAL_CALL_FORWARDED = 10; + + public String toString() + { + return super.toString() + " mobile" + + (notificationType == 0 ? " originated " : " terminated ") + + " code: " + code + + " index: " + index + + " \"" + + PhoneNumberUtils.stringFromStringAndTOA(number, type) + "\" "; + } + +} diff --git a/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java b/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java new file mode 100644 index 0000000..f47ff1b --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/UsimDataDownloadHandler.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2011 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.internal.telephony.gsm; + +import android.app.Activity; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.provider.Telephony.Sms.Intents; +import android.util.Log; + +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.IccIoResult; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.cat.ComprehensionTlvTag; + +/** + * Handler for SMS-PP data download messages. + * See 3GPP TS 31.111 section 7.1.1 + */ +public class UsimDataDownloadHandler extends Handler { + private static final String TAG = "UsimDataDownloadHandler"; + + /** BER-TLV tag for SMS-PP download. TS 31.111 section 9.1. */ + private static final int BER_SMS_PP_DOWNLOAD_TAG = 0xd1; + + /** Device identity value for UICC (destination). */ + private static final int DEV_ID_UICC = 0x81; + + /** Device identity value for network (source). */ + private static final int DEV_ID_NETWORK = 0x83; + + /** Message containing new SMS-PP message to process. */ + private static final int EVENT_START_DATA_DOWNLOAD = 1; + + /** Response to SMS-PP download envelope command. */ + private static final int EVENT_SEND_ENVELOPE_RESPONSE = 2; + + private final CommandsInterface mCI; + + public UsimDataDownloadHandler(CommandsInterface commandsInterface) { + mCI = commandsInterface; + } + + /** + * Start an SMS-PP data download for the specified message. Can be called from a different + * thread than this Handler is running on. + * + * @param smsMessage the message to process + * @return Activity.RESULT_OK on success; Intents.RESULT_SMS_GENERIC_ERROR on failure + */ + public int startDataDownload(SmsMessage smsMessage) { + if (sendMessage(obtainMessage(EVENT_START_DATA_DOWNLOAD, smsMessage))) { + return Activity.RESULT_OK; // we will send SMS ACK/ERROR based on UICC response + } else { + Log.e(TAG, "startDataDownload failed to send message to start data download."); + return Intents.RESULT_SMS_GENERIC_ERROR; + } + } + + private void handleDataDownload(SmsMessage smsMessage) { + int dcs = smsMessage.getDataCodingScheme(); + int pid = smsMessage.getProtocolIdentifier(); + byte[] pdu = smsMessage.getPdu(); // includes SC address + + int scAddressLength = pdu[0] & 0xff; + int tpduIndex = scAddressLength + 1; // start of TPDU + int tpduLength = pdu.length - tpduIndex; + + int bodyLength = getEnvelopeBodyLength(scAddressLength, tpduLength); + + // Add 1 byte for SMS-PP download tag and 1-2 bytes for BER-TLV length. + // See ETSI TS 102 223 Annex C for encoding of length and tags. + int totalLength = bodyLength + 1 + (bodyLength > 127 ? 2 : 1); + + byte[] envelope = new byte[totalLength]; + int index = 0; + + // SMS-PP download tag and length (assumed to be < 256 bytes). + envelope[index++] = (byte) BER_SMS_PP_DOWNLOAD_TAG; + if (bodyLength > 127) { + envelope[index++] = (byte) 0x81; // length 128-255 encoded as 0x81 + length + } + envelope[index++] = (byte) bodyLength; + + // Device identities TLV + envelope[index++] = (byte) (0x80 | ComprehensionTlvTag.DEVICE_IDENTITIES.value()); + envelope[index++] = (byte) 2; + envelope[index++] = (byte) DEV_ID_NETWORK; + envelope[index++] = (byte) DEV_ID_UICC; + + // Address TLV (if present). Encoded length is assumed to be < 127 bytes. + if (scAddressLength != 0) { + envelope[index++] = (byte) ComprehensionTlvTag.ADDRESS.value(); + envelope[index++] = (byte) scAddressLength; + System.arraycopy(pdu, 1, envelope, index, scAddressLength); + index += scAddressLength; + } + + // SMS TPDU TLV. Length is assumed to be < 256 bytes. + envelope[index++] = (byte) (0x80 | ComprehensionTlvTag.SMS_TPDU.value()); + if (tpduLength > 127) { + envelope[index++] = (byte) 0x81; // length 128-255 encoded as 0x81 + length + } + envelope[index++] = (byte) tpduLength; + System.arraycopy(pdu, tpduIndex, envelope, index, tpduLength); + index += tpduLength; + + // Verify that we calculated the payload size correctly. + if (index != envelope.length) { + Log.e(TAG, "startDataDownload() calculated incorrect envelope length, aborting."); + acknowledgeSmsWithError(CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR); + return; + } + + String encodedEnvelope = IccUtils.bytesToHexString(envelope); + mCI.sendEnvelopeWithStatus(encodedEnvelope, obtainMessage( + EVENT_SEND_ENVELOPE_RESPONSE, new int[]{ dcs, pid })); + } + + /** + * Return the size in bytes of the envelope to send to the UICC, excluding the + * SMS-PP download tag byte and length byte(s). If the size returned is <= 127, + * the BER-TLV length will be encoded in 1 byte, otherwise 2 bytes are required. + * + * @param scAddressLength the length of the SMSC address, or zero if not present + * @param tpduLength the length of the TPDU from the SMS-PP message + * @return the number of bytes to allocate for the envelope command + */ + private static int getEnvelopeBodyLength(int scAddressLength, int tpduLength) { + // Add 4 bytes for device identities TLV + 1 byte for SMS TPDU tag byte + int length = tpduLength + 5; + // Add 1 byte for TPDU length, or 2 bytes if length > 127 + length += (tpduLength > 127 ? 2 : 1); + // Add length of address tag, if present (+ 2 bytes for tag and length) + if (scAddressLength != 0) { + length = length + 2 + scAddressLength; + } + return length; + } + + /** + * Handle the response to the ENVELOPE command. + * @param response UICC response encoded as hexadecimal digits. First two bytes are the + * UICC SW1 and SW2 status bytes. + */ + private void sendSmsAckForEnvelopeResponse(IccIoResult response, int dcs, int pid) { + int sw1 = response.sw1; + int sw2 = response.sw2; + + boolean success; + if ((sw1 == 0x90 && sw2 == 0x00) || sw1 == 0x91) { + Log.d(TAG, "USIM data download succeeded: " + response.toString()); + success = true; + } else if (sw1 == 0x93 && sw2 == 0x00) { + Log.e(TAG, "USIM data download failed: Toolkit busy"); + acknowledgeSmsWithError(CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_APP_TOOLKIT_BUSY); + return; + } else if (sw1 == 0x62 || sw1 == 0x63) { + Log.e(TAG, "USIM data download failed: " + response.toString()); + success = false; + } else { + Log.e(TAG, "Unexpected SW1/SW2 response from UICC: " + response.toString()); + success = false; + } + + byte[] responseBytes = response.payload; + if (responseBytes == null || responseBytes.length == 0) { + if (success) { + mCI.acknowledgeLastIncomingGsmSms(true, 0, null); + } else { + acknowledgeSmsWithError( + CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR); + } + return; + } + + byte[] smsAckPdu; + int index = 0; + if (success) { + smsAckPdu = new byte[responseBytes.length + 5]; + smsAckPdu[index++] = 0x00; // TP-MTI, TP-UDHI + smsAckPdu[index++] = 0x07; // TP-PI: TP-PID, TP-DCS, TP-UDL present + } else { + smsAckPdu = new byte[responseBytes.length + 6]; + smsAckPdu[index++] = 0x00; // TP-MTI, TP-UDHI + smsAckPdu[index++] = (byte) + CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR; // TP-FCS + smsAckPdu[index++] = 0x07; // TP-PI: TP-PID, TP-DCS, TP-UDL present + } + + smsAckPdu[index++] = (byte) pid; + smsAckPdu[index++] = (byte) dcs; + + if (is7bitDcs(dcs)) { + int septetCount = responseBytes.length * 8 / 7; + smsAckPdu[index++] = (byte) septetCount; + } else { + smsAckPdu[index++] = (byte) responseBytes.length; + } + + System.arraycopy(responseBytes, 0, smsAckPdu, index, responseBytes.length); + + mCI.acknowledgeIncomingGsmSmsWithPdu(success, + IccUtils.bytesToHexString(smsAckPdu), null); + } + + private void acknowledgeSmsWithError(int cause) { + mCI.acknowledgeLastIncomingGsmSms(false, cause, null); + } + + /** + * Returns whether the DCS is 7 bit. If so, set TP-UDL to the septet count of TP-UD; + * otherwise, set TP-UDL to the octet count of TP-UD. + * @param dcs the TP-Data-Coding-Scheme field from the original download SMS + * @return true if the DCS specifies 7 bit encoding; false otherwise + */ + private static boolean is7bitDcs(int dcs) { + // See 3GPP TS 23.038 section 4 + return ((dcs & 0x8C) == 0x00) || ((dcs & 0xF4) == 0xF0); + } + + /** + * Handle UICC envelope response and send SMS acknowledgement. + * + * @param msg the message to handle + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_START_DATA_DOWNLOAD: + handleDataDownload((SmsMessage) msg.obj); + break; + + case EVENT_SEND_ENVELOPE_RESPONSE: + AsyncResult ar = (AsyncResult) msg.obj; + + if (ar.exception != null) { + Log.e(TAG, "UICC Send Envelope failure, exception: " + ar.exception); + acknowledgeSmsWithError( + CommandsInterface.GSM_SMS_FAIL_CAUSE_USIM_DATA_DOWNLOAD_ERROR); + return; + } + + int[] dcsPid = (int[]) ar.userObj; + sendSmsAckForEnvelopeResponse((IccIoResult) ar.result, dcsPid[0], dcsPid[1]); + break; + + default: + Log.e(TAG, "Ignoring unexpected message, what=" + msg.what); + } + } +} diff --git a/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java b/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java new file mode 100755 index 0000000..8f5a420 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/UsimPhoneBookManager.java @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2009 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.internal.telephony.gsm; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.AdnRecord; +import com.android.internal.telephony.AdnRecordCache; +import com.android.internal.telephony.IccConstants; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.PhoneBase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +/** + * This class implements reading and parsing USIM records. + * Refer to Spec 3GPP TS 31.102 for more details. + * + * {@hide} + */ +public class UsimPhoneBookManager extends Handler implements IccConstants { + private static final String LOG_TAG = "GSM"; + private static final boolean DBG = true; + private PbrFile mPbrFile; + private Boolean mIsPbrPresent; + private IccFileHandler mFh; + private AdnRecordCache mAdnCache; + private Object mLock = new Object(); + private ArrayList<AdnRecord> mPhoneBookRecords; + private boolean mEmailPresentInIap = false; + private int mEmailTagNumberInIap = 0; + private ArrayList<byte[]> mIapFileRecord; + private ArrayList<byte[]> mEmailFileRecord; + private Map<Integer, ArrayList<String>> mEmailsForAdnRec; + private boolean mRefreshCache = false; + + private static final int EVENT_PBR_LOAD_DONE = 1; + private static final int EVENT_USIM_ADN_LOAD_DONE = 2; + private static final int EVENT_IAP_LOAD_DONE = 3; + private static final int EVENT_EMAIL_LOAD_DONE = 4; + + private static final int USIM_TYPE1_TAG = 0xA8; + private static final int USIM_TYPE2_TAG = 0xA9; + private static final int USIM_TYPE3_TAG = 0xAA; + private static final int USIM_EFADN_TAG = 0xC0; + private static final int USIM_EFIAP_TAG = 0xC1; + private static final int USIM_EFEXT1_TAG = 0xC2; + private static final int USIM_EFSNE_TAG = 0xC3; + private static final int USIM_EFANR_TAG = 0xC4; + private static final int USIM_EFPBC_TAG = 0xC5; + private static final int USIM_EFGRP_TAG = 0xC6; + private static final int USIM_EFAAS_TAG = 0xC7; + private static final int USIM_EFGSD_TAG = 0xC8; + private static final int USIM_EFUID_TAG = 0xC9; + private static final int USIM_EFEMAIL_TAG = 0xCA; + private static final int USIM_EFCCP1_TAG = 0xCB; + + public UsimPhoneBookManager(IccFileHandler fh, AdnRecordCache cache) { + mFh = fh; + mPhoneBookRecords = new ArrayList<AdnRecord>(); + mPbrFile = null; + // We assume its present, after the first read this is updated. + // So we don't have to read from UICC if its not present on subsequent reads. + mIsPbrPresent = true; + mAdnCache = cache; + } + + public void reset() { + mPhoneBookRecords.clear(); + mIapFileRecord = null; + mEmailFileRecord = null; + mPbrFile = null; + mIsPbrPresent = true; + mRefreshCache = false; + } + + public ArrayList<AdnRecord> loadEfFilesFromUsim() { + synchronized (mLock) { + if (!mPhoneBookRecords.isEmpty()) { + if (mRefreshCache) { + mRefreshCache = false; + refreshCache(); + } + return mPhoneBookRecords; + } + + if (!mIsPbrPresent) return null; + + // Check if the PBR file is present in the cache, if not read it + // from the USIM. + if (mPbrFile == null) { + readPbrFileAndWait(); + } + + if (mPbrFile == null) return null; + + int numRecs = mPbrFile.mFileIds.size(); + for (int i = 0; i < numRecs; i++) { + readAdnFileAndWait(i); + readEmailFileAndWait(i); + } + // All EF files are loaded, post the response. + } + return mPhoneBookRecords; + } + + private void refreshCache() { + if (mPbrFile == null) return; + mPhoneBookRecords.clear(); + + int numRecs = mPbrFile.mFileIds.size(); + for (int i = 0; i < numRecs; i++) { + readAdnFileAndWait(i); + } + } + + public void invalidateCache() { + mRefreshCache = true; + } + + private void readPbrFileAndWait() { + mFh.loadEFLinearFixedAll(EF_PBR, obtainMessage(EVENT_PBR_LOAD_DONE)); + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted Exception in readAdnFileAndWait"); + } + } + + private void readEmailFileAndWait(int recNum) { + Map <Integer,Integer> fileIds; + fileIds = mPbrFile.mFileIds.get(recNum); + if (fileIds == null) return; + + if (fileIds.containsKey(USIM_EFEMAIL_TAG)) { + int efid = fileIds.get(USIM_EFEMAIL_TAG); + // Check if the EFEmail is a Type 1 file or a type 2 file. + // If mEmailPresentInIap is true, its a type 2 file. + // So we read the IAP file and then read the email records. + // instead of reading directly. + if (mEmailPresentInIap) { + readIapFileAndWait(fileIds.get(USIM_EFIAP_TAG)); + if (mIapFileRecord == null) { + Log.e(LOG_TAG, "Error: IAP file is empty"); + return; + } + } + // Read the EFEmail file. + mFh.loadEFLinearFixedAll(fileIds.get(USIM_EFEMAIL_TAG), + obtainMessage(EVENT_EMAIL_LOAD_DONE)); + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted Exception in readEmailFileAndWait"); + } + + if (mEmailFileRecord == null) { + Log.e(LOG_TAG, "Error: Email file is empty"); + return; + } + updatePhoneAdnRecord(); + } + + } + + private void readIapFileAndWait(int efid) { + mFh.loadEFLinearFixedAll(efid, obtainMessage(EVENT_IAP_LOAD_DONE)); + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted Exception in readIapFileAndWait"); + } + } + + private void updatePhoneAdnRecord() { + if (mEmailFileRecord == null) return; + int numAdnRecs = mPhoneBookRecords.size(); + if (mIapFileRecord != null) { + // The number of records in the IAP file is same as the number of records in ADN file. + // The order of the pointers in an EFIAP shall be the same as the order of file IDs + // that appear in the TLV object indicated by Tag 'A9' in the reference file record. + // i.e value of mEmailTagNumberInIap + + for (int i = 0; i < numAdnRecs; i++) { + byte[] record = null; + try { + record = mIapFileRecord.get(i); + } catch (IndexOutOfBoundsException e) { + Log.e(LOG_TAG, "Error: Improper ICC card: No IAP record for ADN, continuing"); + break; + } + int recNum = record[mEmailTagNumberInIap]; + + if (recNum != -1) { + String[] emails = new String[1]; + // SIM record numbers are 1 based + emails[0] = readEmailRecord(recNum - 1); + AdnRecord rec = mPhoneBookRecords.get(i); + if (rec != null) { + rec.setEmails(emails); + } else { + // might be a record with only email + rec = new AdnRecord("", "", emails); + } + mPhoneBookRecords.set(i, rec); + } + } + } + + // ICC cards can be made such that they have an IAP file but all + // records are empty. So we read both type 1 and type 2 file + // email records, just to be sure. + + int len = mPhoneBookRecords.size(); + // Type 1 file, the number of records is the same as the number of + // records in the ADN file. + if (mEmailsForAdnRec == null) { + parseType1EmailFile(len); + } + for (int i = 0; i < numAdnRecs; i++) { + ArrayList<String> emailList = null; + try { + emailList = mEmailsForAdnRec.get(i); + } catch (IndexOutOfBoundsException e) { + break; + } + if (emailList == null) continue; + + AdnRecord rec = mPhoneBookRecords.get(i); + + String[] emails = new String[emailList.size()]; + System.arraycopy(emailList.toArray(), 0, emails, 0, emailList.size()); + rec.setEmails(emails); + mPhoneBookRecords.set(i, rec); + } + } + + void parseType1EmailFile(int numRecs) { + mEmailsForAdnRec = new HashMap<Integer, ArrayList<String>>(); + byte[] emailRec = null; + for (int i = 0; i < numRecs; i++) { + try { + emailRec = mEmailFileRecord.get(i); + } catch (IndexOutOfBoundsException e) { + Log.e(LOG_TAG, "Error: Improper ICC card: No email record for ADN, continuing"); + break; + } + int adnRecNum = emailRec[emailRec.length - 1]; + + if (adnRecNum == -1) { + continue; + } + + String email = readEmailRecord(i); + + if (email == null || email.equals("")) { + continue; + } + + // SIM record numbers are 1 based. + ArrayList<String> val = mEmailsForAdnRec.get(adnRecNum - 1); + if (val == null) { + val = new ArrayList<String>(); + } + val.add(email); + // SIM record numbers are 1 based. + mEmailsForAdnRec.put(adnRecNum - 1, val); + } + } + + private String readEmailRecord(int recNum) { + byte[] emailRec = null; + try { + emailRec = mEmailFileRecord.get(recNum); + } catch (IndexOutOfBoundsException e) { + return null; + } + + // The length of the record is X+2 byte, where X bytes is the email address + String email = IccUtils.adnStringFieldToString(emailRec, 0, emailRec.length - 2); + return email; + } + + private void readAdnFileAndWait(int recNum) { + Map <Integer,Integer> fileIds; + fileIds = mPbrFile.mFileIds.get(recNum); + if (fileIds == null || fileIds.isEmpty()) return; + + + int extEf = 0; + // Only call fileIds.get while EFEXT1_TAG is available + if (fileIds.containsKey(USIM_EFEXT1_TAG)) { + extEf = fileIds.get(USIM_EFEXT1_TAG); + } + + mAdnCache.requestLoadAllAdnLike(fileIds.get(USIM_EFADN_TAG), + extEf, obtainMessage(EVENT_USIM_ADN_LOAD_DONE)); + try { + mLock.wait(); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "Interrupted Exception in readAdnFileAndWait"); + } + } + + private void createPbrFile(ArrayList<byte[]> records) { + if (records == null) { + mPbrFile = null; + mIsPbrPresent = false; + return; + } + mPbrFile = new PbrFile(records); + } + + @Override + public void handleMessage(Message msg) { + AsyncResult ar; + + switch(msg.what) { + case EVENT_PBR_LOAD_DONE: + ar = (AsyncResult) msg.obj; + if (ar.exception == null) { + createPbrFile((ArrayList<byte[]>)ar.result); + } + synchronized (mLock) { + mLock.notify(); + } + break; + case EVENT_USIM_ADN_LOAD_DONE: + log("Loading USIM ADN records done"); + ar = (AsyncResult) msg.obj; + if (ar.exception == null) { + mPhoneBookRecords.addAll((ArrayList<AdnRecord>)ar.result); + } + synchronized (mLock) { + mLock.notify(); + } + break; + case EVENT_IAP_LOAD_DONE: + log("Loading USIM IAP records done"); + ar = (AsyncResult) msg.obj; + if (ar.exception == null) { + mIapFileRecord = ((ArrayList<byte[]>)ar.result); + } + synchronized (mLock) { + mLock.notify(); + } + break; + case EVENT_EMAIL_LOAD_DONE: + log("Loading USIM Email records done"); + ar = (AsyncResult) msg.obj; + if (ar.exception == null) { + mEmailFileRecord = ((ArrayList<byte[]>)ar.result); + } + + synchronized (mLock) { + mLock.notify(); + } + break; + } + } + + private class PbrFile { + // RecNum <EF Tag, efid> + HashMap<Integer,Map<Integer,Integer>> mFileIds; + + PbrFile(ArrayList<byte[]> records) { + mFileIds = new HashMap<Integer, Map<Integer, Integer>>(); + SimTlv recTlv; + int recNum = 0; + for (byte[] record: records) { + recTlv = new SimTlv(record, 0, record.length); + parseTag(recTlv, recNum); + recNum ++; + } + } + + void parseTag(SimTlv tlv, int recNum) { + SimTlv tlvEf; + int tag; + byte[] data; + Map<Integer, Integer> val = new HashMap<Integer, Integer>(); + do { + tag = tlv.getTag(); + switch(tag) { + case USIM_TYPE1_TAG: // A8 + case USIM_TYPE3_TAG: // AA + case USIM_TYPE2_TAG: // A9 + data = tlv.getData(); + tlvEf = new SimTlv(data, 0, data.length); + parseEf(tlvEf, val, tag); + break; + } + } while (tlv.nextObject()); + mFileIds.put(recNum, val); + } + + void parseEf(SimTlv tlv, Map<Integer, Integer> val, int parentTag) { + int tag; + byte[] data; + int tagNumberWithinParentTag = 0; + do { + tag = tlv.getTag(); + if (parentTag == USIM_TYPE2_TAG && tag == USIM_EFEMAIL_TAG) { + mEmailPresentInIap = true; + mEmailTagNumberInIap = tagNumberWithinParentTag; + } + switch(tag) { + case USIM_EFEMAIL_TAG: + case USIM_EFADN_TAG: + case USIM_EFEXT1_TAG: + case USIM_EFANR_TAG: + case USIM_EFPBC_TAG: + case USIM_EFGRP_TAG: + case USIM_EFAAS_TAG: + case USIM_EFGSD_TAG: + case USIM_EFUID_TAG: + case USIM_EFCCP1_TAG: + case USIM_EFIAP_TAG: + case USIM_EFSNE_TAG: + data = tlv.getData(); + int efid = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); + val.put(tag, efid); + break; + } + tagNumberWithinParentTag ++; + } while(tlv.nextObject()); + } + } + + private void log(String msg) { + if(DBG) Log.d(LOG_TAG, msg); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/UsimServiceTable.java b/src/java/com/android/internal/telephony/gsm/UsimServiceTable.java new file mode 100644 index 0000000..3fe200b --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/UsimServiceTable.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2011 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.internal.telephony.gsm; + +import com.android.internal.telephony.IccServiceTable; + +/** + * Wrapper class for the USIM Service Table EF. + * See 3GPP TS 31.102 Release 10 section 4.2.8 + */ +public final class UsimServiceTable extends IccServiceTable { + public enum UsimService { + PHONEBOOK, + FDN, // Fixed Dialing Numbers + FDN_EXTENSION, // FDN extension data in EF_EXT2 + SDN, // Service Dialing Numbers + SDN_EXTENSION, // SDN extension data in EF_EXT3 + BDN, // Barred Dialing Numbers + BDN_EXTENSION, // BDN extension data in EF_EXT4 + OUTGOING_CALL_INFO, + INCOMING_CALL_INFO, + SM_STORAGE, + SM_STATUS_REPORTS, + SM_SERVICE_PARAMS, + ADVICE_OF_CHARGE, + CAP_CONFIG_PARAMS_2, + CB_MESSAGE_ID, + CB_MESSAGE_ID_RANGES, + GROUP_ID_LEVEL_1, + GROUP_ID_LEVEL_2, + SPN, // Service Provider Name + USER_PLMN_SELECT, + MSISDN, + IMAGE, + LOCALISED_SERVICE_AREAS, + EMLPP, // Enhanced Multi-Level Precedence and Preemption + EMLPP_AUTO_ANSWER, + RFU, + GSM_ACCESS, + DATA_DL_VIA_SMS_PP, + DATA_DL_VIA_SMS_CB, + CALL_CONTROL_BY_USIM, + MO_SMS_CONTROL_BY_USIM, + RUN_AT_COMMAND, + IGNORED_1, + ENABLED_SERVICES_TABLE, + APN_CONTROL_LIST, + DEPERSONALISATION_CONTROL_KEYS, + COOPERATIVE_NETWORK_LIST, + GSM_SECURITY_CONTEXT, + CPBCCH_INFO, + INVESTIGATION_SCAN, + MEXE, + OPERATOR_PLMN_SELECT, + HPLMN_SELECT, + EXTENSION_5, // Extension data for ICI, OCI, MSISDN in EF_EXT5 + PLMN_NETWORK_NAME, + OPERATOR_PLMN_LIST, + MBDN, // Mailbox Dialing Numbers + MWI_STATUS, // Message Waiting Indication status + CFI_STATUS, // Call Forwarding Indication status + IGNORED_2, + SERVICE_PROVIDER_DISPLAY_INFO, + MMS_NOTIFICATION, + MMS_NOTIFICATION_EXTENSION, // MMS Notification extension data in EF_EXT8 + GPRS_CALL_CONTROL_BY_USIM, + MMS_CONNECTIVITY_PARAMS, + NETWORK_INDICATION_OF_ALERTING, + VGCS_GROUP_ID_LIST, + VBS_GROUP_ID_LIST, + PSEUDONYM, + IWLAN_USER_PLMN_SELECT, + IWLAN_OPERATOR_PLMN_SELECT, + USER_WSID_LIST, + OPERATOR_WSID_LIST, + VGCS_SECURITY, + VBS_SECURITY, + WLAN_REAUTH_IDENTITY, + MM_STORAGE, + GBA, // Generic Bootstrapping Architecture + MBMS_SECURITY, + DATA_DL_VIA_USSD, + EQUIVALENT_HPLMN, + TERMINAL_PROFILE_AFTER_UICC_ACTIVATION, + EQUIVALENT_HPLMN_PRESENTATION, + LAST_RPLMN_SELECTION_INDICATION, + OMA_BCAST_PROFILE, + GBA_LOCAL_KEY_ESTABLISHMENT, + TERMINAL_APPLICATIONS, + SPN_ICON, + PLMN_NETWORK_NAME_ICON, + USIM_IP_CONNECTION_PARAMS, + IWLAN_HOME_ID_LIST, + IWLAN_EQUIVALENT_HPLMN_PRESENTATION, + IWLAN_HPLMN_PRIORITY_INDICATION, + IWLAN_LAST_REGISTERED_PLMN, + EPS_MOBILITY_MANAGEMENT_INFO, + ALLOWED_CSG_LISTS_AND_INDICATIONS, + CALL_CONTROL_ON_EPS_PDN_CONNECTION_BY_USIM, + HPLMN_DIRECT_ACCESS, + ECALL_DATA, + OPERATOR_CSG_LISTS_AND_INDICATIONS, + SM_OVER_IP, + CSG_DISPLAY_CONTROL, + IMS_COMMUNICATION_CONTROL_BY_USIM, + EXTENDED_TERMINAL_APPLICATIONS, + UICC_ACCESS_TO_IMS, + NAS_CONFIG_BY_USIM + } + + public UsimServiceTable(byte[] table) { + super(table); + } + + public boolean isAvailable(UsimService service) { + return super.isAvailable(service.ordinal()); + } + + @Override + protected String getTag() { + return "UsimServiceTable"; + } + + @Override + protected Object[] getValues() { + return UsimService.values(); + } +} diff --git a/src/java/com/android/internal/telephony/gsm/VoiceMailConstants.java b/src/java/com/android/internal/telephony/gsm/VoiceMailConstants.java new file mode 100644 index 0000000..0e49e35 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/VoiceMailConstants.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2006 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.internal.telephony.gsm; + +import android.os.Environment; +import android.util.Xml; +import android.util.Log; + +import java.util.HashMap; +import java.io.FileReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.android.internal.util.XmlUtils; + +/** + * {@hide} + */ +class VoiceMailConstants { + private HashMap<String, String[]> CarrierVmMap; + + + static final String LOG_TAG = "GSM"; + static final String PARTNER_VOICEMAIL_PATH ="etc/voicemail-conf.xml"; + + static final int NAME = 0; + static final int NUMBER = 1; + static final int TAG = 2; + static final int SIZE = 3; + + VoiceMailConstants () { + CarrierVmMap = new HashMap<String, String[]>(); + loadVoiceMail(); + } + + boolean containsCarrier(String carrier) { + return CarrierVmMap.containsKey(carrier); + } + + String getCarrierName(String carrier) { + String[] data = CarrierVmMap.get(carrier); + return data[NAME]; + } + + String getVoiceMailNumber(String carrier) { + String[] data = CarrierVmMap.get(carrier); + return data[NUMBER]; + } + + String getVoiceMailTag(String carrier) { + String[] data = CarrierVmMap.get(carrier); + return data[TAG]; + } + + private void loadVoiceMail() { + FileReader vmReader; + + final File vmFile = new File(Environment.getRootDirectory(), + PARTNER_VOICEMAIL_PATH); + + try { + vmReader = new FileReader(vmFile); + } catch (FileNotFoundException e) { + Log.w(LOG_TAG, "Can't open " + + Environment.getRootDirectory() + "/" + PARTNER_VOICEMAIL_PATH); + return; + } + + try { + XmlPullParser parser = Xml.newPullParser(); + parser.setInput(vmReader); + + XmlUtils.beginDocument(parser, "voicemail"); + + while (true) { + XmlUtils.nextElement(parser); + + String name = parser.getName(); + if (!"voicemail".equals(name)) { + break; + } + + String[] data = new String[SIZE]; + String numeric = parser.getAttributeValue(null, "numeric"); + data[NAME] = parser.getAttributeValue(null, "carrier"); + data[NUMBER] = parser.getAttributeValue(null, "vmnumber"); + data[TAG] = parser.getAttributeValue(null, "vmtag"); + + CarrierVmMap.put(numeric, data); + } + } catch (XmlPullParserException e) { + Log.w(LOG_TAG, "Exception in Voicemail parser " + e); + } catch (IOException e) { + Log.w(LOG_TAG, "Exception in Voicemail parser " + e); + } + } +} diff --git a/src/java/com/android/internal/telephony/gsm/package.html b/src/java/com/android/internal/telephony/gsm/package.html new file mode 100755 index 0000000..fed8399 --- /dev/null +++ b/src/java/com/android/internal/telephony/gsm/package.html @@ -0,0 +1,6 @@ +<HTML> +<BODY> +Provides classes to control or read data from GSM phones. +@hide +</BODY> +</HTML> diff --git a/src/java/com/android/internal/telephony/ims/IsimRecords.java b/src/java/com/android/internal/telephony/ims/IsimRecords.java new file mode 100644 index 0000000..b8b98b9 --- /dev/null +++ b/src/java/com/android/internal/telephony/ims/IsimRecords.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 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.internal.telephony.ims; + +/** + * {@hide} + */ +public interface IsimRecords { + + /** + * Return the IMS private user identity (IMPI). + * Returns null if the IMPI hasn't been loaded or isn't present on the ISIM. + * @return the IMS private user identity string, or null if not available + */ + String getIsimImpi(); + + /** + * Return the IMS home network domain name. + * Returns null if the IMS domain hasn't been loaded or isn't present on the ISIM. + * @return the IMS home network domain name, or null if not available + */ + String getIsimDomain(); + + /** + * Return an array of IMS public user identities (IMPU). + * Returns null if the IMPU hasn't been loaded or isn't present on the ISIM. + * @return an array of IMS public user identity strings, or null if not available + */ + String[] getIsimImpu(); +} diff --git a/src/java/com/android/internal/telephony/ims/IsimUiccRecords.java b/src/java/com/android/internal/telephony/ims/IsimUiccRecords.java new file mode 100644 index 0000000..ee1a42d --- /dev/null +++ b/src/java/com/android/internal/telephony/ims/IsimUiccRecords.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 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.internal.telephony.ims; + +import android.os.AsyncResult; +import android.os.Handler; +import android.util.Log; + +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccRecords; +import com.android.internal.telephony.gsm.SimTlv; + +import java.nio.charset.Charset; +import java.util.ArrayList; + +import static com.android.internal.telephony.IccConstants.EF_DOMAIN; +import static com.android.internal.telephony.IccConstants.EF_IMPI; +import static com.android.internal.telephony.IccConstants.EF_IMPU; + +/** + * {@hide} + */ +public final class IsimUiccRecords implements IsimRecords { + protected static final String LOG_TAG = "GSM"; + + private static final boolean DBG = true; + private static final boolean DUMP_RECORDS = false; // Note: PII is logged when this is true + + // ISIM EF records (see 3GPP TS 31.103) + private String mIsimImpi; // IMS private user identity + private String mIsimDomain; // IMS home network domain name + private String[] mIsimImpu; // IMS public user identity(s) + + private static final int TAG_ISIM_VALUE = 0x80; // From 3GPP TS 31.103 + + private class EfIsimImpiLoaded implements IccRecords.IccRecordLoaded { + public String getEfName() { + return "EF_ISIM_IMPI"; + } + public void onRecordLoaded(AsyncResult ar) { + byte[] data = (byte[]) ar.result; + mIsimImpi = isimTlvToString(data); + if (DUMP_RECORDS) log("EF_IMPI=" + mIsimImpi); + } + } + + private class EfIsimImpuLoaded implements IccRecords.IccRecordLoaded { + public String getEfName() { + return "EF_ISIM_IMPU"; + } + public void onRecordLoaded(AsyncResult ar) { + ArrayList<byte[]> impuList = (ArrayList<byte[]>) ar.result; + if (DBG) log("EF_IMPU record count: " + impuList.size()); + mIsimImpu = new String[impuList.size()]; + int i = 0; + for (byte[] identity : impuList) { + String impu = isimTlvToString(identity); + if (DUMP_RECORDS) log("EF_IMPU[" + i + "]=" + impu); + mIsimImpu[i++] = impu; + } + } + } + + private class EfIsimDomainLoaded implements IccRecords.IccRecordLoaded { + public String getEfName() { + return "EF_ISIM_DOMAIN"; + } + public void onRecordLoaded(AsyncResult ar) { + byte[] data = (byte[]) ar.result; + mIsimDomain = isimTlvToString(data); + if (DUMP_RECORDS) log("EF_DOMAIN=" + mIsimDomain); + } + } + + /** + * Request the ISIM records to load. + * @param iccFh the IccFileHandler to load the records from + * @param h the Handler to which the response message will be sent + * @return the number of EF record requests that were added + */ + public int fetchIsimRecords(IccFileHandler iccFh, Handler h) { + iccFh.loadEFTransparent(EF_IMPI, h.obtainMessage( + IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpiLoaded())); + iccFh.loadEFLinearFixedAll(EF_IMPU, h.obtainMessage( + IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimImpuLoaded())); + iccFh.loadEFTransparent(EF_DOMAIN, h.obtainMessage( + IccRecords.EVENT_GET_ICC_RECORD_DONE, new EfIsimDomainLoaded())); + return 3; // number of EF record load requests + } + + /** + * ISIM records for IMS are stored inside a Tag-Length-Value record as a UTF-8 string + * with tag value 0x80. + * @param record the byte array containing the IMS data string + * @return the decoded String value, or null if the record can't be decoded + */ + private static String isimTlvToString(byte[] record) { + SimTlv tlv = new SimTlv(record, 0, record.length); + do { + if (tlv.getTag() == TAG_ISIM_VALUE) { + return new String(tlv.getData(), Charset.forName("UTF-8")); + } + } while (tlv.nextObject()); + + Log.e(LOG_TAG, "[ISIM] can't find TLV tag in ISIM record, returning null"); + return null; + } + + void log(String s) { + if (DBG) Log.d(LOG_TAG, "[ISIM] " + s); + } + + void loge(String s) { + if (DBG) Log.e(LOG_TAG, "[ISIM] " + s); + } + + /** + * Return the IMS private user identity (IMPI). + * Returns null if the IMPI hasn't been loaded or isn't present on the ISIM. + * @return the IMS private user identity string, or null if not available + */ + public String getIsimImpi() { + return mIsimImpi; + } + + /** + * Return the IMS home network domain name. + * Returns null if the IMS domain hasn't been loaded or isn't present on the ISIM. + * @return the IMS home network domain name, or null if not available + */ + public String getIsimDomain() { + return mIsimDomain; + } + + /** + * Return an array of IMS public user identities (IMPU). + * Returns null if the IMPU hasn't been loaded or isn't present on the ISIM. + * @return an array of IMS public user identity strings, or null if not available + */ + public String[] getIsimImpu() { + return (mIsimImpu != null) ? mIsimImpu.clone() : null; + } +} diff --git a/src/java/com/android/internal/telephony/package.html b/src/java/com/android/internal/telephony/package.html new file mode 100644 index 0000000..6377c18 --- /dev/null +++ b/src/java/com/android/internal/telephony/package.html @@ -0,0 +1,5 @@ +<html> +<body> +{@hide} +</body> +</html> diff --git a/src/java/com/android/internal/telephony/sip/SipCallBase.java b/src/java/com/android/internal/telephony/sip/SipCallBase.java new file mode 100644 index 0000000..9050a43 --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipCallBase.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.Phone; + +import android.net.sip.SipManager; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +abstract class SipCallBase extends Call { + protected List<Connection> connections = new ArrayList<Connection>(); + + protected abstract void setState(State newState); + + public List<Connection> getConnections() { + // FIXME should return Collections.unmodifiableList(); + return connections; + } + + public boolean isMultiparty() { + return connections.size() > 1; + } + + public String toString() { + return state.toString() + ":" + super.toString(); + } + + void clearDisconnected() { + for (Iterator<Connection> it = connections.iterator(); it.hasNext(); ) { + Connection c = it.next(); + if (c.getState() == State.DISCONNECTED) it.remove(); + } + + if (connections.isEmpty()) setState(State.IDLE); + } +} diff --git a/src/java/com/android/internal/telephony/sip/SipCommandInterface.java b/src/java/com/android/internal/telephony/sip/SipCommandInterface.java new file mode 100644 index 0000000..99f4e0f --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipCommandInterface.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.telephony.BaseCommands; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.UUSInfo; +import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; + +/** + * SIP doesn't need CommandsInterface. The class does nothing but made to work + * with PhoneBase's constructor. + */ +class SipCommandInterface extends BaseCommands implements CommandsInterface { + SipCommandInterface(Context context) { + super(context); + } + + @Override public void setOnNITZTime(Handler h, int what, Object obj) { + } + + public void getIccCardStatus(Message result) { + } + + public void supplyIccPin(String pin, Message result) { + } + + public void supplyIccPuk(String puk, String newPin, Message result) { + } + + public void supplyIccPin2(String pin, Message result) { + } + + public void supplyIccPuk2(String puk, String newPin2, Message result) { + } + + public void changeIccPin(String oldPin, String newPin, Message result) { + } + + public void changeIccPin2(String oldPin2, String newPin2, Message result) { + } + + public void changeBarringPassword(String facility, String oldPwd, + String newPwd, Message result) { + } + + public void supplyNetworkDepersonalization(String netpin, Message result) { + } + + public void getCurrentCalls(Message result) { + } + + @Deprecated public void getPDPContextList(Message result) { + } + + public void getDataCallList(Message result) { + } + + public void dial(String address, int clirMode, Message result) { + } + + public void dial(String address, int clirMode, UUSInfo uusInfo, + Message result) { + } + + public void getIMSI(Message result) { + } + + public void getIMSIForApp(String aid, Message result) { + } + + public void getIMEI(Message result) { + } + + public void getIMEISV(Message result) { + } + + + public void hangupConnection (int gsmIndex, Message result) { + } + + public void hangupWaitingOrBackground (Message result) { + } + + public void hangupForegroundResumeBackground (Message result) { + } + + public void switchWaitingOrHoldingAndActive (Message result) { + } + + public void conference (Message result) { + } + + + public void setPreferredVoicePrivacy(boolean enable, Message result) { + } + + public void getPreferredVoicePrivacy(Message result) { + } + + public void separateConnection (int gsmIndex, Message result) { + } + + public void acceptCall (Message result) { + } + + public void rejectCall (Message result) { + } + + public void explicitCallTransfer (Message result) { + } + + public void getLastCallFailCause (Message result) { + } + + /** @deprecated */ + public void getLastPdpFailCause (Message result) { + } + + public void getLastDataCallFailCause (Message result) { + } + + public void setMute (boolean enableMute, Message response) { + } + + public void getMute (Message response) { + } + + public void getSignalStrength (Message result) { + } + + public void getVoiceRegistrationState (Message result) { + } + + public void getDataRegistrationState (Message result) { + } + + public void getOperator(Message result) { + } + + public void sendDtmf(char c, Message result) { + } + + public void startDtmf(char c, Message result) { + } + + public void stopDtmf(Message result) { + } + + public void sendBurstDtmf(String dtmfString, int on, int off, + Message result) { + } + + public void sendSMS (String smscPDU, String pdu, Message result) { + } + + public void sendCdmaSms(byte[] pdu, Message result) { + } + + public void deleteSmsOnSim(int index, Message response) { + } + + public void deleteSmsOnRuim(int index, Message response) { + } + + public void writeSmsToSim(int status, String smsc, String pdu, Message response) { + } + + public void writeSmsToRuim(int status, String pdu, Message response) { + } + + public void setupDataCall(String radioTechnology, String profile, + String apn, String user, String password, String authType, + String protocol, Message result) { + } + + public void deactivateDataCall(int cid, int reason, Message result) { + } + + public void setRadioPower(boolean on, Message result) { + } + + public void setSuppServiceNotifications(boolean enable, Message result) { + } + + public void acknowledgeLastIncomingGsmSms(boolean success, int cause, + Message result) { + } + + public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, + Message result) { + } + + public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, + Message result) { + } + + public void iccIO (int command, int fileid, String path, int p1, int p2, + int p3, String data, String pin2, Message result) { + } + public void iccIOForApp (int command, int fileid, String path, int p1, int p2, + int p3, String data, String pin2, String aid, Message result) { + } + + public void getCLIR(Message result) { + } + + public void setCLIR(int clirMode, Message result) { + } + + public void queryCallWaiting(int serviceClass, Message response) { + } + + public void setCallWaiting(boolean enable, int serviceClass, + Message response) { + } + + public void setNetworkSelectionModeAutomatic(Message response) { + } + + public void setNetworkSelectionModeManual( + String operatorNumeric, Message response) { + } + + public void getNetworkSelectionMode(Message response) { + } + + public void getAvailableNetworks(Message response) { + } + + public void setCallForward(int action, int cfReason, int serviceClass, + String number, int timeSeconds, Message response) { + } + + public void queryCallForwardStatus(int cfReason, int serviceClass, + String number, Message response) { + } + + public void queryCLIP(Message response) { + } + + public void getBasebandVersion (Message response) { + } + + @Override + public void queryFacilityLock(String facility, String password, + int serviceClass, Message response) { + } + + @Override + public void queryFacilityLockForApp(String facility, String password, + int serviceClass, String appId, Message response) { + } + + @Override + public void setFacilityLock(String facility, boolean lockState, + String password, int serviceClass, Message response) { + } + + @Override + public void setFacilityLockForApp(String facility, boolean lockState, + String password, int serviceClass, String appId, Message response) { + } + + public void sendUSSD (String ussdString, Message response) { + } + + public void cancelPendingUssd (Message response) { + } + + public void resetRadio(Message result) { + } + + public void invokeOemRilRequestRaw(byte[] data, Message response) { + } + + public void invokeOemRilRequestStrings(String[] strings, Message response) { + } + + public void setBandMode (int bandMode, Message response) { + } + + public void queryAvailableBandMode (Message response) { + } + + public void sendTerminalResponse(String contents, Message response) { + } + + public void sendEnvelope(String contents, Message response) { + } + + public void sendEnvelopeWithStatus(String contents, Message response) { + } + + public void handleCallSetupRequestFromSim( + boolean accept, Message response) { + } + + public void setPreferredNetworkType(int networkType , Message response) { + } + + public void getPreferredNetworkType(Message response) { + } + + public void getNeighboringCids(Message response) { + } + + public void setLocationUpdates(boolean enable, Message response) { + } + + public void getSmscAddress(Message result) { + } + + public void setSmscAddress(String address, Message result) { + } + + public void reportSmsMemoryStatus(boolean available, Message result) { + } + + public void reportStkServiceIsRunning(Message result) { + } + + @Override + public void getCdmaSubscriptionSource(Message response) { + } + + public void getGsmBroadcastConfig(Message response) { + } + + public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) { + } + + public void setGsmBroadcastActivation(boolean activate, Message response) { + } + + // ***** Methods for CDMA support + public void getDeviceIdentity(Message response) { + } + + public void getCDMASubscription(Message response) { + } + + public void setPhoneType(int phoneType) { //Set by CDMAPhone and GSMPhone constructor + } + + public void queryCdmaRoamingPreference(Message response) { + } + + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + } + + public void setCdmaSubscriptionSource(int cdmaSubscription , Message response) { + } + + public void queryTTYMode(Message response) { + } + + public void setTTYMode(int ttyMode, Message response) { + } + + public void sendCDMAFeatureCode(String FeatureCode, Message response) { + } + + public void getCdmaBroadcastConfig(Message response) { + } + + public void setCdmaBroadcastConfig(int[] configValuesArray, Message response) { + } + + public void setCdmaBroadcastActivation(boolean activate, Message response) { + } + + public void exitEmergencyCallbackMode(Message response) { + } + + @Override + public void supplyIccPinForApp(String pin, String aid, Message response) { + } + + @Override + public void supplyIccPukForApp(String puk, String newPin, String aid, Message response) { + } + + @Override + public void supplyIccPin2ForApp(String pin2, String aid, Message response) { + } + + @Override + public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message response) { + } + + @Override + public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message response) { + } + + @Override + public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, + Message response) { + } + + public void requestIsimAuthentication(String nonce, Message response) { + } + + public void getVoiceRadioTechnology(Message result) { + } +} diff --git a/src/java/com/android/internal/telephony/sip/SipConnectionBase.java b/src/java/com/android/internal/telephony/sip/SipConnectionBase.java new file mode 100644 index 0000000..eaba2c4 --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipConnectionBase.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.UUSInfo; + +import android.net.sip.SipAudioCall; +import android.os.SystemClock; +import android.util.Log; +import android.telephony.PhoneNumberUtils; + +abstract class SipConnectionBase extends Connection { + private static final String LOG_TAG = "SIP_CONN"; + + private SipAudioCall mSipAudioCall; + + private String dialString; // outgoing calls only + private String postDialString; // outgoing calls only + private int nextPostDialChar; // index into postDialString + private boolean isIncoming; + + /* + * These time/timespan values are based on System.currentTimeMillis(), + * i.e., "wall clock" time. + */ + private long createTime; + private long connectTime; + private long disconnectTime; + + /* + * These time/timespan values are based on SystemClock.elapsedRealTime(), + * i.e., time since boot. They are appropriate for comparison and + * calculating deltas. + */ + private long connectTimeReal; + private long duration = -1L; + private long holdingStartTime; // The time when the Connection last transitioned + // into HOLDING + + private DisconnectCause mCause = DisconnectCause.NOT_DISCONNECTED; + private PostDialState postDialState = PostDialState.NOT_STARTED; + + SipConnectionBase(String dialString) { + this.dialString = dialString; + + postDialString = PhoneNumberUtils.extractPostDialPortion(dialString); + + isIncoming = false; + createTime = System.currentTimeMillis(); + } + + protected void setState(Call.State state) { + switch (state) { + case ACTIVE: + if (connectTime == 0) { + connectTimeReal = SystemClock.elapsedRealtime(); + connectTime = System.currentTimeMillis(); + } + break; + case DISCONNECTED: + duration = getDurationMillis(); + disconnectTime = System.currentTimeMillis(); + break; + case HOLDING: + holdingStartTime = SystemClock.elapsedRealtime(); + break; + } + } + + @Override + public long getCreateTime() { + return createTime; + } + + @Override + public long getConnectTime() { + return connectTime; + } + + @Override + public long getDisconnectTime() { + return disconnectTime; + } + + @Override + public long getDurationMillis() { + if (connectTimeReal == 0) { + return 0; + } else if (duration < 0) { + return SystemClock.elapsedRealtime() - connectTimeReal; + } else { + return duration; + } + } + + @Override + public long getHoldDurationMillis() { + if (getState() != Call.State.HOLDING) { + // If not holding, return 0 + return 0; + } else { + return SystemClock.elapsedRealtime() - holdingStartTime; + } + } + + @Override + public DisconnectCause getDisconnectCause() { + return mCause; + } + + void setDisconnectCause(DisconnectCause cause) { + mCause = cause; + } + + @Override + public PostDialState getPostDialState() { + return postDialState; + } + + @Override + public void proceedAfterWaitChar() { + // TODO + } + + @Override + public void proceedAfterWildChar(String str) { + // TODO + } + + @Override + public void cancelPostDial() { + // TODO + } + + protected abstract Phone getPhone(); + + @Override + public String getRemainingPostDialString() { + if (postDialState == PostDialState.CANCELLED + || postDialState == PostDialState.COMPLETE + || postDialString == null + || postDialString.length() <= nextPostDialChar) { + return ""; + } + + return postDialString.substring(nextPostDialChar); + } + + private void log(String msg) { + Log.d(LOG_TAG, "[SipConn] " + msg); + } + + @Override + public int getNumberPresentation() { + // TODO: add PRESENTATION_URL + return PhoneConstants.PRESENTATION_ALLOWED; + } + + @Override + public UUSInfo getUUSInfo() { + // FIXME: what's this for SIP? + return null; + } +} diff --git a/src/java/com/android/internal/telephony/sip/SipPhone.java b/src/java/com/android/internal/telephony/sip/SipPhone.java new file mode 100644 index 0000000..346b126 --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipPhone.java @@ -0,0 +1,939 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import android.content.Context; +import android.media.AudioManager; +import android.net.rtp.AudioGroup; +import android.net.sip.SipAudioCall; +import android.net.sip.SipErrorCode; +import android.net.sip.SipException; +import android.net.sip.SipManager; +import android.net.sip.SipProfile; +import android.net.sip.SipSession; +import android.os.AsyncResult; +import android.os.Message; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneNotifier; + +import java.text.ParseException; +import java.util.List; +import java.util.regex.Pattern; + +/** + * {@hide} + */ +public class SipPhone extends SipPhoneBase { + private static final String LOG_TAG = "SipPhone"; + private static final boolean DEBUG = true; + private static final int TIMEOUT_MAKE_CALL = 15; // in seconds + private static final int TIMEOUT_ANSWER_CALL = 8; // in seconds + private static final int TIMEOUT_HOLD_CALL = 15; // in seconds + + // A call that is ringing or (call) waiting + private SipCall ringingCall = new SipCall(); + private SipCall foregroundCall = new SipCall(); + private SipCall backgroundCall = new SipCall(); + + private SipManager mSipManager; + private SipProfile mProfile; + + SipPhone (Context context, PhoneNotifier notifier, SipProfile profile) { + super(context, notifier); + + if (DEBUG) Log.d(LOG_TAG, "new SipPhone: " + profile.getUriString()); + ringingCall = new SipCall(); + foregroundCall = new SipCall(); + backgroundCall = new SipCall(); + mProfile = profile; + mSipManager = SipManager.newInstance(context); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof SipPhone)) return false; + SipPhone that = (SipPhone) o; + return mProfile.getUriString().equals(that.mProfile.getUriString()); + } + + public String getPhoneName() { + return "SIP:" + getUriString(mProfile); + } + + public String getSipUri() { + return mProfile.getUriString(); + } + + public boolean equals(SipPhone phone) { + return getSipUri().equals(phone.getSipUri()); + } + + public boolean canTake(Object incomingCall) { + synchronized (SipPhone.class) { + if (!(incomingCall instanceof SipAudioCall)) return false; + if (ringingCall.getState().isAlive()) return false; + + // FIXME: is it true that we cannot take any incoming call if + // both foreground and background are active + if (foregroundCall.getState().isAlive() + && backgroundCall.getState().isAlive()) { + return false; + } + + try { + SipAudioCall sipAudioCall = (SipAudioCall) incomingCall; + if (DEBUG) Log.d(LOG_TAG, "+++ taking call from: " + + sipAudioCall.getPeerProfile().getUriString()); + String localUri = sipAudioCall.getLocalProfile().getUriString(); + if (localUri.equals(mProfile.getUriString())) { + boolean makeCallWait = foregroundCall.getState().isAlive(); + ringingCall.initIncomingCall(sipAudioCall, makeCallWait); + if (sipAudioCall.getState() + != SipSession.State.INCOMING_CALL) { + // Peer cancelled the call! + if (DEBUG) Log.d(LOG_TAG, " call cancelled !!"); + ringingCall.reset(); + } + return true; + } + } catch (Exception e) { + // Peer may cancel the call at any time during the time we hook + // up ringingCall with sipAudioCall. Clean up ringingCall when + // that happens. + ringingCall.reset(); + } + return false; + } + } + + public void acceptCall() throws CallStateException { + synchronized (SipPhone.class) { + if ((ringingCall.getState() == Call.State.INCOMING) || + (ringingCall.getState() == Call.State.WAITING)) { + if (DEBUG) Log.d(LOG_TAG, "acceptCall"); + // Always unmute when answering a new call + ringingCall.setMute(false); + ringingCall.acceptCall(); + } else { + throw new CallStateException("phone not ringing"); + } + } + } + + public void rejectCall() throws CallStateException { + synchronized (SipPhone.class) { + if (ringingCall.getState().isRinging()) { + if (DEBUG) Log.d(LOG_TAG, "rejectCall"); + ringingCall.rejectCall(); + } else { + throw new CallStateException("phone not ringing"); + } + } + } + + public Connection dial(String dialString) throws CallStateException { + synchronized (SipPhone.class) { + return dialInternal(dialString); + } + } + + private Connection dialInternal(String dialString) + throws CallStateException { + clearDisconnected(); + + if (!canDial()) { + throw new CallStateException("cannot dial in current state"); + } + if (foregroundCall.getState() == SipCall.State.ACTIVE) { + switchHoldingAndActive(); + } + if (foregroundCall.getState() != SipCall.State.IDLE) { + //we should have failed in !canDial() above before we get here + throw new CallStateException("cannot dial in current state"); + } + + foregroundCall.setMute(false); + try { + Connection c = foregroundCall.dial(dialString); + return c; + } catch (SipException e) { + Log.e(LOG_TAG, "dial()", e); + throw new CallStateException("dial error: " + e); + } + } + + public void switchHoldingAndActive() throws CallStateException { + if (DEBUG) Log.d(LOG_TAG, " ~~~~~~ switch fg and bg"); + synchronized (SipPhone.class) { + foregroundCall.switchWith(backgroundCall); + if (backgroundCall.getState().isAlive()) backgroundCall.hold(); + if (foregroundCall.getState().isAlive()) foregroundCall.unhold(); + } + } + + public boolean canConference() { + return true; + } + + public void conference() throws CallStateException { + synchronized (SipPhone.class) { + if ((foregroundCall.getState() != SipCall.State.ACTIVE) + || (foregroundCall.getState() != SipCall.State.ACTIVE)) { + throw new CallStateException("wrong state to merge calls: fg=" + + foregroundCall.getState() + ", bg=" + + backgroundCall.getState()); + } + foregroundCall.merge(backgroundCall); + } + } + + public void conference(Call that) throws CallStateException { + synchronized (SipPhone.class) { + if (!(that instanceof SipCall)) { + throw new CallStateException("expect " + SipCall.class + + ", cannot merge with " + that.getClass()); + } + foregroundCall.merge((SipCall) that); + } + } + + public boolean canTransfer() { + return false; + } + + public void explicitCallTransfer() throws CallStateException { + //mCT.explicitCallTransfer(); + } + + public void clearDisconnected() { + synchronized (SipPhone.class) { + ringingCall.clearDisconnected(); + foregroundCall.clearDisconnected(); + backgroundCall.clearDisconnected(); + + updatePhoneState(); + notifyPreciseCallStateChanged(); + } + } + + public void sendDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "sendDtmf called with invalid character '" + c + "'"); + } else if (foregroundCall.getState().isAlive()) { + synchronized (SipPhone.class) { + foregroundCall.sendDtmf(c); + } + } + } + + public void startDtmf(char c) { + if (!PhoneNumberUtils.is12Key(c)) { + Log.e(LOG_TAG, + "startDtmf called with invalid character '" + c + "'"); + } else { + sendDtmf(c); + } + } + + public void stopDtmf() { + // no op + } + + public void sendBurstDtmf(String dtmfString) { + Log.e(LOG_TAG, "[SipPhone] sendBurstDtmf() is a CDMA method"); + } + + public void getOutgoingCallerIdDisplay(Message onComplete) { + // FIXME: what to reply? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, + Message onComplete) { + // FIXME: what's this for SIP? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void getCallWaiting(Message onComplete) { + // FIXME: what to reply? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void setCallWaiting(boolean enable, Message onComplete) { + // FIXME: what to reply? + Log.e(LOG_TAG, "call waiting not supported"); + } + + @Override + public void setEchoSuppressionEnabled(boolean enabled) { + // TODO: Remove the enabled argument. We should check the speakerphone + // state with AudioManager instead of keeping a state here so the + // method with a state argument is redundant. Also rename the method + // to something like onSpeaerphoneStateChanged(). Echo suppression may + // not be available on every device. + synchronized (SipPhone.class) { + foregroundCall.setAudioGroupMode(); + } + } + + public void setMute(boolean muted) { + synchronized (SipPhone.class) { + foregroundCall.setMute(muted); + } + } + + public boolean getMute() { + return (foregroundCall.getState().isAlive() + ? foregroundCall.getMute() + : backgroundCall.getMute()); + } + + public Call getForegroundCall() { + return foregroundCall; + } + + public Call getBackgroundCall() { + return backgroundCall; + } + + public Call getRingingCall() { + return ringingCall; + } + + public ServiceState getServiceState() { + // FIXME: we may need to provide this when data connectivity is lost + // or when server is down + return super.getServiceState(); + } + + private String getUriString(SipProfile p) { + // SipProfile.getUriString() may contain "SIP:" and port + return p.getUserName() + "@" + getSipDomain(p); + } + + private String getSipDomain(SipProfile p) { + String domain = p.getSipDomain(); + // TODO: move this to SipProfile + if (domain.endsWith(":5060")) { + return domain.substring(0, domain.length() - 5); + } else { + return domain; + } + } + + private class SipCall extends SipCallBase { + void reset() { + connections.clear(); + setState(Call.State.IDLE); + } + + void switchWith(SipCall that) { + synchronized (SipPhone.class) { + SipCall tmp = new SipCall(); + tmp.takeOver(this); + this.takeOver(that); + that.takeOver(tmp); + } + } + + private void takeOver(SipCall that) { + connections = that.connections; + state = that.state; + for (Connection c : connections) { + ((SipConnection) c).changeOwner(this); + } + } + + @Override + public Phone getPhone() { + return SipPhone.this; + } + + @Override + public List<Connection> getConnections() { + synchronized (SipPhone.class) { + // FIXME should return Collections.unmodifiableList(); + return connections; + } + } + + Connection dial(String originalNumber) throws SipException { + String calleeSipUri = originalNumber; + if (!calleeSipUri.contains("@")) { + String replaceStr = Pattern.quote(mProfile.getUserName() + "@"); + calleeSipUri = mProfile.getUriString().replaceFirst(replaceStr, + calleeSipUri + "@"); + } + try { + SipProfile callee = + new SipProfile.Builder(calleeSipUri).build(); + SipConnection c = new SipConnection(this, callee, + originalNumber); + c.dial(); + connections.add(c); + setState(Call.State.DIALING); + return c; + } catch (ParseException e) { + throw new SipException("dial", e); + } + } + + @Override + public void hangup() throws CallStateException { + synchronized (SipPhone.class) { + if (state.isAlive()) { + if (DEBUG) Log.d(LOG_TAG, "hang up call: " + getState() + + ": " + this + " on phone " + getPhone()); + setState(State.DISCONNECTING); + CallStateException excp = null; + for (Connection c : connections) { + try { + c.hangup(); + } catch (CallStateException e) { + excp = e; + } + } + if (excp != null) throw excp; + } else { + if (DEBUG) Log.d(LOG_TAG, "hang up dead call: " + getState() + + ": " + this + " on phone " + getPhone()); + } + } + } + + void initIncomingCall(SipAudioCall sipAudioCall, boolean makeCallWait) { + SipProfile callee = sipAudioCall.getPeerProfile(); + SipConnection c = new SipConnection(this, callee); + connections.add(c); + + Call.State newState = makeCallWait ? State.WAITING : State.INCOMING; + c.initIncomingCall(sipAudioCall, newState); + + setState(newState); + notifyNewRingingConnectionP(c); + } + + void rejectCall() throws CallStateException { + hangup(); + } + + void acceptCall() throws CallStateException { + if (this != ringingCall) { + throw new CallStateException("acceptCall() in a non-ringing call"); + } + if (connections.size() != 1) { + throw new CallStateException("acceptCall() in a conf call"); + } + ((SipConnection) connections.get(0)).acceptCall(); + } + + private boolean isSpeakerOn() { + return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) + .isSpeakerphoneOn(); + } + + void setAudioGroupMode() { + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup == null) return; + int mode = audioGroup.getMode(); + if (state == State.HOLDING) { + audioGroup.setMode(AudioGroup.MODE_ON_HOLD); + } else if (getMute()) { + audioGroup.setMode(AudioGroup.MODE_MUTED); + } else if (isSpeakerOn()) { + audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION); + } else { + audioGroup.setMode(AudioGroup.MODE_NORMAL); + } + if (DEBUG) Log.d(LOG_TAG, String.format( + "audioGroup mode change: %d --> %d", mode, + audioGroup.getMode())); + } + + void hold() throws CallStateException { + setState(State.HOLDING); + for (Connection c : connections) ((SipConnection) c).hold(); + setAudioGroupMode(); + } + + void unhold() throws CallStateException { + setState(State.ACTIVE); + AudioGroup audioGroup = new AudioGroup(); + for (Connection c : connections) { + ((SipConnection) c).unhold(audioGroup); + } + setAudioGroupMode(); + } + + void setMute(boolean muted) { + for (Connection c : connections) { + ((SipConnection) c).setMute(muted); + } + } + + boolean getMute() { + return connections.isEmpty() + ? false + : ((SipConnection) connections.get(0)).getMute(); + } + + void merge(SipCall that) throws CallStateException { + AudioGroup audioGroup = getAudioGroup(); + + // copy to an array to avoid concurrent modification as connections + // in that.connections will be removed in add(SipConnection). + Connection[] cc = that.connections.toArray( + new Connection[that.connections.size()]); + for (Connection c : cc) { + SipConnection conn = (SipConnection) c; + add(conn); + if (conn.getState() == Call.State.HOLDING) { + conn.unhold(audioGroup); + } + } + that.setState(Call.State.IDLE); + } + + private void add(SipConnection conn) { + SipCall call = conn.getCall(); + if (call == this) return; + if (call != null) call.connections.remove(conn); + + connections.add(conn); + conn.changeOwner(this); + } + + void sendDtmf(char c) { + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup == null) return; + audioGroup.sendDtmf(convertDtmf(c)); + } + + private int convertDtmf(char c) { + int code = c - '0'; + if ((code < 0) || (code > 9)) { + switch (c) { + case '*': return 10; + case '#': return 11; + case 'A': return 12; + case 'B': return 13; + case 'C': return 14; + case 'D': return 15; + default: + throw new IllegalArgumentException( + "invalid DTMF char: " + (int) c); + } + } + return code; + } + + @Override + protected void setState(State newState) { + if (state != newState) { + if (DEBUG) Log.v(LOG_TAG, "+***+ call state changed: " + state + + " --> " + newState + ": " + this + ": on phone " + + getPhone() + " " + connections.size()); + + if (newState == Call.State.ALERTING) { + state = newState; // need in ALERTING to enable ringback + SipPhone.this.startRingbackTone(); + } else if (state == Call.State.ALERTING) { + SipPhone.this.stopRingbackTone(); + } + state = newState; + updatePhoneState(); + notifyPreciseCallStateChanged(); + } + } + + void onConnectionStateChanged(SipConnection conn) { + // this can be called back when a conf call is formed + if (state != State.ACTIVE) { + setState(conn.getState()); + } + } + + void onConnectionEnded(SipConnection conn) { + // set state to DISCONNECTED only when all conns are disconnected + if (state != State.DISCONNECTED) { + boolean allConnectionsDisconnected = true; + if (DEBUG) Log.d(LOG_TAG, "---check connections: " + + connections.size()); + for (Connection c : connections) { + if (DEBUG) Log.d(LOG_TAG, " state=" + c.getState() + ": " + + c); + if (c.getState() != State.DISCONNECTED) { + allConnectionsDisconnected = false; + break; + } + } + if (allConnectionsDisconnected) setState(State.DISCONNECTED); + } + notifyDisconnectP(conn); + } + + private AudioGroup getAudioGroup() { + if (connections.isEmpty()) return null; + return ((SipConnection) connections.get(0)).getAudioGroup(); + } + } + + private class SipConnection extends SipConnectionBase { + private SipCall mOwner; + private SipAudioCall mSipAudioCall; + private Call.State mState = Call.State.IDLE; + private SipProfile mPeer; + private String mOriginalNumber; // may be a PSTN number + private boolean mIncoming = false; + + private SipAudioCallAdapter mAdapter = new SipAudioCallAdapter() { + @Override + protected void onCallEnded(DisconnectCause cause) { + if (getDisconnectCause() != DisconnectCause.LOCAL) { + setDisconnectCause(cause); + } + synchronized (SipPhone.class) { + setState(Call.State.DISCONNECTED); + SipAudioCall sipAudioCall = mSipAudioCall; + mSipAudioCall = null; + String sessionState = (sipAudioCall == null) + ? "" + : (sipAudioCall.getState() + ", "); + if (DEBUG) Log.d(LOG_TAG, "--- connection ended: " + + mPeer.getUriString() + ": " + sessionState + + "cause: " + getDisconnectCause() + ", on phone " + + getPhone()); + if (sipAudioCall != null) { + sipAudioCall.setListener(null); + sipAudioCall.close(); + } + mOwner.onConnectionEnded(SipConnection.this); + } + } + + @Override + public void onCallEstablished(SipAudioCall call) { + onChanged(call); + if (mState == Call.State.ACTIVE) call.startAudio(); + } + + @Override + public void onCallHeld(SipAudioCall call) { + onChanged(call); + if (mState == Call.State.HOLDING) call.startAudio(); + } + + @Override + public void onChanged(SipAudioCall call) { + synchronized (SipPhone.class) { + Call.State newState = getCallStateFrom(call); + if (mState == newState) return; + if (newState == Call.State.INCOMING) { + setState(mOwner.getState()); // INCOMING or WAITING + } else { + if (mOwner == ringingCall) { + if (ringingCall.getState() == Call.State.WAITING) { + try { + switchHoldingAndActive(); + } catch (CallStateException e) { + // disconnect the call. + onCallEnded(DisconnectCause.LOCAL); + return; + } + } + foregroundCall.switchWith(ringingCall); + } + setState(newState); + } + mOwner.onConnectionStateChanged(SipConnection.this); + if (DEBUG) Log.v(LOG_TAG, "+***+ connection state changed: " + + mPeer.getUriString() + ": " + mState + + " on phone " + getPhone()); + } + } + + @Override + protected void onError(DisconnectCause cause) { + if (DEBUG) Log.d(LOG_TAG, "SIP error: " + cause); + onCallEnded(cause); + } + }; + + public SipConnection(SipCall owner, SipProfile callee, + String originalNumber) { + super(originalNumber); + mOwner = owner; + mPeer = callee; + mOriginalNumber = originalNumber; + } + + public SipConnection(SipCall owner, SipProfile callee) { + this(owner, callee, getUriString(callee)); + } + + @Override + public String getCnapName() { + String displayName = mPeer.getDisplayName(); + return TextUtils.isEmpty(displayName) ? null + : displayName; + } + + @Override + public int getNumberPresentation() { + return PhoneConstants.PRESENTATION_ALLOWED; + } + + void initIncomingCall(SipAudioCall sipAudioCall, Call.State newState) { + setState(newState); + mSipAudioCall = sipAudioCall; + sipAudioCall.setListener(mAdapter); // call back to set state + mIncoming = true; + } + + void acceptCall() throws CallStateException { + try { + mSipAudioCall.answerCall(TIMEOUT_ANSWER_CALL); + } catch (SipException e) { + throw new CallStateException("acceptCall(): " + e); + } + } + + void changeOwner(SipCall owner) { + mOwner = owner; + } + + AudioGroup getAudioGroup() { + if (mSipAudioCall == null) return null; + return mSipAudioCall.getAudioGroup(); + } + + void dial() throws SipException { + setState(Call.State.DIALING); + mSipAudioCall = mSipManager.makeAudioCall(mProfile, mPeer, null, + TIMEOUT_MAKE_CALL); + mSipAudioCall.setListener(mAdapter); + } + + void hold() throws CallStateException { + setState(Call.State.HOLDING); + try { + mSipAudioCall.holdCall(TIMEOUT_HOLD_CALL); + } catch (SipException e) { + throw new CallStateException("hold(): " + e); + } + } + + void unhold(AudioGroup audioGroup) throws CallStateException { + mSipAudioCall.setAudioGroup(audioGroup); + setState(Call.State.ACTIVE); + try { + mSipAudioCall.continueCall(TIMEOUT_HOLD_CALL); + } catch (SipException e) { + throw new CallStateException("unhold(): " + e); + } + } + + void setMute(boolean muted) { + if ((mSipAudioCall != null) && (muted != mSipAudioCall.isMuted())) { + mSipAudioCall.toggleMute(); + } + } + + boolean getMute() { + return (mSipAudioCall == null) ? false + : mSipAudioCall.isMuted(); + } + + @Override + protected void setState(Call.State state) { + if (state == mState) return; + super.setState(state); + mState = state; + } + + @Override + public Call.State getState() { + return mState; + } + + @Override + public boolean isIncoming() { + return mIncoming; + } + + @Override + public String getAddress() { + // Phone app uses this to query caller ID. Return the original dial + // number (which may be a PSTN number) instead of the peer's SIP + // URI. + return mOriginalNumber; + } + + @Override + public SipCall getCall() { + return mOwner; + } + + @Override + protected Phone getPhone() { + return mOwner.getPhone(); + } + + @Override + public void hangup() throws CallStateException { + synchronized (SipPhone.class) { + if (DEBUG) Log.d(LOG_TAG, "hangup conn: " + mPeer.getUriString() + + ": " + mState + ": on phone " + + getPhone().getPhoneName()); + if (!mState.isAlive()) return; + try { + SipAudioCall sipAudioCall = mSipAudioCall; + if (sipAudioCall != null) { + sipAudioCall.setListener(null); + sipAudioCall.endCall(); + } + } catch (SipException e) { + throw new CallStateException("hangup(): " + e); + } finally { + mAdapter.onCallEnded(((mState == Call.State.INCOMING) + || (mState == Call.State.WAITING)) + ? DisconnectCause.INCOMING_REJECTED + : DisconnectCause.LOCAL); + } + } + } + + @Override + public void separate() throws CallStateException { + synchronized (SipPhone.class) { + SipCall call = (getPhone() == SipPhone.this) + ? (SipCall) SipPhone.this.getBackgroundCall() + : (SipCall) SipPhone.this.getForegroundCall(); + if (call.getState() != Call.State.IDLE) { + throw new CallStateException( + "cannot put conn back to a call in non-idle state: " + + call.getState()); + } + if (DEBUG) Log.d(LOG_TAG, "separate conn: " + + mPeer.getUriString() + " from " + mOwner + " back to " + + call); + + // separate the AudioGroup and connection from the original call + Phone originalPhone = getPhone(); + AudioGroup audioGroup = call.getAudioGroup(); // may be null + call.add(this); + mSipAudioCall.setAudioGroup(audioGroup); + + // put the original call to bg; and the separated call becomes + // fg if it was in bg + originalPhone.switchHoldingAndActive(); + + // start audio and notify the phone app of the state change + call = (SipCall) SipPhone.this.getForegroundCall(); + mSipAudioCall.startAudio(); + call.onConnectionStateChanged(this); + } + } + + } + + private static Call.State getCallStateFrom(SipAudioCall sipAudioCall) { + if (sipAudioCall.isOnHold()) return Call.State.HOLDING; + int sessionState = sipAudioCall.getState(); + switch (sessionState) { + case SipSession.State.READY_TO_CALL: return Call.State.IDLE; + case SipSession.State.INCOMING_CALL: + case SipSession.State.INCOMING_CALL_ANSWERING: return Call.State.INCOMING; + case SipSession.State.OUTGOING_CALL: return Call.State.DIALING; + case SipSession.State.OUTGOING_CALL_RING_BACK: return Call.State.ALERTING; + case SipSession.State.OUTGOING_CALL_CANCELING: return Call.State.DISCONNECTING; + case SipSession.State.IN_CALL: return Call.State.ACTIVE; + default: + Log.w(LOG_TAG, "illegal connection state: " + sessionState); + return Call.State.DISCONNECTED; + } + } + + private abstract class SipAudioCallAdapter extends SipAudioCall.Listener { + protected abstract void onCallEnded(Connection.DisconnectCause cause); + protected abstract void onError(Connection.DisconnectCause cause); + + @Override + public void onCallEnded(SipAudioCall call) { + onCallEnded(call.isInCall() + ? Connection.DisconnectCause.NORMAL + : Connection.DisconnectCause.INCOMING_MISSED); + } + + @Override + public void onCallBusy(SipAudioCall call) { + onCallEnded(Connection.DisconnectCause.BUSY); + } + + @Override + public void onError(SipAudioCall call, int errorCode, + String errorMessage) { + switch (errorCode) { + case SipErrorCode.SERVER_UNREACHABLE: + onError(Connection.DisconnectCause.SERVER_UNREACHABLE); + break; + case SipErrorCode.PEER_NOT_REACHABLE: + onError(Connection.DisconnectCause.NUMBER_UNREACHABLE); + break; + case SipErrorCode.INVALID_REMOTE_URI: + onError(Connection.DisconnectCause.INVALID_NUMBER); + break; + case SipErrorCode.TIME_OUT: + case SipErrorCode.TRANSACTION_TERMINTED: + onError(Connection.DisconnectCause.TIMED_OUT); + break; + case SipErrorCode.DATA_CONNECTION_LOST: + onError(Connection.DisconnectCause.LOST_SIGNAL); + break; + case SipErrorCode.INVALID_CREDENTIALS: + onError(Connection.DisconnectCause.INVALID_CREDENTIALS); + break; + case SipErrorCode.CROSS_DOMAIN_AUTHENTICATION: + onError(Connection.DisconnectCause.OUT_OF_NETWORK); + break; + case SipErrorCode.SERVER_ERROR: + onError(Connection.DisconnectCause.SERVER_ERROR); + break; + case SipErrorCode.SOCKET_ERROR: + case SipErrorCode.CLIENT_ERROR: + default: + Log.w(LOG_TAG, "error: " + SipErrorCode.toString(errorCode) + + ": " + errorMessage); + onError(Connection.DisconnectCause.ERROR_UNSPECIFIED); + } + } + } +} diff --git a/src/java/com/android/internal/telephony/sip/SipPhoneBase.java b/src/java/com/android/internal/telephony/sip/SipPhoneBase.java new file mode 100755 index 0000000..ff64486 --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipPhoneBase.java @@ -0,0 +1,465 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import android.content.Context; +import android.net.LinkProperties; +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.os.Registrant; +import android.os.RegistrantList; +import android.os.SystemProperties; +import android.telephony.CellLocation; +import android.telephony.ServiceState; +import android.telephony.SignalStrength; +import android.util.Log; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.DataConnection; +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.IccFileHandler; +import com.android.internal.telephony.IccPhoneBookInterfaceManager; +import com.android.internal.telephony.IccSmsInterfaceManager; +import com.android.internal.telephony.MmiCode; +import com.android.internal.telephony.OperatorInfo; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.PhoneNotifier; +import com.android.internal.telephony.PhoneSubInfo; +import com.android.internal.telephony.TelephonyProperties; +import com.android.internal.telephony.UUSInfo; + +import java.util.ArrayList; +import java.util.List; + +abstract class SipPhoneBase extends PhoneBase { + private static final String LOG_TAG = "SipPhone"; + + private RegistrantList mRingbackRegistrants = new RegistrantList(); + private PhoneConstants.State state = PhoneConstants.State.IDLE; + + public SipPhoneBase(Context context, PhoneNotifier notifier) { + super(notifier, context, new SipCommandInterface(context), false); + } + + public abstract Call getForegroundCall(); + + public abstract Call getBackgroundCall(); + + public abstract Call getRingingCall(); + + public Connection dial(String dialString, UUSInfo uusInfo) + throws CallStateException { + // ignore UUSInfo + return dial(dialString); + } + + void migrateFrom(SipPhoneBase from) { + migrate(mRingbackRegistrants, from.mRingbackRegistrants); + migrate(mPreciseCallStateRegistrants, from.mPreciseCallStateRegistrants); + migrate(mNewRingingConnectionRegistrants, from.mNewRingingConnectionRegistrants); + migrate(mIncomingRingRegistrants, from.mIncomingRingRegistrants); + migrate(mDisconnectRegistrants, from.mDisconnectRegistrants); + migrate(mServiceStateRegistrants, from.mServiceStateRegistrants); + migrate(mMmiCompleteRegistrants, from.mMmiCompleteRegistrants); + migrate(mMmiRegistrants, from.mMmiRegistrants); + migrate(mUnknownConnectionRegistrants, from.mUnknownConnectionRegistrants); + migrate(mSuppServiceFailedRegistrants, from.mSuppServiceFailedRegistrants); + } + + static void migrate(RegistrantList to, RegistrantList from) { + from.removeCleared(); + for (int i = 0, n = from.size(); i < n; i++) { + to.add((Registrant) from.get(i)); + } + } + + @Override + public void registerForRingbackTone(Handler h, int what, Object obj) { + mRingbackRegistrants.addUnique(h, what, obj); + } + + @Override + public void unregisterForRingbackTone(Handler h) { + mRingbackRegistrants.remove(h); + } + + protected void startRingbackTone() { + AsyncResult result = new AsyncResult(null, Boolean.TRUE, null); + mRingbackRegistrants.notifyRegistrants(result); + } + + protected void stopRingbackTone() { + AsyncResult result = new AsyncResult(null, Boolean.FALSE, null); + mRingbackRegistrants.notifyRegistrants(result); + } + + public ServiceState getServiceState() { + // FIXME: we may need to provide this when data connectivity is lost + // or when server is down + ServiceState s = new ServiceState(); + s.setState(ServiceState.STATE_IN_SERVICE); + return s; + } + + public CellLocation getCellLocation() { + return null; + } + + public PhoneConstants.State getState() { + return state; + } + + public int getPhoneType() { + return PhoneConstants.PHONE_TYPE_SIP; + } + + public SignalStrength getSignalStrength() { + return new SignalStrength(); + } + + public boolean getMessageWaitingIndicator() { + return false; + } + + public boolean getCallForwardingIndicator() { + return false; + } + + public List<? extends MmiCode> getPendingMmiCodes() { + return new ArrayList<MmiCode>(0); + } + + public PhoneConstants.DataState getDataConnectionState() { + return PhoneConstants.DataState.DISCONNECTED; + } + + public PhoneConstants.DataState getDataConnectionState(String apnType) { + return PhoneConstants.DataState.DISCONNECTED; + } + + public DataActivityState getDataActivityState() { + return DataActivityState.NONE; + } + + /** + * Notify any interested party of a Phone state change {@link Phone.State} + */ + void notifyPhoneStateChanged() { + mNotifier.notifyPhoneState(this); + } + + /** + * Notify registrants of a change in the call state. This notifies changes in {@link Call.State} + * Use this when changes in the precise call state are needed, else use notifyPhoneStateChanged. + */ + void notifyPreciseCallStateChanged() { + /* we'd love it if this was package-scoped*/ + super.notifyPreciseCallStateChangedP(); + } + + void notifyNewRingingConnection(Connection c) { + super.notifyNewRingingConnectionP(c); + } + + void notifyDisconnect(Connection cn) { + mDisconnectRegistrants.notifyResult(cn); + } + + void notifyUnknownConnection() { + mUnknownConnectionRegistrants.notifyResult(this); + } + + void notifySuppServiceFailed(SuppService code) { + mSuppServiceFailedRegistrants.notifyResult(code); + } + + void notifyServiceStateChanged(ServiceState ss) { + super.notifyServiceStateChangedP(ss); + } + + public void notifyCallForwardingIndicator() { + mNotifier.notifyCallForwardingChanged(this); + } + + public boolean canDial() { + int serviceState = getServiceState().getState(); + Log.v(LOG_TAG, "canDial(): serviceState = " + serviceState); + if (serviceState == ServiceState.STATE_POWER_OFF) return false; + + String disableCall = SystemProperties.get( + TelephonyProperties.PROPERTY_DISABLE_CALL, "false"); + Log.v(LOG_TAG, "canDial(): disableCall = " + disableCall); + if (disableCall.equals("true")) return false; + + Log.v(LOG_TAG, "canDial(): ringingCall: " + getRingingCall().getState()); + Log.v(LOG_TAG, "canDial(): foregndCall: " + getForegroundCall().getState()); + Log.v(LOG_TAG, "canDial(): backgndCall: " + getBackgroundCall().getState()); + return !getRingingCall().isRinging() + && (!getForegroundCall().getState().isAlive() + || !getBackgroundCall().getState().isAlive()); + } + + public boolean handleInCallMmiCommands(String dialString) + throws CallStateException { + return false; + } + + boolean isInCall() { + Call.State foregroundCallState = getForegroundCall().getState(); + Call.State backgroundCallState = getBackgroundCall().getState(); + Call.State ringingCallState = getRingingCall().getState(); + + return (foregroundCallState.isAlive() || backgroundCallState.isAlive() + || ringingCallState.isAlive()); + } + + public boolean handlePinMmi(String dialString) { + return false; + } + + public void sendUssdResponse(String ussdMessge) { + } + + public void registerForSuppServiceNotification( + Handler h, int what, Object obj) { + } + + public void unregisterForSuppServiceNotification(Handler h) { + } + + public void setRadioPower(boolean power) { + } + + public String getVoiceMailNumber() { + return null; + } + + public String getVoiceMailAlphaTag() { + return null; + } + + public String getDeviceId() { + return null; + } + + public String getDeviceSvn() { + return null; + } + + public String getImei() { + return null; + } + + public String getEsn() { + Log.e(LOG_TAG, "[SipPhone] getEsn() is a CDMA method"); + return "0"; + } + + public String getMeid() { + Log.e(LOG_TAG, "[SipPhone] getMeid() is a CDMA method"); + return "0"; + } + + public String getSubscriberId() { + return null; + } + + public String getIccSerialNumber() { + return null; + } + + public String getLine1Number() { + return null; + } + + public String getLine1AlphaTag() { + return null; + } + + public void setLine1Number(String alphaTag, String number, Message onComplete) { + // FIXME: what to reply for SIP? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void setVoiceMailNumber(String alphaTag, String voiceMailNumber, + Message onComplete) { + // FIXME: what to reply for SIP? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void getCallForwardingOption(int commandInterfaceCFReason, Message onComplete) { + } + + public void setCallForwardingOption(int commandInterfaceCFAction, + int commandInterfaceCFReason, String dialingNumber, + int timerSeconds, Message onComplete) { + } + + public void getOutgoingCallerIdDisplay(Message onComplete) { + // FIXME: what to reply? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void setOutgoingCallerIdDisplay(int commandInterfaceCLIRMode, + Message onComplete) { + // FIXME: what's this for SIP? + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void getCallWaiting(Message onComplete) { + AsyncResult.forMessage(onComplete, null, null); + onComplete.sendToTarget(); + } + + public void setCallWaiting(boolean enable, Message onComplete) { + Log.e(LOG_TAG, "call waiting not supported"); + } + + public boolean getIccRecordsLoaded() { + return false; + } + + public IccCard getIccCard() { + return null; + } + + public void getAvailableNetworks(Message response) { + } + + public void setNetworkSelectionModeAutomatic(Message response) { + } + + public void selectNetworkManually( + OperatorInfo network, + Message response) { + } + + public void getNeighboringCids(Message response) { + } + + public void setOnPostDialCharacter(Handler h, int what, Object obj) { + } + + public void getDataCallList(Message response) { + } + + public List<DataConnection> getCurrentDataConnectionList () { + return null; + } + + public void updateServiceLocation() { + } + + public void enableLocationUpdates() { + } + + public void disableLocationUpdates() { + } + + public boolean getDataRoamingEnabled() { + return false; + } + + public void setDataRoamingEnabled(boolean enable) { + } + + public boolean enableDataConnectivity() { + return false; + } + + public boolean disableDataConnectivity() { + return false; + } + + public boolean isDataConnectivityPossible() { + return false; + } + + boolean updateCurrentCarrierInProvider() { + return false; + } + + public void saveClirSetting(int commandInterfaceCLIRMode) { + } + + public PhoneSubInfo getPhoneSubInfo(){ + return null; + } + + public IccSmsInterfaceManager getIccSmsInterfaceManager(){ + return null; + } + + public IccPhoneBookInterfaceManager getIccPhoneBookInterfaceManager(){ + return null; + } + + public IccFileHandler getIccFileHandler(){ + return null; + } + + public void activateCellBroadcastSms(int activate, Message response) { + Log.e(LOG_TAG, "Error! This functionality is not implemented for SIP."); + } + + public void getCellBroadcastSmsConfig(Message response) { + Log.e(LOG_TAG, "Error! This functionality is not implemented for SIP."); + } + + public void setCellBroadcastSmsConfig(int[] configValuesArray, Message response){ + Log.e(LOG_TAG, "Error! This functionality is not implemented for SIP."); + } + + //@Override + public boolean needsOtaServiceProvisioning() { + // FIXME: what's this for SIP? + return false; + } + + //@Override + public LinkProperties getLinkProperties(String apnType) { + // FIXME: what's this for SIP? + return null; + } + + void updatePhoneState() { + PhoneConstants.State oldState = state; + + if (getRingingCall().isRinging()) { + state = PhoneConstants.State.RINGING; + } else if (getForegroundCall().isIdle() + && getBackgroundCall().isIdle()) { + state = PhoneConstants.State.IDLE; + } else { + state = PhoneConstants.State.OFFHOOK; + } + + if (state != oldState) { + Log.d(LOG_TAG, " ^^^ new phone state: " + state); + notifyPhoneStateChanged(); + } + } +} diff --git a/src/java/com/android/internal/telephony/sip/SipPhoneFactory.java b/src/java/com/android/internal/telephony/sip/SipPhoneFactory.java new file mode 100644 index 0000000..611e3ea --- /dev/null +++ b/src/java/com/android/internal/telephony/sip/SipPhoneFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 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.internal.telephony.sip; + +import com.android.internal.telephony.PhoneNotifier; + +import android.content.Context; +import android.net.sip.SipProfile; +import android.util.Log; + +import java.text.ParseException; + +/** + * {@hide} + */ +public class SipPhoneFactory { + /** + * Makes a {@link SipPhone} object. + * @param sipUri the local SIP URI the phone runs on + * @param context {@code Context} needed to create a Phone object + * @param phoneNotifier {@code PhoneNotifier} needed to create a Phone + * object + * @return the {@code SipPhone} object or null if the SIP URI is not valid + */ + public static SipPhone makePhone(String sipUri, Context context, + PhoneNotifier phoneNotifier) { + try { + SipProfile profile = new SipProfile.Builder(sipUri).build(); + return new SipPhone(context, phoneNotifier, profile); + } catch (ParseException e) { + Log.w("SipPhoneFactory", "makePhone", e); + return null; + } + } +} diff --git a/src/java/com/android/internal/telephony/test/ModelInterpreter.java b/src/java/com/android/internal/telephony/test/ModelInterpreter.java new file mode 100644 index 0000000..b116c35 --- /dev/null +++ b/src/java/com/android/internal/telephony/test/ModelInterpreter.java @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2006 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.internal.telephony.test; + +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; + +// Also in ATChannel.java +class LineReader +{ + /** + * Not threadsafe + * Assumes input is ASCII + */ + + //***** Constants + + // For what it's worth, this is also the size of an + // OMAP CSMI mailbox + static final int BUFFER_SIZE = 0x1000; + + // just to prevent constant allocations + byte buffer[] = new byte[BUFFER_SIZE]; + + //***** Instance Variables + + InputStream inStream; + + LineReader (InputStream s) + { + inStream = s; + } + + String + getNextLine() + { + return getNextLine(false); + } + + String + getNextLineCtrlZ() + { + return getNextLine(true); + } + + /** + * Note: doesn't return the last incomplete line read on EOF, since + * it doesn't typically matter anyway + * + * Returns NULL on EOF + */ + + String + getNextLine(boolean ctrlZ) + { + int i = 0; + + try { + for (;;) { + int result; + + result = inStream.read(); + + if (result < 0) { + return null; + } + + if (ctrlZ && result == 0x1a) { + break; + } else if (result == '\r' || result == '\n') { + if (i == 0) { + // Skip leading cr/lf + continue; + } else { + break; + } + } + + buffer[i++] = (byte)result; + } + } catch (IOException ex) { + return null; + } catch (IndexOutOfBoundsException ex) { + System.err.println("ATChannel: buffer overflow"); + } + + try { + return new String(buffer, 0, i, "US-ASCII"); + } catch (UnsupportedEncodingException ex) { + System.err.println("ATChannel: implausable UnsupportedEncodingException"); + return null; + } + } +} + + + +class InterpreterEx extends Exception +{ + public + InterpreterEx (String result) + { + this.result = result; + } + + String result; +} + +public class ModelInterpreter + implements Runnable, SimulatedRadioControl +{ + static final int MAX_CALLS = 6; + + /** number of msec between dialing -> alerting and alerting->active */ + static final int CONNECTING_PAUSE_MSEC = 5 * 100; + + static final String LOG_TAG = "ModelInterpreter"; + + //***** Instance Variables + + InputStream in; + OutputStream out; + LineReader lineReader; + ServerSocket ss; + + private String finalResponse; + + SimulatedGsmCallState simulatedCallState; + + HandlerThread mHandlerThread; + + int pausedResponseCount; + Object pausedResponseMonitor = new Object(); + + //***** Events + + static final int PROGRESS_CALL_STATE = 1; + + //***** Constructor + + public + ModelInterpreter (InputStream in, OutputStream out) + { + this.in = in; + this.out = out; + + init(); + } + + public + ModelInterpreter (InetSocketAddress sa) throws java.io.IOException + { + ss = new ServerSocket(); + + ss.setReuseAddress(true); + ss.bind(sa); + + init(); + } + + private void + init() + { + new Thread(this, "ModelInterpreter").start(); + mHandlerThread = new HandlerThread("ModelInterpreter"); + mHandlerThread.start(); + Looper looper = mHandlerThread.getLooper(); + simulatedCallState = new SimulatedGsmCallState(looper); + } + + //***** Runnable Implementation + + public void run() + { + for (;;) { + if (ss != null) { + Socket s; + + try { + s = ss.accept(); + } catch (java.io.IOException ex) { + Log.w(LOG_TAG, + "IOException on socket.accept(); stopping", ex); + return; + } + + try { + in = s.getInputStream(); + out = s.getOutputStream(); + } catch (java.io.IOException ex) { + Log.w(LOG_TAG, + "IOException on accepted socket(); re-listening", ex); + continue; + } + + Log.i(LOG_TAG, "New connection accepted"); + } + + + lineReader = new LineReader (in); + + println ("Welcome"); + + for (;;) { + String line; + + line = lineReader.getNextLine(); + + //System.out.println("MI<< " + line); + + if (line == null) { + break; + } + + synchronized(pausedResponseMonitor) { + while (pausedResponseCount > 0) { + try { + pausedResponseMonitor.wait(); + } catch (InterruptedException ex) { + } + } + } + + synchronized (this) { + try { + finalResponse = "OK"; + processLine(line); + println(finalResponse); + } catch (InterpreterEx ex) { + println(ex.result); + } catch (RuntimeException ex) { + ex.printStackTrace(); + println("ERROR"); + } + } + } + + Log.i(LOG_TAG, "Disconnected"); + + if (ss == null) { + // no reconnect in this case + break; + } + } + } + + + //***** Instance Methods + + /** Start the simulated phone ringing */ + public void + triggerRing(String number) + { + synchronized (this) { + boolean success; + + success = simulatedCallState.triggerRing(number); + + if (success) { + println ("RING"); + } + } + } + + /** If a call is DIALING or ALERTING, progress it to the next state */ + public void + progressConnectingCallState() + { + simulatedCallState.progressConnectingCallState(); + } + + + /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */ + public void + progressConnectingToActive() + { + simulatedCallState.progressConnectingToActive(); + } + + /** automatically progress mobile originated calls to ACTIVE. + * default to true + */ + public void + setAutoProgressConnectingCall(boolean b) + { + simulatedCallState.setAutoProgressConnectingCall(b); + } + + public void + setNextDialFailImmediately(boolean b) + { + simulatedCallState.setNextDialFailImmediately(b); + } + + public void setNextCallFailCause(int gsmCause) + { + //FIXME implement + } + + + /** hangup ringing, dialing, or actuve calls */ + public void + triggerHangupForeground() + { + boolean success; + + success = simulatedCallState.triggerHangupForeground(); + + if (success) { + println ("NO CARRIER"); + } + } + + /** hangup holding calls */ + public void + triggerHangupBackground() + { + boolean success; + + success = simulatedCallState.triggerHangupBackground(); + + if (success) { + println ("NO CARRIER"); + } + } + + /** hangup all */ + + public void + triggerHangupAll() + { + boolean success; + + success = simulatedCallState.triggerHangupAll(); + + if (success) { + println ("NO CARRIER"); + } + } + + public void + sendUnsolicited (String unsol) + { + synchronized (this) { + println(unsol); + } + } + + public void triggerSsn(int a, int b) {} + public void triggerIncomingUssd(String statusCode, String message) {} + + public void + triggerIncomingSMS(String message) + { +/************** + StringBuilder pdu = new StringBuilder(); + + pdu.append ("00"); //SMSC address - 0 bytes + + pdu.append ("04"); // Message type indicator + + // source address: +18005551212 + pdu.append("918100551521F0"); + + // protocol ID and data coding scheme + pdu.append("0000"); + + Calendar c = Calendar.getInstance(); + + pdu.append (c. + + + + synchronized (this) { + println("+CMT: ,1\r" + pdu.toString()); + } + +**************/ + } + + public void + pauseResponses() + { + synchronized(pausedResponseMonitor) { + pausedResponseCount++; + } + } + + public void + resumeResponses() + { + synchronized(pausedResponseMonitor) { + pausedResponseCount--; + + if (pausedResponseCount == 0) { + pausedResponseMonitor.notifyAll(); + } + } + } + + //***** Private Instance Methods + + private void + onAnswer() throws InterpreterEx + { + boolean success; + + success = simulatedCallState.onAnswer(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + onHangup() throws InterpreterEx + { + boolean success = false; + + success = simulatedCallState.onAnswer(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + + finalResponse = "NO CARRIER"; + } + + private void + onCHLD(String command) throws InterpreterEx + { + // command starts with "+CHLD=" + char c0; + char c1 = 0; + boolean success; + + c0 = command.charAt(6); + + if (command.length() >= 8) { + c1 = command.charAt(7); + } + + success = simulatedCallState.onChld(c0, c1); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + releaseHeldOrUDUB() throws InterpreterEx + { + boolean success; + + success = simulatedCallState.releaseHeldOrUDUB(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + releaseActiveAcceptHeldOrWaiting() throws InterpreterEx + { + boolean success; + + success = simulatedCallState.releaseActiveAcceptHeldOrWaiting(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + switchActiveAndHeldOrWaiting() throws InterpreterEx + { + boolean success; + + success = simulatedCallState.switchActiveAndHeldOrWaiting(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + separateCall(int index) throws InterpreterEx + { + boolean success; + + success = simulatedCallState.separateCall(index); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + conference() throws InterpreterEx + { + boolean success; + + success = simulatedCallState.conference(); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + onDial(String command) throws InterpreterEx + { + boolean success; + + success = simulatedCallState.onDial(command.substring(1)); + + if (!success) { + throw new InterpreterEx("ERROR"); + } + } + + private void + onCLCC() throws InterpreterEx + { + List<String> lines; + + lines = simulatedCallState.getClccLines(); + + for (int i = 0, s = lines.size() ; i < s ; i++) { + println (lines.get(i)); + } + } + + private void + onSMSSend(String command) throws InterpreterEx + { + String pdu; + + print ("> "); + pdu = lineReader.getNextLineCtrlZ(); + + println("+CMGS: 1"); + } + + void + processLine (String line) throws InterpreterEx + { + String[] commands; + + commands = splitCommands(line); + + for (int i = 0; i < commands.length ; i++) { + String command = commands[i]; + + if (command.equals("A")) { + onAnswer(); + } else if (command.equals("H")) { + onHangup(); + } else if (command.startsWith("+CHLD=")) { + onCHLD(command); + } else if (command.equals("+CLCC")) { + onCLCC(); + } else if (command.startsWith("D")) { + onDial(command); + } else if (command.startsWith("+CMGS=")) { + onSMSSend(command); + } else { + boolean found = false; + + for (int j = 0; j < sDefaultResponses.length ; j++) { + if (command.equals(sDefaultResponses[j][0])) { + String r = sDefaultResponses[j][1]; + if (r != null) { + println(r); + } + found = true; + break; + } + } + + if (!found) { + throw new InterpreterEx ("ERROR"); + } + } + } + } + + + String[] + splitCommands(String line) throws InterpreterEx + { + if (!line.startsWith ("AT")) { + throw new InterpreterEx("ERROR"); + } + + if (line.length() == 2) { + // Just AT by itself + return new String[0]; + } + + String ret[] = new String[1]; + + //TODO fix case here too + ret[0] = line.substring(2); + + return ret; +/**** + try { + // i = 2 to skip over AT + for (int i = 2, s = line.length() ; i < s ; i++) { + // r"|([A-RT-Z]\d?)" # Normal commands eg ATA or I0 + // r"|(&[A-Z]\d*)" # & commands eg &C + // r"|(S\d+(=\d+)?)" # S registers + // r"((\+|%)\w+(\?|=([^;]+(;|$)))?)" # extended command eg +CREG=2 + + + } + } catch (StringIndexOutOfBoundsException ex) { + throw new InterpreterEx ("ERROR"); + } +***/ + } + + void + println (String s) + { + synchronized(this) { + try { + byte[] bytes = s.getBytes("US-ASCII"); + + //System.out.println("MI>> " + s); + + out.write(bytes); + out.write('\r'); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + void + print (String s) + { + synchronized(this) { + try { + byte[] bytes = s.getBytes("US-ASCII"); + + //System.out.println("MI>> " + s + " (no <cr>)"); + + out.write(bytes); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + } + + + public void + shutdown() + { + Looper looper = mHandlerThread.getLooper(); + if (looper != null) { + looper.quit(); + } + + try { + in.close(); + } catch (IOException ex) { + } + try { + out.close(); + } catch (IOException ex) { + } + } + + + static final String [][] sDefaultResponses = { + {"E0Q0V1", null}, + {"+CMEE=2", null}, + {"+CREG=2", null}, + {"+CGREG=2", null}, + {"+CCWA=1", null}, + {"+COPS=0", null}, + {"+CFUN=1", null}, + {"+CGMI", "+CGMI: Android Model AT Interpreter\r"}, + {"+CGMM", "+CGMM: Android Model AT Interpreter\r"}, + {"+CGMR", "+CGMR: 1.0\r"}, + {"+CGSN", "000000000000000\r"}, + {"+CIMI", "320720000000000\r"}, + {"+CSCS=?", "+CSCS: (\"HEX\",\"UCS2\")\r"}, + {"+CFUN?", "+CFUN: 1\r"}, + {"+COPS=3,0;+COPS?;+COPS=3,1;+COPS?;+COPS=3,2;+COPS?", + "+COPS: 0,0,\"Android\"\r" + + "+COPS: 0,1,\"Android\"\r" + + "+COPS: 0,2,\"310995\"\r"}, + {"+CREG?", "+CREG: 2,5, \"0113\", \"6614\"\r"}, + {"+CGREG?", "+CGREG: 2,0\r"}, + {"+CSQ", "+CSQ: 16,99\r"}, + {"+CNMI?", "+CNMI: 1,2,2,1,1\r"}, + {"+CLIR?", "+CLIR: 1,3\r"}, + {"%CPVWI=2", "%CPVWI: 0\r"}, + {"+CUSD=1,\"#646#\"", "+CUSD=0,\"You have used 23 minutes\"\r"}, + {"+CRSM=176,12258,0,0,10", "+CRSM: 144,0,981062200050259429F6\r"}, + {"+CRSM=192,12258,0,0,15", "+CRSM: 144,0,0000000A2FE204000FF55501020000\r"}, + + /* EF[ADN] */ + {"+CRSM=192,28474,0,0,15", "+CRSM: 144,0,0000005a6f3a040011f5220102011e\r"}, + {"+CRSM=178,28474,1,4,30", "+CRSM: 144,0,437573746f6d65722043617265ffffff07818100398799f7ffffffffffff\r"}, + {"+CRSM=178,28474,2,4,30", "+CRSM: 144,0,566f696365204d61696cffffffffffff07918150367742f3ffffffffffff\r"}, + {"+CRSM=178,28474,3,4,30", "+CRSM: 144,0,4164676a6dffffffffffffffffffffff0b918188551512c221436587ff01\r"}, + {"+CRSM=178,28474,4,4,30", "+CRSM: 144,0,810101c1ffffffffffffffffffffffff068114455245f8ffffffffffffff\r"}, + /* EF[EXT1] */ + {"+CRSM=192,28490,0,0,15", "+CRSM: 144,0,000000416f4a040011f5550102010d\r"}, + {"+CRSM=178,28490,1,4,13", "+CRSM: 144,0,0206092143658709ffffffffff\r"} + }; +} diff --git a/src/java/com/android/internal/telephony/test/SimulatedCommands.java b/src/java/com/android/internal/telephony/test/SimulatedCommands.java new file mode 100644 index 0000000..4f61509 --- /dev/null +++ b/src/java/com/android/internal/telephony/test/SimulatedCommands.java @@ -0,0 +1,1525 @@ +/* + * Copyright (C) 2006 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.internal.telephony.test; + +import android.os.AsyncResult; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.BaseCommands; +import com.android.internal.telephony.CommandException; +import com.android.internal.telephony.CommandsInterface; +import com.android.internal.telephony.DataCallState; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.UUSInfo; +import com.android.internal.telephony.gsm.CallFailCause; +import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; +import com.android.internal.telephony.gsm.SuppServiceNotification; + +import java.util.ArrayList; + +public final class SimulatedCommands extends BaseCommands + implements CommandsInterface, SimulatedRadioControl { + private final static String LOG_TAG = "SIM"; + + private enum SimLockState { + NONE, + REQUIRE_PIN, + REQUIRE_PUK, + SIM_PERM_LOCKED + } + + private enum SimFdnState { + NONE, + REQUIRE_PIN2, + REQUIRE_PUK2, + SIM_PERM_LOCKED + } + + private final static SimLockState INITIAL_LOCK_STATE = SimLockState.NONE; + private final static String DEFAULT_SIM_PIN_CODE = "1234"; + private final static String SIM_PUK_CODE = "12345678"; + private final static SimFdnState INITIAL_FDN_STATE = SimFdnState.NONE; + private final static String DEFAULT_SIM_PIN2_CODE = "5678"; + private final static String SIM_PUK2_CODE = "87654321"; + + //***** Instance Variables + + SimulatedGsmCallState simulatedCallState; + HandlerThread mHandlerThread; + SimLockState mSimLockedState; + boolean mSimLockEnabled; + int mPinUnlockAttempts; + int mPukUnlockAttempts; + String mPinCode; + SimFdnState mSimFdnEnabledState; + boolean mSimFdnEnabled; + int mPin2UnlockAttempts; + int mPuk2UnlockAttempts; + int mNetworkType; + String mPin2Code; + boolean mSsnNotifyOn = false; + + int pausedResponseCount; + ArrayList<Message> pausedResponses = new ArrayList<Message>(); + + int nextCallFailCause = CallFailCause.NORMAL_CLEARING; + + //***** Constructor + + public + SimulatedCommands() { + super(null); // Don't log statistics + mHandlerThread = new HandlerThread("SimulatedCommands"); + mHandlerThread.start(); + Looper looper = mHandlerThread.getLooper(); + + simulatedCallState = new SimulatedGsmCallState(looper); + + setRadioState(RadioState.RADIO_OFF); + mSimLockedState = INITIAL_LOCK_STATE; + mSimLockEnabled = (mSimLockedState != SimLockState.NONE); + mPinCode = DEFAULT_SIM_PIN_CODE; + mSimFdnEnabledState = INITIAL_FDN_STATE; + mSimFdnEnabled = (mSimFdnEnabledState != SimFdnState.NONE); + mPin2Code = DEFAULT_SIM_PIN2_CODE; + } + + //***** CommandsInterface implementation + + public void getIccCardStatus(Message result) { + unimplemented(result); + } + + public void supplyIccPin(String pin, Message result) { + if (mSimLockedState != SimLockState.REQUIRE_PIN) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin: wrong state, state=" + + mSimLockedState); + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + return; + } + + if (pin != null && pin.equals(mPinCode)) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin: success!"); + mPinUnlockAttempts = 0; + mSimLockedState = SimLockState.NONE; + mIccStatusChangedRegistrants.notifyRegistrants(); + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + mPinUnlockAttempts ++; + + Log.i(LOG_TAG, "[SimCmd] supplyIccPin: failed! attempt=" + + mPinUnlockAttempts); + if (mPinUnlockAttempts >= 3) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin: set state to REQUIRE_PUK"); + mSimLockedState = SimLockState.REQUIRE_PUK; + } + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void supplyIccPuk(String puk, String newPin, Message result) { + if (mSimLockedState != SimLockState.REQUIRE_PUK) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk: wrong state, state=" + + mSimLockedState); + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + return; + } + + if (puk != null && puk.equals(SIM_PUK_CODE)) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk: success!"); + mSimLockedState = SimLockState.NONE; + mPukUnlockAttempts = 0; + mIccStatusChangedRegistrants.notifyRegistrants(); + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + mPukUnlockAttempts ++; + + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk: failed! attempt=" + + mPukUnlockAttempts); + if (mPukUnlockAttempts >= 10) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk: set state to SIM_PERM_LOCKED"); + mSimLockedState = SimLockState.SIM_PERM_LOCKED; + } + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void supplyIccPin2(String pin2, Message result) { + if (mSimFdnEnabledState != SimFdnState.REQUIRE_PIN2) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin2: wrong state, state=" + + mSimFdnEnabledState); + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + return; + } + + if (pin2 != null && pin2.equals(mPin2Code)) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin2: success!"); + mPin2UnlockAttempts = 0; + mSimFdnEnabledState = SimFdnState.NONE; + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + mPin2UnlockAttempts ++; + + Log.i(LOG_TAG, "[SimCmd] supplyIccPin2: failed! attempt=" + + mPin2UnlockAttempts); + if (mPin2UnlockAttempts >= 3) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPin2: set state to REQUIRE_PUK2"); + mSimFdnEnabledState = SimFdnState.REQUIRE_PUK2; + } + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void supplyIccPuk2(String puk2, String newPin2, Message result) { + if (mSimFdnEnabledState != SimFdnState.REQUIRE_PUK2) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk2: wrong state, state=" + + mSimLockedState); + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + return; + } + + if (puk2 != null && puk2.equals(SIM_PUK2_CODE)) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk2: success!"); + mSimFdnEnabledState = SimFdnState.NONE; + mPuk2UnlockAttempts = 0; + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + mPuk2UnlockAttempts ++; + + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk2: failed! attempt=" + + mPuk2UnlockAttempts); + if (mPuk2UnlockAttempts >= 10) { + Log.i(LOG_TAG, "[SimCmd] supplyIccPuk2: set state to SIM_PERM_LOCKED"); + mSimFdnEnabledState = SimFdnState.SIM_PERM_LOCKED; + } + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void changeIccPin(String oldPin, String newPin, Message result) { + if (oldPin != null && oldPin.equals(mPinCode)) { + mPinCode = newPin; + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + Log.i(LOG_TAG, "[SimCmd] changeIccPin: pin failed!"); + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void changeIccPin2(String oldPin2, String newPin2, Message result) { + if (oldPin2 != null && oldPin2.equals(mPin2Code)) { + mPin2Code = newPin2; + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + Log.i(LOG_TAG, "[SimCmd] changeIccPin2: pin2 failed!"); + + CommandException ex = new CommandException( + CommandException.Error.PASSWORD_INCORRECT); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + } + + public void + changeBarringPassword(String facility, String oldPwd, String newPwd, Message result) { + unimplemented(result); + } + + public void + setSuppServiceNotifications(boolean enable, Message result) { + resultSuccess(result, null); + + if (enable && mSsnNotifyOn) { + Log.w(LOG_TAG, "Supp Service Notifications already enabled!"); + } + + mSsnNotifyOn = enable; + } + + @Override + public void queryFacilityLock(String facility, String pin, + int serviceClass, Message result) { + queryFacilityLockForApp(facility, pin, serviceClass, null, result); + } + + @Override + public void queryFacilityLockForApp(String facility, String pin, int serviceClass, + String appId, Message result) { + if (facility != null && facility.equals(CommandsInterface.CB_FACILITY_BA_SIM)) { + if (result != null) { + int[] r = new int[1]; + r[0] = (mSimLockEnabled ? 1 : 0); + Log.i(LOG_TAG, "[SimCmd] queryFacilityLock: SIM is " + + (r[0] == 0 ? "unlocked" : "locked")); + AsyncResult.forMessage(result, r, null); + result.sendToTarget(); + } + return; + } else if (facility != null && facility.equals(CommandsInterface.CB_FACILITY_BA_FD)) { + if (result != null) { + int[] r = new int[1]; + r[0] = (mSimFdnEnabled ? 1 : 0); + Log.i(LOG_TAG, "[SimCmd] queryFacilityLock: FDN is " + + (r[0] == 0 ? "disabled" : "enabled")); + AsyncResult.forMessage(result, r, null); + result.sendToTarget(); + } + return; + } + + unimplemented(result); + } + + @Override + public void setFacilityLock(String facility, boolean lockEnabled, String pin, int serviceClass, + Message result) { + setFacilityLockForApp(facility, lockEnabled, pin, serviceClass, null, result); + } + + @Override + public void setFacilityLockForApp(String facility, boolean lockEnabled, + String pin, int serviceClass, String appId, + Message result) { + if (facility != null && + facility.equals(CommandsInterface.CB_FACILITY_BA_SIM)) { + if (pin != null && pin.equals(mPinCode)) { + Log.i(LOG_TAG, "[SimCmd] setFacilityLock: pin is valid"); + mSimLockEnabled = lockEnabled; + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + Log.i(LOG_TAG, "[SimCmd] setFacilityLock: pin failed!"); + + CommandException ex = new CommandException( + CommandException.Error.GENERIC_FAILURE); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + + return; + } else if (facility != null && + facility.equals(CommandsInterface.CB_FACILITY_BA_FD)) { + if (pin != null && pin.equals(mPin2Code)) { + Log.i(LOG_TAG, "[SimCmd] setFacilityLock: pin2 is valid"); + mSimFdnEnabled = lockEnabled; + + if (result != null) { + AsyncResult.forMessage(result, null, null); + result.sendToTarget(); + } + + return; + } + + if (result != null) { + Log.i(LOG_TAG, "[SimCmd] setFacilityLock: pin2 failed!"); + + CommandException ex = new CommandException( + CommandException.Error.GENERIC_FAILURE); + AsyncResult.forMessage(result, null, ex); + result.sendToTarget(); + } + + return; + } + + unimplemented(result); + } + + public void supplyNetworkDepersonalization(String netpin, Message result) { + unimplemented(result); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result contains a List of DriverCall + * The ar.result List is sorted by DriverCall.index + */ + public void getCurrentCalls (Message result) { + if ((mState == RadioState.RADIO_ON) && !isSimLocked()) { + //Log.i("GSM", "[SimCmds] getCurrentCalls"); + resultSuccess(result, simulatedCallState.getDriverCalls()); + } else { + //Log.i("GSM", "[SimCmds] getCurrentCalls: RADIO_OFF or SIM not ready!"); + resultFail(result, + new CommandException( + CommandException.Error.RADIO_NOT_AVAILABLE)); + } + } + + /** + * @deprecated + */ + public void getPDPContextList(Message result) { + getDataCallList(result); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result contains a List of DataCallState + */ + public void getDataCallList(Message result) { + resultSuccess(result, new ArrayList<DataCallState>(0)); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + * + * CLIR_DEFAULT == on "use subscription default value" + * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation) + * CLIR_INVOCATION == on "CLIR invocation" (restrict CLI presentation) + */ + public void dial (String address, int clirMode, Message result) { + simulatedCallState.onDial(address); + + resultSuccess(result, null); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + * + * CLIR_DEFAULT == on "use subscription default value" + * CLIR_SUPPRESSION == on "CLIR suppression" (allow CLI presentation) + * CLIR_INVOCATION == on "CLIR invocation" (restrict CLI presentation) + */ + public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) { + simulatedCallState.onDial(address); + + resultSuccess(result, null); + } + + public void getIMSI(Message result) { + getIMSIForApp(null, result); + } + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is String containing IMSI on success + */ + public void getIMSIForApp(String aid, Message result) { + resultSuccess(result, "012345678901234"); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is String containing IMEI on success + */ + public void getIMEI(Message result) { + resultSuccess(result, "012345678901234"); + } + + /** + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is String containing IMEISV on success + */ + public void getIMEISV(Message result) { + resultSuccess(result, "99"); + } + + /** + * Hang up one individual connection. + * returned message + * retMsg.obj = AsyncResult ar + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + * + * 3GPP 22.030 6.5.5 + * "Releases a specific active call X" + */ + public void hangupConnection (int gsmIndex, Message result) { + boolean success; + + success = simulatedCallState.onChld('1', (char)('0'+gsmIndex)); + + if (!success){ + Log.i("GSM", "[SimCmd] hangupConnection: resultFail"); + resultFail(result, new RuntimeException("Hangup Error")); + } else { + Log.i("GSM", "[SimCmd] hangupConnection: resultSuccess"); + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Releases all held calls or sets User Determined User Busy (UDUB) + * for a waiting call." + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void hangupWaitingOrBackground (Message result) { + boolean success; + + success = simulatedCallState.onChld('0', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Releases all active calls (if any exist) and accepts + * the other (held or waiting) call." + * + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void hangupForegroundResumeBackground (Message result) { + boolean success; + + success = simulatedCallState.onChld('1', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Places all active calls (if any exist) on hold and accepts + * the other (held or waiting) call." + * + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void switchWaitingOrHoldingAndActive (Message result) { + boolean success; + + success = simulatedCallState.onChld('2', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Adds a held call to the conversation" + * + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void conference (Message result) { + boolean success; + + success = simulatedCallState.onChld('3', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Connects the two calls and disconnects the subscriber from both calls" + * + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void explicitCallTransfer (Message result) { + boolean success; + + success = simulatedCallState.onChld('4', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * 3GPP 22.030 6.5.5 + * "Places all active calls on hold except call X with which + * communication shall be supported." + */ + public void separateConnection (int gsmIndex, Message result) { + boolean success; + + char ch = (char)(gsmIndex + '0'); + success = simulatedCallState.onChld('2', ch); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void acceptCall (Message result) { + boolean success; + + success = simulatedCallState.onAnswer(); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * also known as UDUB + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void rejectCall (Message result) { + boolean success; + + success = simulatedCallState.onChld('0', '\0'); + + if (!success){ + resultFail(result, new RuntimeException("Hangup Error")); + } else { + resultSuccess(result, null); + } + } + + /** + * cause code returned as Integer in Message.obj.response + * Returns integer cause code defined in TS 24.008 + * Annex H or closest approximation. + * Most significant codes: + * - Any defined in 22.001 F.4 (for generating busy/congestion) + * - Cause 68: ACM >= ACMMax + */ + public void getLastCallFailCause (Message result) { + int[] ret = new int[1]; + + ret[0] = nextCallFailCause; + resultSuccess(result, ret); + } + + /** + * @deprecated + */ + public void getLastPdpFailCause (Message result) { + unimplemented(result); + } + + public void getLastDataCallFailCause(Message result) { + // + unimplemented(result); + } + + public void setMute (boolean enableMute, Message result) {unimplemented(result);} + + public void getMute (Message result) {unimplemented(result);} + + /** + * response.obj is an AsyncResult + * response.obj.result is an int[2] + * response.obj.result[0] is received signal strength (0-31, 99) + * response.obj.result[1] is bit error rate (0-7, 99) + * as defined in TS 27.007 8.5 + */ + public void getSignalStrength (Message result) { + int ret[] = new int[2]; + + ret[0] = 23; + ret[1] = 0; + + resultSuccess(result, ret); + } + + /** + * Assign a specified band for RF configuration. + * + * @param bandMode one of BM_*_BAND + * @param result is callback message + */ + public void setBandMode (int bandMode, Message result) { + resultSuccess(result, null); + } + + /** + * Query the list of band mode supported by RF. + * + * @param result is callback message + * ((AsyncResult)response.obj).result is an int[] with every + * element representing one available BM_*_BAND + */ + public void queryAvailableBandMode (Message result) { + int ret[] = new int [4]; + + ret[0] = 4; + ret[1] = Phone.BM_US_BAND; + ret[2] = Phone.BM_JPN_BAND; + ret[3] = Phone.BM_AUS_BAND; + + resultSuccess(result, ret); + } + + /** + * {@inheritDoc} + */ + public void sendTerminalResponse(String contents, Message response) { + resultSuccess(response, null); + } + + /** + * {@inheritDoc} + */ + public void sendEnvelope(String contents, Message response) { + resultSuccess(response, null); + } + + /** + * {@inheritDoc} + */ + public void sendEnvelopeWithStatus(String contents, Message response) { + resultSuccess(response, null); + } + + /** + * {@inheritDoc} + */ + public void handleCallSetupRequestFromSim( + boolean accept, Message response) { + resultSuccess(response, null); + } + + /** + * response.obj.result is an String[14] + * See ril.h for details + * + * Please note that registration state 4 ("unknown") is treated + * as "out of service" above + */ + public void getVoiceRegistrationState (Message result) { + String ret[] = new String[14]; + + ret[0] = "5"; // registered roam + ret[1] = null; + ret[2] = null; + ret[3] = null; + ret[4] = null; + ret[5] = null; + ret[6] = null; + ret[7] = null; + ret[8] = null; + ret[9] = null; + ret[10] = null; + ret[11] = null; + ret[12] = null; + ret[13] = null; + + resultSuccess(result, ret); + } + + /** + * response.obj.result is an String[4] + * response.obj.result[0] is registration state 0-5 from TS 27.007 7.2 + * response.obj.result[1] is LAC if registered or NULL if not + * response.obj.result[2] is CID if registered or NULL if not + * response.obj.result[3] indicates the available radio technology, where: + * 0 == unknown + * 1 == GPRS only + * 2 == EDGE + * 3 == UMTS + * + * valid LAC are 0x0000 - 0xffff + * valid CID are 0x00000000 - 0xffffffff + * + * Please note that registration state 4 ("unknown") is treated + * as "out of service" in the Android telephony system + */ + public void getDataRegistrationState (Message result) { + String ret[] = new String[4]; + + ret[0] = "5"; // registered roam + ret[1] = null; + ret[2] = null; + ret[3] = "2"; + + resultSuccess(result, ret); + } + + /** + * response.obj.result is a String[3] + * response.obj.result[0] is long alpha or null if unregistered + * response.obj.result[1] is short alpha or null if unregistered + * response.obj.result[2] is numeric or null if unregistered + */ + public void getOperator(Message result) { + String[] ret = new String[3]; + + ret[0] = "El Telco Loco"; + ret[1] = "Telco Loco"; + ret[2] = "001001"; + + resultSuccess(result, ret); + } + + /** + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void sendDtmf(char c, Message result) { + resultSuccess(result, null); + } + + /** + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void startDtmf(char c, Message result) { + resultSuccess(result, null); + } + + /** + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void stopDtmf(Message result) { + resultSuccess(result, null); + } + + /** + * ar.exception carries exception on failure + * ar.userObject contains the original value of result.obj + * ar.result is null on success and failure + */ + public void sendBurstDtmf(String dtmfString, int on, int off, Message result) { + resultSuccess(result, null); + } + + /** + * smscPDU is smsc address in PDU form GSM BCD format prefixed + * by a length byte (as expected by TS 27.005) or NULL for default SMSC + * pdu is SMS in PDU format as an ASCII hex string + * less the SMSC address + */ + public void sendSMS (String smscPDU, String pdu, Message result) {unimplemented(result);} + + public void deleteSmsOnSim(int index, Message response) { + Log.d(LOG_TAG, "Delete message at index " + index); + unimplemented(response); + } + + public void deleteSmsOnRuim(int index, Message response) { + Log.d(LOG_TAG, "Delete RUIM message at index " + index); + unimplemented(response); + } + + public void writeSmsToSim(int status, String smsc, String pdu, Message response) { + Log.d(LOG_TAG, "Write SMS to SIM with status " + status); + unimplemented(response); + } + + public void writeSmsToRuim(int status, String pdu, Message response) { + Log.d(LOG_TAG, "Write SMS to RUIM with status " + status); + unimplemented(response); + } + + public void setupDataCall(String radioTechnology, String profile, + String apn, String user, String password, String authType, + String protocol, Message result) { + unimplemented(result); + } + + public void deactivateDataCall(int cid, int reason, Message result) {unimplemented(result);} + + public void setPreferredNetworkType(int networkType , Message result) { + mNetworkType = networkType; + resultSuccess(result, null); + } + + public void getPreferredNetworkType(Message result) { + int ret[] = new int[1]; + + ret[0] = mNetworkType; + resultSuccess(result, ret); + } + + public void getNeighboringCids(Message result) { + int ret[] = new int[7]; + + ret[0] = 6; + for (int i = 1; i<7; i++) { + ret[i] = i; + } + resultSuccess(result, ret); + } + + public void setLocationUpdates(boolean enable, Message response) { + unimplemented(response); + } + + public void getSmscAddress(Message result) { + unimplemented(result); + } + + public void setSmscAddress(String address, Message result) { + unimplemented(result); + } + + public void reportSmsMemoryStatus(boolean available, Message result) { + unimplemented(result); + } + + public void reportStkServiceIsRunning(Message result) { + resultSuccess(result, null); + } + + @Override + public void getCdmaSubscriptionSource(Message result) { + unimplemented(result); + } + + private boolean isSimLocked() { + if (mSimLockedState != SimLockState.NONE) { + return true; + } + return false; + } + + public void setRadioPower(boolean on, Message result) { + if(on) { + setRadioState(RadioState.RADIO_ON); + } else { + setRadioState(RadioState.RADIO_OFF); + } + } + + + public void acknowledgeLastIncomingGsmSms(boolean success, int cause, Message result) { + unimplemented(result); + } + + public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message result) { + unimplemented(result); + } + + public void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, + Message result) { + unimplemented(result); + } + + public void iccIO(int command, int fileid, String path, int p1, int p2, int p3, String data, + String pin2, Message response) { + iccIOForApp(command, fileid, path, p1, p2, p3, data,pin2, null, response); + } + + /** + * parameters equivalent to 27.007 AT+CRSM command + * response.obj will be an AsyncResult + * response.obj.userObj will be a SimIoResult on success + */ + public void iccIOForApp (int command, int fileid, String path, int p1, int p2, + int p3, String data, String pin2, String aid, Message result) { + unimplemented(result); + } + + /** + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * 1 for "CLIP is provisioned", and 0 for "CLIP is not provisioned". + * + * @param response is callback message + */ + public void queryCLIP(Message response) { unimplemented(response); } + + + /** + * response.obj will be a an int[2] + * + * response.obj[0] will be TS 27.007 +CLIR parameter 'n' + * 0 presentation indicator is used according to the subscription of the CLIR service + * 1 CLIR invocation + * 2 CLIR suppression + * + * response.obj[1] will be TS 27.007 +CLIR parameter 'm' + * 0 CLIR not provisioned + * 1 CLIR provisioned in permanent mode + * 2 unknown (e.g. no network, etc.) + * 3 CLIR temporary mode presentation restricted + * 4 CLIR temporary mode presentation allowed + */ + + public void getCLIR(Message result) {unimplemented(result);} + + /** + * clirMode is one of the CLIR_* constants above + * + * response.obj is null + */ + + public void setCLIR(int clirMode, Message result) {unimplemented(result);} + + /** + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * 0 for disabled, 1 for enabled. + * + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + + public void queryCallWaiting(int serviceClass, Message response) { + unimplemented(response); + } + + /** + * @param enable is true to enable, false to disable + * @param serviceClass is a sum of SERVICE_CLASS_* + * @param response is callback message + */ + + public void setCallWaiting(boolean enable, int serviceClass, + Message response) { + unimplemented(response); + } + + /** + * @param action is one of CF_ACTION_* + * @param cfReason is one of CF_REASON_* + * @param serviceClass is a sum of SERVICE_CLASSS_* + */ + public void setCallForward(int action, int cfReason, int serviceClass, + String number, int timeSeconds, Message result) {unimplemented(result);} + + /** + * cfReason is one of CF_REASON_* + * + * ((AsyncResult)response.obj).result will be an array of + * CallForwardInfo's + * + * An array of length 0 means "disabled for all codes" + */ + public void queryCallForwardStatus(int cfReason, int serviceClass, + String number, Message result) {unimplemented(result);} + + public void setNetworkSelectionModeAutomatic(Message result) {unimplemented(result);} + public void exitEmergencyCallbackMode(Message result) {unimplemented(result);} + public void setNetworkSelectionModeManual( + String operatorNumeric, Message result) {unimplemented(result);} + + /** + * Queries whether the current network selection mode is automatic + * or manual + * + * ((AsyncResult)response.obj).result is an int[] with element [0] being + * a 0 for automatic selection and a 1 for manual selection + */ + + public void getNetworkSelectionMode(Message result) { + int ret[] = new int[1]; + + ret[0] = 0; + resultSuccess(result, ret); + } + + /** + * Queries the currently available networks + * + * ((AsyncResult)response.obj).result is a List of NetworkInfo objects + */ + public void getAvailableNetworks(Message result) {unimplemented(result);} + + public void getBasebandVersion (Message result) { + resultSuccess(result, "SimulatedCommands"); + } + + /** + * Simulates an incoming USSD message + * @param statusCode Status code string. See <code>setOnUSSD</code> + * in CommandsInterface.java + * @param message Message text to send or null if none + */ + public void triggerIncomingUssd(String statusCode, String message) { + if (mUSSDRegistrant != null) { + String[] result = {statusCode, message}; + mUSSDRegistrant.notifyResult(result); + } + } + + + public void sendUSSD (String ussdString, Message result) { + + // We simulate this particular sequence + if (ussdString.equals("#646#")) { + resultSuccess(result, null); + + // 0 == USSD-Notify + triggerIncomingUssd("0", "You have NNN minutes remaining."); + } else { + resultSuccess(result, null); + + triggerIncomingUssd("0", "All Done"); + } + } + + // inherited javadoc suffices + public void cancelPendingUssd (Message response) { + resultSuccess(response, null); + } + + + public void resetRadio(Message result) { + unimplemented(result); + } + + public void invokeOemRilRequestRaw(byte[] data, Message response) { + // Just echo back data + if (response != null) { + AsyncResult.forMessage(response).result = data; + response.sendToTarget(); + } + } + + public void invokeOemRilRequestStrings(String[] strings, Message response) { + // Just echo back data + if (response != null) { + AsyncResult.forMessage(response).result = strings; + response.sendToTarget(); + } + } + + //***** SimulatedRadioControl + + + /** Start the simulated phone ringing */ + public void + triggerRing(String number) { + simulatedCallState.triggerRing(number); + mCallStateRegistrants.notifyRegistrants(); + } + + public void + progressConnectingCallState() { + simulatedCallState.progressConnectingCallState(); + mCallStateRegistrants.notifyRegistrants(); + } + + /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */ + public void + progressConnectingToActive() { + simulatedCallState.progressConnectingToActive(); + mCallStateRegistrants.notifyRegistrants(); + } + + /** automatically progress mobile originated calls to ACTIVE. + * default to true + */ + public void + setAutoProgressConnectingCall(boolean b) { + simulatedCallState.setAutoProgressConnectingCall(b); + } + + public void + setNextDialFailImmediately(boolean b) { + simulatedCallState.setNextDialFailImmediately(b); + } + + public void + setNextCallFailCause(int gsmCause) { + nextCallFailCause = gsmCause; + } + + public void + triggerHangupForeground() { + simulatedCallState.triggerHangupForeground(); + mCallStateRegistrants.notifyRegistrants(); + } + + /** hangup holding calls */ + public void + triggerHangupBackground() { + simulatedCallState.triggerHangupBackground(); + mCallStateRegistrants.notifyRegistrants(); + } + + public void triggerSsn(int type, int code) { + SuppServiceNotification not = new SuppServiceNotification(); + not.notificationType = type; + not.code = code; + mSsnRegistrant.notifyRegistrant(new AsyncResult(null, not, null)); + } + + public void + shutdown() { + setRadioState(RadioState.RADIO_UNAVAILABLE); + Looper looper = mHandlerThread.getLooper(); + if (looper != null) { + looper.quit(); + } + } + + /** hangup all */ + + public void + triggerHangupAll() { + simulatedCallState.triggerHangupAll(); + mCallStateRegistrants.notifyRegistrants(); + } + + public void + triggerIncomingSMS(String message) { + //TODO + } + + public void + pauseResponses() { + pausedResponseCount++; + } + + public void + resumeResponses() { + pausedResponseCount--; + + if (pausedResponseCount == 0) { + for (int i = 0, s = pausedResponses.size(); i < s ; i++) { + pausedResponses.get(i).sendToTarget(); + } + pausedResponses.clear(); + } else { + Log.e("GSM", "SimulatedCommands.resumeResponses < 0"); + } + } + + //***** Private Methods + + private void unimplemented(Message result) { + if (result != null) { + AsyncResult.forMessage(result).exception + = new RuntimeException("Unimplemented"); + + if (pausedResponseCount > 0) { + pausedResponses.add(result); + } else { + result.sendToTarget(); + } + } + } + + private void resultSuccess(Message result, Object ret) { + if (result != null) { + AsyncResult.forMessage(result).result = ret; + if (pausedResponseCount > 0) { + pausedResponses.add(result); + } else { + result.sendToTarget(); + } + } + } + + private void resultFail(Message result, Throwable tr) { + if (result != null) { + AsyncResult.forMessage(result).exception = tr; + if (pausedResponseCount > 0) { + pausedResponses.add(result); + } else { + result.sendToTarget(); + } + } + } + + // ***** Methods for CDMA support + public void + getDeviceIdentity(Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + public void + getCDMASubscription(Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + public void + setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + public void queryCdmaRoamingPreference(Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + public void + setPhoneType(int phoneType) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + } + + public void getPreferredVoicePrivacy(Message result) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(result); + } + + public void setPreferredVoicePrivacy(boolean enable, Message result) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(result); + } + + /** + * Set the TTY mode + * + * @param ttyMode is one of the following: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * @param response is callback message + */ + public void setTTYMode(int ttyMode, Message response) { + Log.w(LOG_TAG, "Not implemented in SimulatedCommands"); + unimplemented(response); + } + + /** + * Query the TTY mode + * (AsyncResult)response.obj).result is an int[] with element [0] set to + * tty mode: + * - {@link com.android.internal.telephony.Phone#TTY_MODE_OFF} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_FULL} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_HCO} + * - {@link com.android.internal.telephony.Phone#TTY_MODE_VCO} + * @param response is callback message + */ + public void queryTTYMode(Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + /** + * {@inheritDoc} + */ + public void sendCDMAFeatureCode(String FeatureCode, Message response) { + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + unimplemented(response); + } + + /** + * {@inheritDoc} + */ + public void sendCdmaSms(byte[] pdu, Message response){ + Log.w(LOG_TAG, "CDMA not implemented in SimulatedCommands"); + } + + public void setCdmaBroadcastActivation(boolean activate, Message response) { + unimplemented(response); + + } + + public void getCdmaBroadcastConfig(Message response) { + unimplemented(response); + + } + + public void setCdmaBroadcastConfig(int[] configValuesArray, Message response) { + unimplemented(response); + + } + + public void forceDataDormancy(Message response) { + unimplemented(response); + } + + + public void setGsmBroadcastActivation(boolean activate, Message response) { + unimplemented(response); + } + + + public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) { + unimplemented(response); + } + + public void getGsmBroadcastConfig(Message response) { + unimplemented(response); + } + + @Override + public void supplyIccPinForApp(String pin, String aid, Message response) { + unimplemented(response); + } + + @Override + public void supplyIccPukForApp(String puk, String newPin, String aid, Message response) { + unimplemented(response); + } + + @Override + public void supplyIccPin2ForApp(String pin2, String aid, Message response) { + unimplemented(response); + } + + @Override + public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message response) { + unimplemented(response); + } + + @Override + public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message response) { + unimplemented(response); + } + + @Override + public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, + Message response) { + unimplemented(response); + } + + public void requestIsimAuthentication(String nonce, Message response) { + unimplemented(response); + } + + public void getVoiceRadioTechnology(Message response) { + unimplemented(response); + } +} diff --git a/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java b/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java new file mode 100644 index 0000000..c6c301d --- /dev/null +++ b/src/java/com/android/internal/telephony/test/SimulatedGsmCallState.java @@ -0,0 +1,809 @@ +/* + * Copyright (C) 2006 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.internal.telephony.test; + +import android.os.Looper; +import android.os.Message; +import android.os.Handler; +import android.telephony.PhoneNumberUtils; +import com.android.internal.telephony.ATParseEx; +import com.android.internal.telephony.DriverCall; +import java.util.List; +import java.util.ArrayList; + +import android.util.Log; + +class CallInfo { + enum State { + ACTIVE(0), + HOLDING(1), + DIALING(2), // MO call only + ALERTING(3), // MO call only + INCOMING(4), // MT call only + WAITING(5); // MT call only + + State (int value) {this.value = value;} + + private final int value; + public int value() {return value;}; + }; + + boolean isMT; + State state; + boolean isMpty; + String number; + int TOA; + + CallInfo (boolean isMT, State state, boolean isMpty, String number) { + this.isMT = isMT; + this.state = state; + this.isMpty = isMpty; + this.number = number; + + if (number.length() > 0 && number.charAt(0) == '+') { + TOA = PhoneNumberUtils.TOA_International; + } else { + TOA = PhoneNumberUtils.TOA_Unknown; + } + } + + static CallInfo + createOutgoingCall(String number) { + return new CallInfo (false, State.DIALING, false, number); + } + + static CallInfo + createIncomingCall(String number) { + return new CallInfo (true, State.INCOMING, false, number); + } + + String + toCLCCLine(int index) { + return + "+CLCC: " + + index + "," + (isMT ? "1" : "0") +"," + + state.value() + ",0," + (isMpty ? "1" : "0") + + ",\"" + number + "\"," + TOA; + } + + DriverCall + toDriverCall(int index) { + DriverCall ret; + + ret = new DriverCall(); + + ret.index = index; + ret.isMT = isMT; + + try { + ret.state = DriverCall.stateFromCLCC(state.value()); + } catch (ATParseEx ex) { + throw new RuntimeException("should never happen", ex); + } + + ret.isMpty = isMpty; + ret.number = number; + ret.TOA = TOA; + ret.isVoice = true; + ret.als = 0; + + return ret; + } + + + boolean + isActiveOrHeld() { + return state == State.ACTIVE || state == State.HOLDING; + } + + boolean + isConnecting() { + return state == State.DIALING || state == State.ALERTING; + } + + boolean + isRinging() { + return state == State.INCOMING || state == State.WAITING; + } + +} + +class InvalidStateEx extends Exception { + InvalidStateEx() { + + } +} + + +class SimulatedGsmCallState extends Handler { + //***** Instance Variables + + CallInfo calls[] = new CallInfo[MAX_CALLS]; + + private boolean autoProgressConnecting = true; + private boolean nextDialFailImmediately; + + + //***** Event Constants + + static final int EVENT_PROGRESS_CALL_STATE = 1; + + //***** Constants + + static final int MAX_CALLS = 7; + /** number of msec between dialing -> alerting and alerting->active */ + static final int CONNECTING_PAUSE_MSEC = 5 * 100; + + + //***** Overridden from Handler + + public SimulatedGsmCallState(Looper looper) { + super(looper); + } + + public void + handleMessage(Message msg) { + synchronized(this) { switch (msg.what) { + // PLEASE REMEMBER + // calls may have hung up by the time delayed events happen + + case EVENT_PROGRESS_CALL_STATE: + progressConnectingCallState(); + break; + }} + } + + //***** Public Methods + + /** + * Start the simulated phone ringing + * true if succeeded, false if failed + */ + public boolean + triggerRing(String number) { + synchronized (this) { + int empty = -1; + boolean isCallWaiting = false; + + // ensure there aren't already calls INCOMING or WAITING + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call == null && empty < 0) { + empty = i; + } else if (call != null + && (call.state == CallInfo.State.INCOMING + || call.state == CallInfo.State.WAITING) + ) { + Log.w("ModelInterpreter", + "triggerRing failed; phone already ringing"); + return false; + } else if (call != null) { + isCallWaiting = true; + } + } + + if (empty < 0 ) { + Log.w("ModelInterpreter", "triggerRing failed; all full"); + return false; + } + + calls[empty] = CallInfo.createIncomingCall( + PhoneNumberUtils.extractNetworkPortion(number)); + + if (isCallWaiting) { + calls[empty].state = CallInfo.State.WAITING; + } + + } + return true; + } + + /** If a call is DIALING or ALERTING, progress it to the next state */ + public void + progressConnectingCallState() { + synchronized (this) { + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null && call.state == CallInfo.State.DIALING) { + call.state = CallInfo.State.ALERTING; + + if (autoProgressConnecting) { + sendMessageDelayed( + obtainMessage(EVENT_PROGRESS_CALL_STATE, call), + CONNECTING_PAUSE_MSEC); + } + break; + } else if (call != null + && call.state == CallInfo.State.ALERTING + ) { + call.state = CallInfo.State.ACTIVE; + break; + } + } + } + } + + /** If a call is DIALING or ALERTING, progress it all the way to ACTIVE */ + public void + progressConnectingToActive() { + synchronized (this) { + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null && (call.state == CallInfo.State.DIALING + || call.state == CallInfo.State.ALERTING) + ) { + call.state = CallInfo.State.ACTIVE; + break; + } + } + } + } + + /** automatically progress mobile originated calls to ACTIVE. + * default to true + */ + public void + setAutoProgressConnectingCall(boolean b) { + autoProgressConnecting = b; + } + + public void + setNextDialFailImmediately(boolean b) { + nextDialFailImmediately = b; + } + + /** + * hangup ringing, dialing, or active calls + * returns true if call was hung up, false if not + */ + public boolean + triggerHangupForeground() { + synchronized (this) { + boolean found; + + found = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null + && (call.state == CallInfo.State.INCOMING + || call.state == CallInfo.State.WAITING) + ) { + calls[i] = null; + found = true; + } + } + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null + && (call.state == CallInfo.State.DIALING + || call.state == CallInfo.State.ACTIVE + || call.state == CallInfo.State.ALERTING) + ) { + calls[i] = null; + found = true; + } + } + return found; + } + } + + /** + * hangup holding calls + * returns true if call was hung up, false if not + */ + public boolean + triggerHangupBackground() { + synchronized (this) { + boolean found = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null && call.state == CallInfo.State.HOLDING) { + calls[i] = null; + found = true; + } + } + + return found; + } + } + + /** + * hangup all + * returns true if call was hung up, false if not + */ + public boolean + triggerHangupAll() { + synchronized(this) { + boolean found = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (calls[i] != null) { + found = true; + } + + calls[i] = null; + } + + return found; + } + } + + public boolean + onAnswer() { + synchronized (this) { + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null + && (call.state == CallInfo.State.INCOMING + || call.state == CallInfo.State.WAITING) + ) { + return switchActiveAndHeldOrWaiting(); + } + } + } + + return false; + } + + public boolean + onHangup() { + boolean found = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null && call.state != CallInfo.State.WAITING) { + calls[i] = null; + found = true; + } + } + + return found; + } + + public boolean + onChld(char c0, char c1) { + boolean ret; + int callIndex = 0; + + if (c1 != 0) { + callIndex = c1 - '1'; + + if (callIndex < 0 || callIndex >= calls.length) { + return false; + } + } + + switch (c0) { + case '0': + ret = releaseHeldOrUDUB(); + break; + case '1': + if (c1 <= 0) { + ret = releaseActiveAcceptHeldOrWaiting(); + } else { + if (calls[callIndex] == null) { + ret = false; + } else { + calls[callIndex] = null; + ret = true; + } + } + break; + case '2': + if (c1 <= 0) { + ret = switchActiveAndHeldOrWaiting(); + } else { + ret = separateCall(callIndex); + } + break; + case '3': + ret = conference(); + break; + case '4': + ret = explicitCallTransfer(); + break; + case '5': + if (true) { //just so javac doesnt complain about break + //CCBS not impled + ret = false; + } + break; + default: + ret = false; + + } + + return ret; + } + + public boolean + releaseHeldOrUDUB() { + boolean found = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.isRinging()) { + found = true; + calls[i] = null; + break; + } + } + + if (!found) { + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.state == CallInfo.State.HOLDING) { + found = true; + calls[i] = null; + // don't stop...there may be more than one + } + } + } + + return true; + } + + + public boolean + releaseActiveAcceptHeldOrWaiting() { + boolean foundHeld = false; + boolean foundActive = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.state == CallInfo.State.ACTIVE) { + calls[i] = null; + foundActive = true; + } + } + + if (!foundActive) { + // FIXME this may not actually be how most basebands react + // CHLD=1 may not hang up dialing/alerting calls + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null + && (c.state == CallInfo.State.DIALING + || c.state == CallInfo.State.ALERTING) + ) { + calls[i] = null; + foundActive = true; + } + } + } + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.state == CallInfo.State.HOLDING) { + c.state = CallInfo.State.ACTIVE; + foundHeld = true; + } + } + + if (foundHeld) { + return true; + } + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.isRinging()) { + c.state = CallInfo.State.ACTIVE; + return true; + } + } + + return true; + } + + public boolean + switchActiveAndHeldOrWaiting() { + boolean hasHeld = false; + + // first, are there held calls? + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null && c.state == CallInfo.State.HOLDING) { + hasHeld = true; + break; + } + } + + // Now, switch + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + if (c.state == CallInfo.State.ACTIVE) { + c.state = CallInfo.State.HOLDING; + } else if (c.state == CallInfo.State.HOLDING) { + c.state = CallInfo.State.ACTIVE; + } else if (!hasHeld && c.isRinging()) { + c.state = CallInfo.State.ACTIVE; + } + } + } + + return true; + } + + + public boolean + separateCall(int index) { + try { + CallInfo c; + + c = calls[index]; + + if (c == null || c.isConnecting() || countActiveLines() != 1) { + return false; + } + + c.state = CallInfo.State.ACTIVE; + c.isMpty = false; + + for (int i = 0 ; i < calls.length ; i++) { + int countHeld=0, lastHeld=0; + + if (i != index) { + CallInfo cb = calls[i]; + + if (cb != null && cb.state == CallInfo.State.ACTIVE) { + cb.state = CallInfo.State.HOLDING; + countHeld++; + lastHeld = i; + } + } + + if (countHeld == 1) { + // if there's only one left, clear the MPTY flag + calls[lastHeld].isMpty = false; + } + } + + return true; + } catch (InvalidStateEx ex) { + return false; + } + } + + + + public boolean + conference() { + int countCalls = 0; + + // if there's connecting calls, we can't do this yet + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + countCalls++; + + if (c.isConnecting()) { + return false; + } + } + } + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + c.state = CallInfo.State.ACTIVE; + if (countCalls > 0) { + c.isMpty = true; + } + } + } + + return true; + } + + public boolean + explicitCallTransfer() { + int countCalls = 0; + + // if there's connecting calls, we can't do this yet + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + countCalls++; + + if (c.isConnecting()) { + return false; + } + } + } + + // disconnect the subscriber from both calls + return triggerHangupAll(); + } + + public boolean + onDial(String address) { + CallInfo call; + int freeSlot = -1; + + Log.d("GSM", "SC> dial '" + address + "'"); + + if (nextDialFailImmediately) { + nextDialFailImmediately = false; + + Log.d("GSM", "SC< dial fail (per request)"); + return false; + } + + String phNum = PhoneNumberUtils.extractNetworkPortion(address); + + if (phNum.length() == 0) { + Log.d("GSM", "SC< dial fail (invalid ph num)"); + return false; + } + + // Ignore setting up GPRS + if (phNum.startsWith("*99") && phNum.endsWith("#")) { + Log.d("GSM", "SC< dial ignored (gprs)"); + return true; + } + + // There can be at most 1 active "line" when we initiate + // a new call + try { + if (countActiveLines() > 1) { + Log.d("GSM", "SC< dial fail (invalid call state)"); + return false; + } + } catch (InvalidStateEx ex) { + Log.d("GSM", "SC< dial fail (invalid call state)"); + return false; + } + + for (int i = 0 ; i < calls.length ; i++) { + if (freeSlot < 0 && calls[i] == null) { + freeSlot = i; + } + + if (calls[i] != null && !calls[i].isActiveOrHeld()) { + // Can't make outgoing calls when there is a ringing or + // connecting outgoing call + Log.d("GSM", "SC< dial fail (invalid call state)"); + return false; + } else if (calls[i] != null && calls[i].state == CallInfo.State.ACTIVE) { + // All active calls behome held + calls[i].state = CallInfo.State.HOLDING; + } + } + + if (freeSlot < 0) { + Log.d("GSM", "SC< dial fail (invalid call state)"); + return false; + } + + calls[freeSlot] = CallInfo.createOutgoingCall(phNum); + + if (autoProgressConnecting) { + sendMessageDelayed( + obtainMessage(EVENT_PROGRESS_CALL_STATE, calls[freeSlot]), + CONNECTING_PAUSE_MSEC); + } + + Log.d("GSM", "SC< dial (slot = " + freeSlot + ")"); + + return true; + } + + public List<DriverCall> + getDriverCalls() { + ArrayList<DriverCall> ret = new ArrayList<DriverCall>(calls.length); + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + DriverCall dc; + + dc = c.toDriverCall(i + 1); + ret.add(dc); + } + } + + Log.d("GSM", "SC< getDriverCalls " + ret); + + return ret; + } + + public List<String> + getClccLines() { + ArrayList<String> ret = new ArrayList<String>(calls.length); + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo c = calls[i]; + + if (c != null) { + ret.add((c.toCLCCLine(i + 1))); + } + } + + return ret; + } + + private int + countActiveLines() throws InvalidStateEx { + boolean hasMpty = false; + boolean hasHeld = false; + boolean hasActive = false; + boolean hasConnecting = false; + boolean hasRinging = false; + boolean mptyIsHeld = false; + + for (int i = 0 ; i < calls.length ; i++) { + CallInfo call = calls[i]; + + if (call != null) { + if (!hasMpty && call.isMpty) { + mptyIsHeld = call.state == CallInfo.State.HOLDING; + } else if (call.isMpty && mptyIsHeld + && call.state == CallInfo.State.ACTIVE + ) { + Log.e("ModelInterpreter", "Invalid state"); + throw new InvalidStateEx(); + } else if (!call.isMpty && hasMpty && mptyIsHeld + && call.state == CallInfo.State.HOLDING + ) { + Log.e("ModelInterpreter", "Invalid state"); + throw new InvalidStateEx(); + } + + hasMpty |= call.isMpty; + hasHeld |= call.state == CallInfo.State.HOLDING; + hasActive |= call.state == CallInfo.State.ACTIVE; + hasConnecting |= call.isConnecting(); + hasRinging |= call.isRinging(); + } + } + + int ret = 0; + + if (hasHeld) ret++; + if (hasActive) ret++; + if (hasConnecting) ret++; + if (hasRinging) ret++; + + return ret; + } + +} diff --git a/src/java/com/android/internal/telephony/test/SimulatedRadioControl.java b/src/java/com/android/internal/telephony/test/SimulatedRadioControl.java new file mode 100644 index 0000000..054d370 --- /dev/null +++ b/src/java/com/android/internal/telephony/test/SimulatedRadioControl.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2006 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.internal.telephony.test; + +public interface SimulatedRadioControl +{ + public void triggerRing(String number); + + public void progressConnectingCallState(); + + public void progressConnectingToActive(); + + public void setAutoProgressConnectingCall(boolean b); + + public void setNextDialFailImmediately(boolean b); + + public void setNextCallFailCause(int gsmCause); + + public void triggerHangupForeground(); + + public void triggerHangupBackground(); + + public void triggerHangupAll(); + + public void triggerIncomingSMS(String message); + + public void shutdown(); + + /** Pause responses to async requests until (ref-counted) resumeResponses() */ + public void pauseResponses(); + + /** see pauseResponses */ + public void resumeResponses(); + + public void triggerSsn(int type, int code); + + /** Generates an incoming USSD message. */ + public void triggerIncomingUssd(String statusCode, String message); +} diff --git a/src/java/com/android/internal/telephony/test/package.html b/src/java/com/android/internal/telephony/test/package.html new file mode 100755 index 0000000..c9f96a6 --- /dev/null +++ b/src/java/com/android/internal/telephony/test/package.html @@ -0,0 +1,5 @@ +<body> + +{@hide} + +</body> diff --git a/src/java/com/android/internal/telephony/uicc/UiccController.java b/src/java/com/android/internal/telephony/uicc/UiccController.java new file mode 100644 index 0000000..5961efd --- /dev/null +++ b/src/java/com/android/internal/telephony/uicc/UiccController.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 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.internal.telephony.uicc; + +import com.android.internal.telephony.IccCard; +import com.android.internal.telephony.PhoneBase; +import com.android.internal.telephony.cdma.CDMALTEPhone; +import com.android.internal.telephony.cdma.CDMAPhone; +import com.android.internal.telephony.gsm.GSMPhone; + +import android.util.Log; + +/* This class is responsible for keeping all knowledge about + * ICCs in the system. It is also used as API to get appropriate + * applications to pass them to phone and service trackers. + */ +public class UiccController { + private static final boolean DBG = true; + private static final String LOG_TAG = "RIL_UiccController"; + + private static UiccController mInstance; + + private PhoneBase mCurrentPhone; + private boolean mIsCurrentCard3gpp; + private IccCard mIccCard; + + public static synchronized UiccController getInstance(PhoneBase phone) { + if (mInstance == null) { + mInstance = new UiccController(phone); + } else { + mInstance.setNewPhone(phone); + } + return mInstance; + } + + public IccCard getIccCard() { + return mIccCard; + } + + private UiccController(PhoneBase phone) { + if (DBG) log("Creating UiccController"); + setNewPhone(phone); + } + + private void setNewPhone(PhoneBase phone) { + mCurrentPhone = phone; + if (phone instanceof GSMPhone) { + if (DBG) log("New phone is GSMPhone"); + updateCurrentCard(IccCard.CARD_IS_3GPP); + } else if (phone instanceof CDMALTEPhone){ + if (DBG) log("New phone type is CDMALTEPhone"); + updateCurrentCard(IccCard.CARD_IS_3GPP); + } else if (phone instanceof CDMAPhone){ + if (DBG) log("New phone type is CDMAPhone"); + updateCurrentCard(IccCard.CARD_IS_NOT_3GPP); + } else { + Log.e(LOG_TAG, "Unhandled phone type. Critical error!"); + } + } + + private void updateCurrentCard(boolean isNewCard3gpp) { + if (mIsCurrentCard3gpp == isNewCard3gpp && mIccCard != null) { + return; + } + + if (mIccCard != null) { + mIccCard.dispose(); + mIccCard = null; + } + + mIsCurrentCard3gpp = isNewCard3gpp; + mIccCard = new IccCard(mCurrentPhone, mCurrentPhone.getPhoneName(), + isNewCard3gpp, DBG); + } + + private void log(String string) { + Log.d(LOG_TAG, string); + } +}
\ No newline at end of file diff --git a/tests/telephonymockriltests/Android.mk b/tests/telephonymockriltests/Android.mk new file mode 100644 index 0000000..9731d0d --- /dev/null +++ b/tests/telephonymockriltests/Android.mk @@ -0,0 +1,14 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_STATIC_JAVA_LIBRARIES := mockrilcontroller + +LOCAL_JAVA_LIBRARIES := android.test.runner + +LOCAL_PACKAGE_NAME := TelephonyMockRilTests + +include $(BUILD_PACKAGE) diff --git a/tests/telephonymockriltests/AndroidManifest.xml b/tests/telephonymockriltests/AndroidManifest.xml new file mode 100644 index 0000000..63f44a2 --- /dev/null +++ b/tests/telephonymockriltests/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright (C) 2009 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.telephonymockriltests"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:label="TelephonyMockRilTest" + android:name="TelephonyMockRilTest"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + + <instrumentation android:name=".TelephonyMockTestRunner" + android:targetPackage="com.android.telephonymockriltests" + android:label="Test runner for Telephony Tests Using Mock RIL" + /> + + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + +</manifest> diff --git a/tests/telephonymockriltests/src/com/android/telephonymockriltests/TelephonyMockTestRunner.java b/tests/telephonymockriltests/src/com/android/telephonymockriltests/TelephonyMockTestRunner.java new file mode 100644 index 0000000..78ee738 --- /dev/null +++ b/tests/telephonymockriltests/src/com/android/telephonymockriltests/TelephonyMockTestRunner.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010, 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.telephonymockriltests; + +import android.os.Bundle; +import android.test.InstrumentationTestRunner; +import android.test.InstrumentationTestSuite; +import com.android.internal.telephony.mockril.MockRilController; +import android.util.Log; + +import com.android.telephonymockriltests.functional.SimpleTestUsingMockRil; + +import java.io.IOException; +import junit.framework.TestSuite; +import junit.framework.TestCase; + +/** + * Test runner for telephony tests that using Mock RIL + * + */ +public class TelephonyMockTestRunner extends InstrumentationTestRunner { + private static final String TAG="TelephonyMockTestRunner"; + public MockRilController mController; + + @Override + public TestSuite getAllTests() { + TestSuite suite = new InstrumentationTestSuite(this); + suite.addTestSuite(SimpleTestUsingMockRil.class); + return suite; + } + + @Override + public void onCreate(Bundle icicle) { + try { + mController = new MockRilController(); + } catch (IOException e) { + e.printStackTrace(); + TestCase.assertTrue("Create Mock RIl Controller failed", false); + } + TestCase.assertNotNull(mController); + super.onCreate(icicle); + } + + @Override + public void finish(int resultCode, Bundle results) { + if (mController != null) + mController.closeChannel(); + super.finish(resultCode, results); + } +} diff --git a/tests/telephonymockriltests/src/com/android/telephonymockriltests/functional/SimpleTestUsingMockRil.java b/tests/telephonymockriltests/src/com/android/telephonymockriltests/functional/SimpleTestUsingMockRil.java new file mode 100644 index 0000000..3ea1cf2 --- /dev/null +++ b/tests/telephonymockriltests/src/com/android/telephonymockriltests/functional/SimpleTestUsingMockRil.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2010 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.telephonymockriltests.functional; + +import com.android.internal.telephony.mockril.MockRilController; +import android.test.InstrumentationTestCase; +import android.util.Log; + +import com.android.telephonymockriltests.TelephonyMockTestRunner; + +/** + * A simple test that using Mock RIL Controller + */ +public class SimpleTestUsingMockRil extends InstrumentationTestCase { + private static final String TAG = "SimpleTestUsingMockRil"; + private MockRilController mMockRilCtrl = null; + private TelephonyMockTestRunner mRunner; + + @Override + public void setUp() throws Exception { + super.setUp(); + mRunner = (TelephonyMockTestRunner)getInstrumentation(); + mMockRilCtrl = mRunner.mController; + assertNotNull(mMockRilCtrl); + } + + /** + * Get the current radio state of RIL + */ + public void testGetRadioState() { + int state = mMockRilCtrl.getRadioState(); + Log.v(TAG, "testGetRadioState: " + state); + assertTrue(state >= 0 && state <= 9); + } + + /** + * Set the current radio state of RIL + * and verify the radio state is set correctly + */ + public void testSetRadioState() { + for (int state = 0; state <= 9; state++) { + Log.v(TAG, "set radio state to be " + state); + assertTrue("set radio state: " + state + " failed.", + mMockRilCtrl.setRadioState(state)); + } + assertFalse("use an invalid radio state", mMockRilCtrl.setRadioState(-1)); + assertFalse("the radio state doesn't exist", mMockRilCtrl.setRadioState(10)); + } +} diff --git a/tests/telephonytests/Android.mk b/tests/telephonytests/Android.mk new file mode 100644 index 0000000..197cedf --- /dev/null +++ b/tests/telephonytests/Android.mk @@ -0,0 +1,14 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_STATIC_JAVA_LIBRARIES := librilproto-java + +LOCAL_JAVA_LIBRARIES := android.test.runner telephony-common + +LOCAL_PACKAGE_NAME := FrameworksTelephonyTests + +include $(BUILD_PACKAGE) diff --git a/tests/telephonytests/AndroidManifest.xml b/tests/telephonytests/AndroidManifest.xml new file mode 100644 index 0000000..ba1d957 --- /dev/null +++ b/tests/telephonytests/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright (C) 2009 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.frameworks.telephonytests"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:label="TelephonyTest" + android:name="TelephonyTest"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + <instrumentation android:name="android.test.InstrumentationTestRunner" + android:targetPackage="com.android.frameworks.telephonytests" + android:label="Frameworks Telephony Tests"> + </instrumentation> + + <instrumentation android:name=".TelephonyMockRilTestRunner" + android:targetPackage="com.android.frameworks.telephonytests" + android:label="Test Runner for Mock Ril Tests" + /> + + <uses-permission android:name="android.permission.READ_PHONE_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + +</manifest> diff --git a/tests/telephonytests/src/com/android/frameworks/telephonytests/TelephonyMockRilTestRunner.java b/tests/telephonytests/src/com/android/frameworks/telephonytests/TelephonyMockRilTestRunner.java new file mode 100644 index 0000000..9192f57 --- /dev/null +++ b/tests/telephonytests/src/com/android/frameworks/telephonytests/TelephonyMockRilTestRunner.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010, 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.frameworks.telephonytests; + +import android.os.Bundle; + +import android.test.InstrumentationTestRunner; +import android.test.InstrumentationTestSuite; +import android.util.Log; + +import java.io.IOException; + +import com.android.internal.telephony.RilChannel; +import com.android.internal.telephony.mockril.MockRilTest; + +import junit.framework.TestSuite; + +public class TelephonyMockRilTestRunner extends InstrumentationTestRunner { + + public RilChannel mMockRilChannel; + + @Override + public TestSuite getAllTests() { + log("getAllTests E"); + TestSuite suite = new InstrumentationTestSuite(this); + suite.addTestSuite(MockRilTest.class); + log("getAllTests X"); + return suite; + } + + @Override + public ClassLoader getLoader() { + log("getLoader EX"); + return TelephonyMockRilTestRunner.class.getClassLoader(); + } + + @Override + public void onCreate(Bundle icicle) { + log("onCreate E"); + try { + mMockRilChannel = RilChannel.makeRilChannel(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + log("onCreate X"); + + super.onCreate(icicle); + } + + @Override + public void onDestroy() { + // I've not seen this called + log("onDestroy EX"); + super.onDestroy(); + } + + @Override + public void onStart() { + // Called when the instrumentation thread is started. + // At the moment we don't need the thread so return + // which will shut down this unused thread. + log("onStart EX"); + super.onStart(); + } + + @Override + public void finish(int resultCode, Bundle results) { + // Called when complete so I ask the mMockRilChannel to quit. + log("finish E"); + mMockRilChannel.close(); + log("finish X"); + super.finish(resultCode, results); + } + + private void log(String s) { + Log.e("TelephonyMockRilTestRunner", s); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/ATResponseParserTest.java b/tests/telephonytests/src/com/android/internal/telephony/ATResponseParserTest.java new file mode 100644 index 0000000..81727e4 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/ATResponseParserTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class ATResponseParserTest extends TestCase { + @SmallTest + public void testBasic() throws Exception { + ATResponseParser p = new ATResponseParser("+CREG: 0"); + + assertEquals(0, p.nextInt()); + + assertFalse(p.hasMore()); + + try { + p.nextInt(); + fail("exception expected"); + } catch (ATParseEx ex) { + //test pass + } + + p = new ATResponseParser("+CREG: 0,1"); + assertEquals(0, p.nextInt()); + assertEquals(1, p.nextInt()); + assertFalse(p.hasMore()); + + p = new ATResponseParser("+CREG: 0, 1"); + assertEquals(0, p.nextInt()); + assertEquals(1, p.nextInt()); + assertFalse(p.hasMore()); + + p = new ATResponseParser("+CREG: 0, 1,"); + assertEquals(0, p.nextInt()); + assertEquals(1, p.nextInt()); + // this seems odd but is probably OK + assertFalse(p.hasMore()); + try { + p.nextInt(); + fail("exception expected"); + } catch (ATParseEx ex) { + //test pass + } + + p = new ATResponseParser("+CREG: 0, 1 "); + assertEquals(0, p.nextInt()); + assertEquals(1, p.nextInt()); + assertFalse(p.hasMore()); + + p = new ATResponseParser("0, 1 "); + // no prefix -> exception + try { + p.nextInt(); + fail("exception expected"); + } catch (ATParseEx ex) { + //test pass + } + + p = new ATResponseParser("+CREG: 0, 1, 5"); + assertFalse(p.nextBoolean()); + assertTrue(p.nextBoolean()); + try { + // is this over-constraining? + p.nextBoolean(); + fail("exception expected"); + } catch (ATParseEx ex) { + //test pass + } + + p = new ATResponseParser("+CLCC: 1,0,2,0,0,\"+18005551212\",145"); + + assertEquals(1, p.nextInt()); + assertFalse(p.nextBoolean()); + assertEquals(2, p.nextInt()); + assertEquals(0, p.nextInt()); + assertEquals(0, p.nextInt()); + assertEquals("+18005551212", p.nextString()); + assertEquals(145, p.nextInt()); + assertFalse(p.hasMore()); + + p = new ATResponseParser("+CLCC: 1,0,2,0,0,\"+18005551212,145"); + + assertEquals(1, p.nextInt()); + assertFalse(p.nextBoolean()); + assertEquals(2, p.nextInt()); + assertEquals(0, p.nextInt()); + assertEquals(0, p.nextInt()); + try { + p.nextString(); + fail("expected ex"); + } catch (ATParseEx ex) { + //test pass + } + + p = new ATResponseParser("+FOO: \"\""); + assertEquals("", p.nextString()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/AdnRecordTest.java b/tests/telephonytests/src/com/android/internal/telephony/AdnRecordTest.java new file mode 100644 index 0000000..8a4a285 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/AdnRecordTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + +/** + * {@hide} + */ +public class AdnRecordTest extends TestCase { + + @SmallTest + public void testBasic() throws Exception { + AdnRecord adn; + + // + // Typical record + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C07918150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("+18056377243", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // Empty records, empty strings + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")); + + assertEquals("", adn.getAlphaTag()); + assertEquals("", adn.getNumber()); + assertTrue(adn.isEmpty()); + + // + // Record too short + // + adn = new AdnRecord(IccUtils.hexStringToBytes( "FF")); + + assertEquals("", adn.getAlphaTag()); + assertEquals("", adn.getNumber()); + assertTrue(adn.isEmpty()); + + // + // TOA = 0xff ("control string") + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C07FF8150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("18056377243", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // TOA = 0x81 (unknown) + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C07818150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("18056377243", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // Number Length is too long + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C0F918150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // Number Length is zero (invalid) + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C00918150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // Number Length is 2, first number byte is FF, TOA is international + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C0291FF50367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // Number Length is 2, first number digit is valid, TOA is international + // + adn = new AdnRecord( + IccUtils.hexStringToBytes("566F696365204D61696C0291F150367742F3FFFFFFFFFFFF")); + + assertEquals("Voice Mail", adn.getAlphaTag()); + assertEquals("+1", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // An extended record + // + adn = new AdnRecord( + IccUtils.hexStringToBytes( + "4164676A6DFFFFFFFFFFFFFFFFFFFFFF0B918188551512C221436587FF01")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678", adn.getNumber()); + assertFalse(adn.isEmpty()); + assertTrue(adn.hasExtendedRecord()); + + adn.appendExtRecord(IccUtils.hexStringToBytes("0206092143658709ffffffffff")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678901234567890", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // An extended record with an invalid extension + // + adn = new AdnRecord( + IccUtils.hexStringToBytes( + "4164676A6DFFFFFFFFFFFFFFFFFFFFFF0B918188551512C221436587FF01")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678", adn.getNumber()); + assertFalse(adn.isEmpty()); + assertTrue(adn.hasExtendedRecord()); + + adn.appendExtRecord(IccUtils.hexStringToBytes("0106092143658709ffffffffff")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678", adn.getNumber()); + assertFalse(adn.isEmpty()); + + // + // An extended record with an invalid extension + // + adn = new AdnRecord( + IccUtils.hexStringToBytes( + "4164676A6DFFFFFFFFFFFFFFFFFFFFFF0B918188551512C221436587FF01")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678", adn.getNumber()); + assertFalse(adn.isEmpty()); + assertTrue(adn.hasExtendedRecord()); + + adn.appendExtRecord(IccUtils.hexStringToBytes("020B092143658709ffffffffff")); + + assertEquals("Adgjm", adn.getAlphaTag()); + assertEquals("+18885551212,12345678", adn.getNumber()); + assertFalse(adn.isEmpty()); + } +} + + diff --git a/tests/telephonytests/src/com/android/internal/telephony/ApnSettingTest.java b/tests/telephonytests/src/com/android/internal/telephony/ApnSettingTest.java new file mode 100755 index 0000000..ac8c4c1 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/ApnSettingTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +import junit.framework.TestCase; + +import android.test.suitebuilder.annotation.SmallTest; + +public class ApnSettingTest extends TestCase { + + public static final String[] TYPES = {"default", "*"}; + + public static void assertApnSettingEqual(ApnSetting a1, ApnSetting a2) { + assertEquals(a1.carrier, a2.carrier); + assertEquals(a1.apn, a2.apn); + assertEquals(a1.proxy, a2.proxy); + assertEquals(a1.port, a2.port); + assertEquals(a1.mmsc, a2.mmsc); + assertEquals(a1.mmsProxy, a2.mmsProxy); + assertEquals(a1.mmsPort, a2.mmsPort); + assertEquals(a1.user, a2.user); + assertEquals(a1.password, a2.password); + assertEquals(a1.authType, a2.authType); + assertEquals(a1.id, a2.id); + assertEquals(a1.numeric, a2.numeric); + assertEquals(a1.protocol, a2.protocol); + assertEquals(a1.roamingProtocol, a2.roamingProtocol); + assertEquals(a1.types.length, a2.types.length); + int i; + for (i = 0; i < a1.types.length; i++) { + assertEquals(a1.types[i], a2.types[i]); + } + assertEquals(a1.carrierEnabled, a2.carrierEnabled); + assertEquals(a1.bearer, a2.bearer); + } + + @SmallTest + public void testFromString() throws Exception { + String[] dunTypes = {"DUN"}; + String[] mmsTypes = {"mms", "*"}; + + ApnSetting expected_apn; + String testString; + + // A real-world v1 example string. + testString = "Vodafone IT,web.omnitel.it,,,,,,,,,222,10,,DUN"; + expected_apn = new ApnSetting( + -1, "22210", "Vodafone IT", "web.omnitel.it", "", "", + "", "", "", "", "", 0, dunTypes, "IP", "IP",true,0); + assertApnSettingEqual(expected_apn, ApnSetting.fromString(testString)); + + // A v2 string. + testString = "[ApnSettingV2] Name,apn,,,,,,,,,123,45,,mms|*,IPV6,IP,true,14"; + expected_apn = new ApnSetting( + -1, "12345", "Name", "apn", "", "", + "", "", "", "", "", 0, mmsTypes, "IPV6", "IP",true,14); + assertApnSettingEqual(expected_apn, ApnSetting.fromString(testString)); + + // A v2 string with spaces. + testString = "[ApnSettingV2] Name,apn, ,,,,,,,,123,45,,mms|*,IPV4V6, IP,true,14"; + expected_apn = new ApnSetting( + -1, "12345", "Name", "apn", "", "", + "", "", "", "", "", 0, mmsTypes, "IPV4V6", "IP",true,14); + assertApnSettingEqual(expected_apn, ApnSetting.fromString(testString)); + + // Return null if insufficient fields given. + testString = "[ApnSettingV2] Name,apn,,,,,,,,,123, 45,,mms|*"; + assertEquals(null, ApnSetting.fromString(testString)); + + testString = "Name,apn,,,,,,,,,123, 45,"; + assertEquals(null, ApnSetting.fromString(testString)); + + // Parse (incorrect) V2 format without the tag as V1. + testString = "Name,apn,,,,,,,,,123, 45,,mms|*,IPV6,true,14"; + String[] incorrectTypes = {"mms|*", "IPV6"}; + expected_apn = new ApnSetting( + -1, "12345", "Name", "apn", "", "", + "", "", "", "", "", 0, incorrectTypes, "IP", "IP",true,14); + assertApnSettingEqual(expected_apn, ApnSetting.fromString(testString)); + } + + + @SmallTest + public void testToString() throws Exception { + String[] types = {"default", "*"}; + ApnSetting apn = new ApnSetting( + 99, "12345", "Name", "apn", "proxy", "port", + "mmsc", "mmsproxy", "mmsport", "user", "password", 0, + types, "IPV4V6", "IP", true, 14); + String expected = "[ApnSettingV2] Name, 99, 12345, apn, proxy, " + + "mmsc, mmsproxy, mmsport, port, 0, default | *, " + + "IPV4V6, IP, true, 14"; + assertEquals(expected, apn.toString()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/CallerInfoTest.java b/tests/telephonytests/src/com/android/internal/telephony/CallerInfoTest.java new file mode 100644 index 0000000..1e5dafb --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/CallerInfoTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2009 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.internal.telephony; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import com.android.internal.telephony.CallerInfo; +import com.android.internal.telephony.CallerInfoAsyncQuery; +import android.util.Log; +import android.os.Looper; +import android.test.ActivityInstrumentationTestCase; +import android.util.StringBuilderPrinter; + +/* + * Check the CallerInfo utility class works as expected. + * + */ + +public class CallerInfoTest extends AndroidTestCase { + private CallerInfo mInfo; + private Context mContext; + + private static final String kEmergencyNumber = "Emergency Number"; + private static final int kToken = 0xdeadbeef; + private static final String TAG = "CallerInfoUnitTest"; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mContext = new MockContext(); + mInfo = new CallerInfo(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Checks the caller info instance is flagged as an emergency if + * the number is an emergency one. There is no test for the + * contact based constructors because emergency number are not in + * the contact DB. + */ + @SmallTest + public void testEmergencyIsProperlySet() throws Exception { + assertFalse(mInfo.isEmergencyNumber()); + + mInfo = CallerInfo.getCallerInfo(mContext, "911"); + assertIsValidEmergencyCallerInfo(); + + mInfo = CallerInfo.getCallerInfo(mContext, "tel:911"); + assertIsValidEmergencyCallerInfo(); + + + // This one hits the content resolver. + mInfo = CallerInfo.getCallerInfo(mContext, "18001234567"); + assertFalse(mInfo.isEmergencyNumber()); + } + + /** + * Same as testEmergencyIsProperlySet but uses the async query api. + */ + @SmallTest + public void testEmergencyIsProperlySetUsingAsyncQuery() throws Exception { + QueryRunner query; + + query = new QueryRunner("911"); + query.runAndCheckCompletion(); + assertIsValidEmergencyCallerInfo(); + + query = new QueryRunner("tel:911"); + query.runAndCheckCompletion(); + assertIsValidEmergencyCallerInfo(); + + query = new QueryRunner("18001234567"); + query.runAndCheckCompletion(); + assertFalse(mInfo.isEmergencyNumber()); + } + + /** + * For emergency caller info, phoneNumber should be set to the + * string emergency_call_dialog_number_for_display and the + * photoResource should be set to the picture_emergency drawable. + */ + @SmallTest + public void testEmergencyNumberAndPhotoAreSet() throws Exception { + mInfo = CallerInfo.getCallerInfo(mContext, "911"); + + assertIsValidEmergencyCallerInfo(); + } + + // TODO: Add more tests: + /** + * Check if the voice mail number cannot be retrieved that the + * original phone number is preserved. + */ + /** + * Check the markAs* methods work. + */ + + + // + // Helpers + // + + // Partial implementation of MockResources. + public class MockResources extends android.test.mock.MockResources + { + @Override + public String getString(int resId) throws Resources.NotFoundException { + switch (resId) { + case com.android.internal.R.string.emergency_call_dialog_number_for_display: + return kEmergencyNumber; + default: + throw new UnsupportedOperationException("Missing handling for resid " + resId); + } + } + } + + // Partial implementation of MockContext. + public class MockContext extends android.test.mock.MockContext { + private ContentResolver mResolver; + private Resources mResources; + + public MockContext() { + mResolver = new android.test.mock.MockContentResolver(); + mResources = new MockResources(); + } + + @Override + public ContentResolver getContentResolver() { + return mResolver; + } + + @Override + public Resources getResources() { + return mResources; + } + } + + /** + * Class to run a CallerInfoAsyncQuery in a separate thread, with + * its own Looper. We cannot use the main Looper because on the + * 1st quit the thread is maked dead, ie no further test can use + * it. Also there is not way to inject a Looper instance in the + * query, so we have to use a thread with its own looper. + */ + private class QueryRunner extends Thread + implements CallerInfoAsyncQuery.OnQueryCompleteListener { + private Looper mLooper; + private String mNumber; + private boolean mAsyncCompleted; + + public QueryRunner(String number) { + super(); + mNumber = number; + } + + // Run the query in the thread, wait for completion. + public void runAndCheckCompletion() throws InterruptedException { + start(); + join(); + assertTrue(mAsyncCompleted); + } + + @Override + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + mAsyncCompleted = false; + // The query will pick the thread local looper we've just prepared. + CallerInfoAsyncQuery.startQuery(kToken, mContext, mNumber, this, null); + mLooper.loop(); + } + + // Quit the Looper on the 1st callback + // (EVENT_EMERGENCY_NUMBER). There is another message + // (EVENT_END_OF_QUEUE) that will never be delivered because + // the test has exited. The corresponding stack trace + // "Handler{xxxxx} sending message to a Handler on a dead + // thread" can be ignored. + public void onQueryComplete(int token, Object cookie, CallerInfo info) { + mAsyncCompleted = true; + mInfo = info; + mLooper.quit(); + } + } + + /** + * Fail if mInfo does not contain a valid emergency CallerInfo instance. + */ + private void assertIsValidEmergencyCallerInfo() throws Exception { + assertTrue(mInfo.isEmergencyNumber()); + + // For emergency caller info, phoneNumber should be set to the + // string emergency_call_dialog_number_for_display and the + // photoResource should be set to the picture_emergency drawable. + assertEquals(kEmergencyNumber, mInfo.phoneNumber); + assertEquals(com.android.internal.R.drawable.picture_emergency, mInfo.photoResource); + + // The name should be null + assertNull(mInfo.name); + assertEquals(0, mInfo.namePresentation); + assertNull(mInfo.cnapName); + assertEquals(0, mInfo.numberPresentation); + + assertFalse(mInfo.contactExists); + assertEquals(0, mInfo.person_id); + assertFalse(mInfo.needUpdate); + assertNull(mInfo.contactRefUri); + + assertNull(mInfo.phoneLabel); + assertEquals(0, mInfo.numberType); + assertNull(mInfo.numberLabel); + + assertNull(mInfo.contactRingtoneUri); + assertFalse(mInfo.shouldSendToVoicemail); + + assertNull(mInfo.cachedPhoto); + assertFalse(mInfo.isCachedPhotoCurrent); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java b/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java new file mode 100644 index 0000000..f9dc3a9 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/GsmAlphabetTest.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.GsmAlphabet; + +import junit.framework.TestCase; + +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.SmallTest; + +public class GsmAlphabetTest extends TestCase { + + private static final String sGsmExtendedChars = "{|}\\[~]\f\u20ac"; + + @SmallTest + public void test7bitWithHeader() throws Exception { + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 1; + concatRef.seqNumber = 2; + concatRef.msgCount = 2; + concatRef.isEightBits = true; + SmsHeader header = new SmsHeader(); + header.concatRef = concatRef; + + String message = "aaaaaaaaaabbbbbbbbbbcccccccccc"; + byte[] userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, + SmsHeader.toByteArray(header), 0, 0); + int septetCount = GsmAlphabet.countGsmSeptetsUsingTables(message, true, 0, 0); + String parsedMessage = GsmAlphabet.gsm7BitPackedToString( + userData, SmsHeader.toByteArray(header).length+2, septetCount, 1, 0, 0); + assertEquals(message, parsedMessage); + } + + // TODO: This method should *really* be a series of individual test methods. + // However, it's a SmallTest because it executes quickly. + @SmallTest + public void testBasic() throws Exception { + // '@' maps to char 0 + assertEquals(0, GsmAlphabet.charToGsm('@')); + + // `a (a with grave accent) maps to last GSM character + assertEquals(0x7f, GsmAlphabet.charToGsm('\u00e0')); + + // + // These are the extended chars + // They should all return GsmAlphabet.GSM_EXTENDED_ESCAPE + // + + for (int i = 0, s = sGsmExtendedChars.length(); i < s; i++) { + assertEquals(GsmAlphabet.GSM_EXTENDED_ESCAPE, + GsmAlphabet.charToGsm(sGsmExtendedChars.charAt(i))); + + } + + // euro symbol + assertEquals(GsmAlphabet.GSM_EXTENDED_ESCAPE, + GsmAlphabet.charToGsm('\u20ac')); + + // An unmappable char (the 'cent' char) maps to a space + assertEquals(GsmAlphabet.charToGsm(' '), + GsmAlphabet.charToGsm('\u00a2')); + + // unmappable = space = 1 septet + assertEquals(1, GsmAlphabet.countGsmSeptets('\u00a2')); + + // + // Test extended table + // + + for (int i = 0, s = sGsmExtendedChars.length(); i < s; i++) { + assertEquals(sGsmExtendedChars.charAt(i), + GsmAlphabet.gsmExtendedToChar( + GsmAlphabet.charToGsmExtended(sGsmExtendedChars.charAt(i)))); + + } + + // Unmappable extended char + assertEquals(GsmAlphabet.charToGsm(' '), + GsmAlphabet.charToGsmExtended('@')); + + // + // gsmToChar() + // + + assertEquals('@', GsmAlphabet.gsmToChar(0)); + + // `a (a with grave accent) maps to last GSM character + assertEquals('\u00e0', GsmAlphabet.gsmToChar(0x7f)); + + assertEquals('\uffff', + GsmAlphabet.gsmToChar(GsmAlphabet.GSM_EXTENDED_ESCAPE)); + + // Out-of-range/unmappable value + assertEquals(' ', GsmAlphabet.gsmToChar(0x80)); + + // + // gsmExtendedToChar() + // + + assertEquals('{', GsmAlphabet.gsmExtendedToChar(0x28)); + + // No double-escapes + assertEquals(' ', GsmAlphabet.gsmExtendedToChar( + GsmAlphabet.GSM_EXTENDED_ESCAPE)); + + // Reserved for extension to extension table (mapped to space) + assertEquals(' ', GsmAlphabet.gsmExtendedToChar(GsmAlphabet.GSM_EXTENDED_ESCAPE)); + + // Unmappable (mapped to character in default or national locking shift table) + assertEquals('@', GsmAlphabet.gsmExtendedToChar(0)); + assertEquals('\u00e0', GsmAlphabet.gsmExtendedToChar(0x7f)); + + // + // stringTo7BitPacked, gsm7BitPackedToString + // + + byte[] packed; + StringBuilder testString = new StringBuilder(300); + + // Check all alignment cases + for (int i = 0; i < 9; i++, testString.append('@')) { + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + assertEquals(testString.toString(), + GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); + } + + // Check full non-extended alphabet + for (int i = 0; i < 0x80; i++) { + char c; + + if (i == GsmAlphabet.GSM_EXTENDED_ESCAPE) { + continue; + } + + c = GsmAlphabet.gsmToChar(i); + testString.append(c); + + // These are all non-extended chars, so it should be + // one septet per char + assertEquals(1, GsmAlphabet.countGsmSeptets(c)); + } + + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + assertEquals(testString.toString(), + GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); + + // Test extended chars too + + testString.append(sGsmExtendedChars); + + for (int i = 0, s = sGsmExtendedChars.length(); i < s; i++) { + // These are all extended chars, so it should be + // two septets per char + assertEquals(2, GsmAlphabet.countGsmSeptets(sGsmExtendedChars.charAt(i))); + + } + + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + assertEquals(testString.toString(), + GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); + + // stringTo7BitPacked handles up to 255 septets + + testString.setLength(0); + for (int i = 0; i < 255; i++) { + testString.append('@'); + } + + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + assertEquals(testString.toString(), + GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); + + // > 255 septets throws runtime exception + testString.append('@'); + + try { + GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + fail("expected exception"); + } catch (EncodeException ex) { + // exception expected + } + + // Try 254 septets with 127 extended chars + + testString.setLength(0); + for (int i = 0; i < (255 / 2); i++) { + testString.append('{'); + } + + packed = GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + assertEquals(testString.toString(), + GsmAlphabet.gsm7BitPackedToString(packed, 1, 0xff & packed[0])); + + // > 255 septets throws runtime exception + testString.append('{'); + + try { + GsmAlphabet.stringToGsm7BitPacked(testString.toString(), 0, 0); + fail("expected exception"); + } catch (EncodeException ex) { + // exception expected + } + + // Reserved for extension to extension table (mapped to space) + packed = new byte[]{(byte)(0x1b | 0x80), 0x1b >> 1}; + assertEquals(" ", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + + // Unmappable (mapped to character in default alphabet table) + packed[0] = 0x1b; + packed[1] = 0x00; + assertEquals("@", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + packed[0] = (byte)(0x1b | 0x80); + packed[1] = (byte)(0x7f >> 1); + assertEquals("\u00e0", GsmAlphabet.gsm7BitPackedToString(packed, 0, 2)); + + // + // 8 bit unpacked format + // + // Note: we compare hex strings here + // because Assert doesn't have array comparisons + + byte unpacked[]; + + unpacked = IccUtils.hexStringToBytes("566F696365204D61696C"); + assertEquals("Voice Mail", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + assertEquals(IccUtils.bytesToHexString(unpacked), + IccUtils.bytesToHexString( + GsmAlphabet.stringToGsm8BitPacked("Voice Mail"))); + + unpacked = GsmAlphabet.stringToGsm8BitPacked(sGsmExtendedChars); + // two bytes for every extended char + assertEquals(2 * sGsmExtendedChars.length(), unpacked.length); + assertEquals(sGsmExtendedChars, + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + // should be two bytes per extended char + assertEquals(2 * sGsmExtendedChars.length(), unpacked.length); + + // Test truncation of unaligned extended chars + unpacked = new byte[3]; + GsmAlphabet.stringToGsm8BitUnpackedField(sGsmExtendedChars, unpacked, + 0, unpacked.length); + + // Should be one extended char and an 0xff at the end + + assertEquals(0xff, 0xff & unpacked[2]); + assertEquals(sGsmExtendedChars.substring(0, 1), + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + // Test truncation of normal chars + unpacked = new byte[3]; + GsmAlphabet.stringToGsm8BitUnpackedField("abcd", unpacked, + 0, unpacked.length); + + assertEquals("abc", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + // Test truncation of mixed normal and extended chars + unpacked = new byte[3]; + GsmAlphabet.stringToGsm8BitUnpackedField("a{cd", unpacked, + 0, unpacked.length); + + assertEquals("a{", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + // Test padding after normal char + unpacked = new byte[3]; + GsmAlphabet.stringToGsm8BitUnpackedField("a", unpacked, + 0, unpacked.length); + + assertEquals("a", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + assertEquals(0xff, 0xff & unpacked[1]); + assertEquals(0xff, 0xff & unpacked[2]); + + // Test malformed input -- escape char followed by end of field + unpacked[0] = 0; + unpacked[1] = 0; + unpacked[2] = GsmAlphabet.GSM_EXTENDED_ESCAPE; + + assertEquals("@@", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length)); + + // non-zero offset + assertEquals("@", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 1, unpacked.length - 1)); + + // test non-zero offset + unpacked[0] = 0; + GsmAlphabet.stringToGsm8BitUnpackedField("abcd", unpacked, + 1, unpacked.length - 1); + + + assertEquals(0, unpacked[0]); + + assertEquals("ab", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 1, unpacked.length - 1)); + + // test non-zero offset with truncated extended char + unpacked[0] = 0; + + GsmAlphabet.stringToGsm8BitUnpackedField("a{", unpacked, + 1, unpacked.length - 1); + + assertEquals(0, unpacked[0]); + + assertEquals("a", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 1, unpacked.length - 1)); + + // Reserved for extension to extension table (mapped to space) + unpacked[0] = 0x1b; + unpacked[1] = 0x1b; + assertEquals(" ", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); + + // Unmappable (mapped to character in default or national locking shift table) + unpacked[1] = 0x00; + assertEquals("@", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); + unpacked[1] = 0x7f; + assertEquals("\u00e0", GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, 2)); + } + + @SmallTest + public void testGsm8BitUpackedWithEuckr() throws Exception { + // Some feature phones in Korea store contacts as euc-kr. + // Test this situations. + byte unpacked[]; + + // Test general alphabet strings. + unpacked = IccUtils.hexStringToBytes("61626320646566FF"); + assertEquals("abc def", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length, "euc-kr")); + + // Test korean strings. + unpacked = IccUtils.hexStringToBytes("C5D7BDBAC6AEFF"); + assertEquals("\uD14C\uC2A4\uD2B8", + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length, "euc-kr")); + + // Test gsm Extented Characters. + unpacked = GsmAlphabet.stringToGsm8BitPacked(sGsmExtendedChars); + assertEquals(sGsmExtendedChars, + GsmAlphabet.gsm8BitUnpackedToString(unpacked, 0, unpacked.length, "euc-kr")); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java b/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java new file mode 100644 index 0000000..f2e5da1 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/GsmSmsTest.java @@ -0,0 +1,538 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.telephony.TelephonyManager; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import com.android.internal.telephony.gsm.SmsMessage; +import com.android.internal.util.HexDump; + +import java.util.ArrayList; + +public class GsmSmsTest extends AndroidTestCase { + + @SmallTest + public void testAddressing() throws Exception { + String pdu = "07914151551512f2040B916105551511f100006060605130308A04D4F29C0E"; + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + assertEquals("+14155551212", sms.getServiceCenterAddress()); + assertEquals("+16505551111", sms.getOriginatingAddress()); + assertEquals("Test", sms.getMessageBody()); + + pdu = "07914151551512f2040B916105551511f100036060924180008A0DA" + + "8695DAC2E8FE9296A794E07"; + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + assertEquals("+14155551212", sms.getServiceCenterAddress()); + assertEquals("+16505551111", sms.getOriginatingAddress()); + assertEquals("(Subject)Test", sms.getMessageBody()); + } + + @SmallTest + public void testUdh() throws Exception { + String pdu = "07914140279510F6440A8111110301003BF56080207130138A8C0B05040B8423F" + + "000032A02010106276170706C69636174696F6E2F766E642E7761702E6D6D732D" + + "6D65737361676500AF848D0185B4848C8298524E453955304A6D7135514141426" + + "66C414141414D7741414236514141414141008D908918802B3135313232393737" + + "3638332F545950453D504C4D4E008A808E022B918805810306977F83687474703" + + "A2F2F36"; + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + SmsHeader header = sms.getUserDataHeader(); + assertNotNull(header); + assertNotNull(header.concatRef); + assertEquals(header.concatRef.refNumber, 42); + assertEquals(header.concatRef.msgCount, 2); + assertEquals(header.concatRef.seqNumber, 1); + assertEquals(header.concatRef.isEightBits, true); + assertNotNull(header.portAddrs); + assertEquals(header.portAddrs.destPort, 2948); + assertEquals(header.portAddrs.origPort, 9200); + assertEquals(header.portAddrs.areEightBits, false); + + pdu = "07914140279510F6440A8111110301003BF56080207130238A3B0B05040B8423F" + + "000032A0202362E3130322E3137312E3135302F524E453955304A6D7135514141" + + "42666C414141414D774141423651414141414100"; + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + header = sms.getUserDataHeader(); + assertNotNull(header); + assertNotNull(header.concatRef); + assertEquals(header.concatRef.refNumber, 42); + assertEquals(header.concatRef.msgCount, 2); + assertEquals(header.concatRef.seqNumber, 2); + assertEquals(header.concatRef.isEightBits, true); + assertNotNull(header.portAddrs); + assertEquals(header.portAddrs.destPort, 2948); + assertEquals(header.portAddrs.origPort, 9200); + assertEquals(header.portAddrs.areEightBits, false); + } + + @SmallTest + public void testUcs2() throws Exception { + String pdu = "07912160130300F4040B914151245584F600087010807121352B1021220" + + "0A900AE00680065006C006C006F"; + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + assertEquals("\u2122\u00a9\u00aehello", sms.getMessageBody()); + } + + @SmallTest + public void testMultipart() throws Exception { + /* + * Multi-part text SMS with septet data. + */ + String pdu = "07916163838408F6440B816105224431F700007060217175830AA0050003" + + "00020162B1582C168BC562B1582C168BC562B1582C168BC562B1582C" + + "168BC562B1582C168BC562B1582C168BC562B1582C168BC562B1582C" + + "168BC562B1582C168BC562B1582C168BC562B1582C168BC562B1582C" + + "168BC562B1582C168BC562B1582C168BC562B1582C168BC562B1582C" + + "168BC562B1582C168BC562B1582C168BC562B1582C168BC562"; + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + assertEquals(sms.getMessageBody(), + "1111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111" + + "1111111111111111111111111111111111111111" + + "111111111111111111111111111111111"); + + pdu = "07916163838408F6440B816105224431F700007060217185000A23050003" + + "00020262B1582C168BC96432994C2693C96432994C2693C96432990C"; + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + assertEquals("1111111222222222222222222222", sms.getMessageBody()); + } + + @SmallTest + public void testCPHSVoiceMail() throws Exception { + // "set MWI flag" + + String pdu = "07912160130310F20404D0110041006060627171118A0120"; + + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertTrue(sms.isReplace()); + assertEquals("_@", sms.getOriginatingAddress()); + assertEquals(" ", sms.getMessageBody()); + assertTrue(sms.isMWISetMessage()); + + // "clear mwi flag" + + pdu = "07912160130310F20404D0100041006021924193352B0120"; + + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertTrue(sms.isMWIClearMessage()); + + // "clear MWI flag" + + pdu = "07912160130310F20404D0100041006060627161058A0120"; + + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertTrue(sms.isReplace()); + assertEquals("\u0394@", sms.getOriginatingAddress()); + assertEquals(" ", sms.getMessageBody()); + assertTrue(sms.isMWIClearMessage()); + } + + @SmallTest + public void testCingularVoiceMail() throws Exception { + // "set MWI flag" + + String pdu = "07912180958750F84401800500C87020026195702B06040102000200"; + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertTrue(sms.isMWISetMessage()); + assertTrue(sms.isMwiDontStore()); + + // "clear mwi flag" + + pdu = "07912180958750F84401800500C07020027160112B06040102000000"; + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertTrue(sms.isMWIClearMessage()); + assertTrue(sms.isMwiDontStore()); + } + + @SmallTest + public void testEmailGateway() throws Exception { + String pdu = "07914151551512f204038105f300007011103164638a28e6f71b50c687db" + + "7076d9357eb7412f7a794e07cdeb6275794c07bde8e5391d247e93f3"; + + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertEquals("+14155551212", sms.getServiceCenterAddress()); + assertTrue(sms.isEmail()); + assertEquals("foo@example.com", sms.getEmailFrom()); + assertEquals("foo@example.com", sms.getDisplayOriginatingAddress()); + // As of https://android-git.corp.google.com/g/#change,9324 + // getPseudoSubject will always be empty, and any subject is not extracted. + assertEquals("", sms.getPseudoSubject()); + assertEquals("test subject /test body", sms.getDisplayMessageBody()); + assertEquals("test subject /test body", sms.getEmailBody()); + + // email gateway sms test, including gsm extended character set. + pdu = "07914151551512f204038105f400007011103105458a29e6f71b50c687db" + + "7076d9357eb741af0d0a442fcfe9c23739bfe16d289bdee6b5f1813629"; + + sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertEquals("+14155551212", sms.getServiceCenterAddress()); + assertTrue(sms.isEmail()); + assertEquals("foo@example.com", sms.getDisplayOriginatingAddress()); + assertEquals("foo@example.com", sms.getEmailFrom()); + assertEquals("{ testBody[^~\\] }", sms.getDisplayMessageBody()); + assertEquals("{ testBody[^~\\] }", sms.getEmailBody()); + } + + @SmallTest + public void testExtendedCharacterTable() throws Exception { + String pdu = "07914151551512f2040B916105551511f100006080615131728A44D4F29C0E2" + + "AE3E96537B94C068DD16179784C2FCB41F4B0985D06B958ADD00FB0E94536AF9749" + + "74DA6D281BA00E95E26D509B946FC3DBF87A25D56A04"; + + SmsMessage sms = SmsMessage.createFromPdu(HexDump.hexStringToByteArray(pdu)); + + assertEquals("+14155551212", sms.getServiceCenterAddress()); + assertEquals("+16505551111", sms.getOriginatingAddress()); + assertEquals("Test extended character table .,-!?@~_\\/&\"';^|:()<{}>[]=%*+#", + sms.getMessageBody()); + } + + // GSM 7 bit tables in String form, Escape (0x1B) replaced with '@' + private static final String[] sBasicTables = { + // GSM 7 bit default alphabet + "@\u00a3$\u00a5\u00e8\u00e9\u00f9\u00ec\u00f2\u00c7\n\u00d8\u00f8\r\u00c5\u00e5\u0394_" + + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e@\u00c6\u00e6\u00df\u00c9" + + " !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u00a1ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6" + + "\u00d1\u00dc\u00a7\u00bfabcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0", + + // Turkish locking shift table + "@\u00a3$\u00a5\u20ac\u00e9\u00f9\u0131\u00f2\u00c7\n\u011e\u011f\r\u00c5\u00e5\u0394_" + + "\u03a6\u0393\u039b\u03a9\u03a0\u03a8\u03a3\u0398\u039e@\u015e\u015f\u00df\u00c9" + + " !\"#\u00a4%&'()*+,-./0123456789:;<=>?\u0130ABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c4\u00d6" + + "\u00d1\u00dc\u00a7\u00e7abcdefghijklmnopqrstuvwxyz\u00e4\u00f6\u00f1\u00fc\u00e0", + + // no locking shift table defined for Spanish + "", + + // Portuguese locking shift table + "@\u00a3$\u00a5\u00ea\u00e9\u00fa\u00ed\u00f3\u00e7\n\u00d4\u00f4\r\u00c1\u00e1\u0394_" + + "\u00aa\u00c7\u00c0\u221e^\\\u20ac\u00d3|@\u00c2\u00e2\u00ca\u00c9 !\"#\u00ba%&'()" + + "*+,-./0123456789:;<=>?\u00cdABCDEFGHIJKLMNOPQRSTUVWXYZ\u00c3\u00d5\u00da\u00dc" + + "\u00a7~abcdefghijklmnopqrstuvwxyz\u00e3\u00f5`\u00fc\u00e0" + }; + + @SmallTest + public void testFragmentText() throws Exception { + boolean isGsmPhone = (TelephonyManager.getDefault().getPhoneType() == + TelephonyManager.PHONE_TYPE_GSM); + + // Valid 160 character 7-bit text. + String text = "123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890" + + "123456789012345678901234567890"; + GsmAlphabet.TextEncodingDetails ted = SmsMessage.calculateLength(text, false); + assertEquals(1, ted.msgCount); + assertEquals(160, ted.codeUnitCount); + assertEquals(1, ted.codeUnitSize); + assertEquals(0, ted.languageTable); + assertEquals(0, ted.languageShiftTable); + if (isGsmPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text); + assertEquals(1, fragments.size()); + } + + // Valid 161 character 7-bit text. + text = "123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901"; + ted = SmsMessage.calculateLength(text, false); + assertEquals(2, ted.msgCount); + assertEquals(161, ted.codeUnitCount); + assertEquals(1, ted.codeUnitSize); + assertEquals(0, ted.languageTable); + assertEquals(0, ted.languageShiftTable); + if (isGsmPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text); + assertEquals(2, fragments.size()); + assertEquals(text, fragments.get(0) + fragments.get(1)); + assertEquals(153, fragments.get(0).length()); + assertEquals(8, fragments.get(1).length()); + } + } + + @SmallTest + public void testFragmentTurkishText() throws Exception { + boolean isGsmPhone = (TelephonyManager.getDefault().getPhoneType() == + TelephonyManager.PHONE_TYPE_GSM); + + int[] oldTables = GsmAlphabet.getEnabledSingleShiftTables(); + int[] turkishTable = { 1 }; + GsmAlphabet.setEnabledSingleShiftTables(turkishTable); + + // Valid 77 character text with Turkish characters. + String text = "ĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşı" + + "ĞŞİğşıĞŞİğşıĞŞİğş"; + GsmAlphabet.TextEncodingDetails ted = SmsMessage.calculateLength(text, false); + assertEquals(1, ted.msgCount); + assertEquals(154, ted.codeUnitCount); + assertEquals(1, ted.codeUnitSize); + assertEquals(0, ted.languageTable); + assertEquals(1, ted.languageShiftTable); + if (isGsmPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text); + assertEquals(1, fragments.size()); + assertEquals(text, fragments.get(0)); + assertEquals(77, fragments.get(0).length()); + } + + // Valid 78 character text with Turkish characters. + text = "ĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşı" + + "ĞŞİğşıĞŞİğşıĞŞİğşı"; + ted = SmsMessage.calculateLength(text, false); + assertEquals(2, ted.msgCount); + assertEquals(156, ted.codeUnitCount); + assertEquals(1, ted.codeUnitSize); + assertEquals(0, ted.languageTable); + assertEquals(1, ted.languageShiftTable); + if (isGsmPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text); + assertEquals(2, fragments.size()); + assertEquals(text, fragments.get(0) + fragments.get(1)); + assertEquals(74, fragments.get(0).length()); + assertEquals(4, fragments.get(1).length()); + } + + // Valid 160 character text with Turkish characters. + text = "ĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşı" + + "ĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğ" + + "ĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşıĞŞİğşı"; + ted = SmsMessage.calculateLength(text, false); + assertEquals(3, ted.msgCount); + assertEquals(320, ted.codeUnitCount); + assertEquals(1, ted.codeUnitSize); + assertEquals(0, ted.languageTable); + assertEquals(1, ted.languageShiftTable); + if (isGsmPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text); + assertEquals(3, fragments.size()); + assertEquals(text, fragments.get(0) + fragments.get(1) + fragments.get(2)); + assertEquals(74, fragments.get(0).length()); + assertEquals(74, fragments.get(1).length()); + assertEquals(12, fragments.get(2).length()); + } + + GsmAlphabet.setEnabledSingleShiftTables(oldTables); + } + + + @SmallTest + public void testDecode() throws Exception { + decodeSingle(0); // default table + decodeSingle(1); // Turkish locking shift table + decodeSingle(3); // Portuguese locking shift table + } + + private void decodeSingle(int language) throws Exception { + byte[] septets = new byte[(7 * 128 + 7) / 8]; + + int bitOffset = 0; + + for (int i = 0; i < 128; i++) { + int v; + if (i == 0x1b) { + // extended escape char + v = 0; + } else { + v = i; + } + + int byteOffset = bitOffset / 8; + int shift = bitOffset % 8; + + septets[byteOffset] |= v << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (v >> (8 - shift)); + } + + bitOffset += 7; + } + + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, 128, 0, language, 0); + byte[] reEncoded = GsmAlphabet.stringToGsm7BitPacked(decoded, language, 0); + + assertEquals(sBasicTables[language], decoded); + + // reEncoded has the count septets byte at the front + assertEquals(septets.length + 1, reEncoded.length); + + for (int i = 0; i < septets.length; i++) { + assertEquals(septets[i], reEncoded[i + 1]); + } + } + + private static final int GSM_ESCAPE_CHARACTER = 0x1b; + + private static final String[] sExtendedTables = { + // GSM 7 bit default alphabet extension table + "\f^{}\\[~]|\u20ac", + + // Turkish single shift extension table + "\f^{}\\[~]|\u011e\u0130\u015e\u00e7\u20ac\u011f\u0131\u015f", + + // Spanish single shift extension table + "\u00e7\f^{}\\[~]|\u00c1\u00cd\u00d3\u00da\u00e1\u20ac\u00ed\u00f3\u00fa", + + // Portuguese single shift extension table + "\u00ea\u00e7\f\u00d4\u00f4\u00c1\u00e1\u03a6\u0393^\u03a9\u03a0\u03a8\u03a3\u0398\u00ca" + + "{}\\[~]|\u00c0\u00cd\u00d3\u00da\u00c3\u00d5\u00c2\u20ac\u00ed\u00f3\u00fa\u00e3" + + "\u00f5\u00e2" + }; + + private static final int[][] sExtendedTableIndexes = { + {0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x65}, + {0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x47, 0x49, 0x53, 0x63, + 0x65, 0x67, 0x69, 0x73}, + {0x09, 0x0a, 0x14, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x41, 0x49, 0x4f, + 0x55, 0x61, 0x65, 0x69, 0x6f, 0x75}, + {0x05, 0x09, 0x0a, 0x0b, 0x0c, 0x0e, 0x0f, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1f, 0x28, 0x29, 0x2f, 0x3c, 0x3d, 0x3e, 0x40, 0x41, 0x49, + 0x4f, 0x55, 0x5b, 0x5c, 0x61, 0x65, 0x69, 0x6f, 0x75, 0x7b, 0x7c, 0x7f} + }; + + @SmallTest + public void testDecodeExtended() throws Exception { + for (int language = 0; language < 3; language++) { + int[] tableIndex = sExtendedTableIndexes[language]; + int numSeptets = tableIndex.length * 2; // two septets per extended char + byte[] septets = new byte[(7 * numSeptets + 7) / 8]; + + int bitOffset = 0; + + for (int v : tableIndex) { + // escape character + int byteOffset = bitOffset / 8; + int shift = bitOffset % 8; + + septets[byteOffset] |= GSM_ESCAPE_CHARACTER << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (GSM_ESCAPE_CHARACTER >> (8 - shift)); + } + + bitOffset += 7; + + // extended table index + byteOffset = bitOffset / 8; + shift = bitOffset % 8; + + septets[byteOffset] |= v << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (v >> (8 - shift)); + } + + bitOffset += 7; + } + + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, + 0, language); + byte[] reEncoded = GsmAlphabet.stringToGsm7BitPacked(decoded, 0, language); + + assertEquals(sExtendedTables[language], decoded); + + // reEncoded has the count septets byte at the front + assertEquals(septets.length + 1, reEncoded.length); + + for (int i = 0; i < septets.length; i++) { + assertEquals(septets[i], reEncoded[i + 1]); + } + } + } + + @SmallTest + public void testDecodeExtendedFallback() throws Exception { + // verify that unmapped characters in extension table fall back to locking shift table + for (int language = 0; language < 3; language++) { + int[] tableIndex = sExtendedTableIndexes[language]; + int numChars = 128 - tableIndex.length; + int numSeptets = numChars * 2; // two septets per extended char + byte[] septets = new byte[(7 * numSeptets + 7) / 8]; + + int tableOffset = 0; + int bitOffset = 0; + + StringBuilder defaultTable = new StringBuilder(128); + StringBuilder turkishTable = new StringBuilder(128); + StringBuilder portugueseTable = new StringBuilder(128); + + for (char c = 0; c < 128; c++) { + // skip characters that are present in the current extension table + if (tableOffset < tableIndex.length && tableIndex[tableOffset] == c) { + tableOffset++; + continue; + } + + // escape character + int byteOffset = bitOffset / 8; + int shift = bitOffset % 8; + + septets[byteOffset] |= GSM_ESCAPE_CHARACTER << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (GSM_ESCAPE_CHARACTER >> (8 - shift)); + } + + bitOffset += 7; + + // extended table index + byteOffset = bitOffset / 8; + shift = bitOffset % 8; + + septets[byteOffset] |= c << shift; + + if (shift > 1) { + septets[byteOffset + 1] = (byte) (c >> (8 - shift)); + } + + bitOffset += 7; + + if (c == GsmAlphabet.GSM_EXTENDED_ESCAPE) { + // double Escape maps to space character + defaultTable.append(' '); + turkishTable.append(' '); + portugueseTable.append(' '); + } else { + // other unmapped chars map to the default or locking shift table + defaultTable.append(sBasicTables[0].charAt(c)); + turkishTable.append(sBasicTables[1].charAt(c)); + portugueseTable.append(sBasicTables[3].charAt(c)); + } + } + + String decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, + 0, language); + + assertEquals(defaultTable.toString(), decoded); + + decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, 1, language); + assertEquals(turkishTable.toString(), decoded); + + decoded = GsmAlphabet.gsm7BitPackedToString(septets, 0, numSeptets, 0, 3, language); + assertEquals(portugueseTable.toString(), decoded); + } + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/IccServiceTableTest.java b/tests/telephonytests/src/com/android/internal/telephony/IccServiceTableTest.java new file mode 100644 index 0000000..c89f33a --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/IccServiceTableTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +/** + * Test IccServiceTable class. + */ +public class IccServiceTableTest extends AndroidTestCase { + + static class TestIccServiceTable extends IccServiceTable { + public enum TestIccService { + SERVICE1, + SERVICE2, + SERVICE3, + SERVICE4 + } + + public TestIccServiceTable(byte[] table) { + super(table); + } + + public boolean isAvailable(TestIccService service) { + return super.isAvailable(service.ordinal()); + } + + @Override + protected String getTag() { + return "TestIccServiceTable"; + } + + @Override + protected Object[] getValues() { + return TestIccService.values(); + } + } + + @SmallTest + public void testIccServiceTable() { + byte[] noServices = {0x00}; + byte[] service1 = {0x01}; + byte[] service4 = {0x08}; + byte[] allServices = {0x0f}; + + TestIccServiceTable testTable1 = new TestIccServiceTable(noServices); + assertFalse(testTable1.isAvailable(TestIccServiceTable.TestIccService.SERVICE1)); + assertFalse(testTable1.isAvailable(TestIccServiceTable.TestIccService.SERVICE2)); + assertFalse(testTable1.isAvailable(TestIccServiceTable.TestIccService.SERVICE3)); + assertFalse(testTable1.isAvailable(TestIccServiceTable.TestIccService.SERVICE4)); + + TestIccServiceTable testTable2 = new TestIccServiceTable(service1); + assertTrue(testTable2.isAvailable(TestIccServiceTable.TestIccService.SERVICE1)); + assertFalse(testTable2.isAvailable(TestIccServiceTable.TestIccService.SERVICE2)); + assertFalse(testTable2.isAvailable(TestIccServiceTable.TestIccService.SERVICE3)); + assertFalse(testTable2.isAvailable(TestIccServiceTable.TestIccService.SERVICE4)); + + TestIccServiceTable testTable3 = new TestIccServiceTable(service4); + assertFalse(testTable3.isAvailable(TestIccServiceTable.TestIccService.SERVICE1)); + assertFalse(testTable3.isAvailable(TestIccServiceTable.TestIccService.SERVICE2)); + assertFalse(testTable3.isAvailable(TestIccServiceTable.TestIccService.SERVICE3)); + assertTrue(testTable3.isAvailable(TestIccServiceTable.TestIccService.SERVICE4)); + + TestIccServiceTable testTable4 = new TestIccServiceTable(allServices); + assertTrue(testTable4.isAvailable(TestIccServiceTable.TestIccService.SERVICE1)); + assertTrue(testTable4.isAvailable(TestIccServiceTable.TestIccService.SERVICE2)); + assertTrue(testTable4.isAvailable(TestIccServiceTable.TestIccService.SERVICE3)); + assertTrue(testTable4.isAvailable(TestIccServiceTable.TestIccService.SERVICE4)); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/IntRangeManagerTest.java b/tests/telephonytests/src/com/android/internal/telephony/IntRangeManagerTest.java new file mode 100644 index 0000000..79dca39 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/IntRangeManagerTest.java @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2011 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.internal.telephony; + +import android.test.AndroidTestCase; + +import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; + +import java.util.ArrayList; + +/** + * Test cases for the IntRangeManager class. + */ +public class IntRangeManagerTest extends AndroidTestCase { + + private static final int SMS_CB_CODE_SCHEME_MIN = 0; + private static final int SMS_CB_CODE_SCHEME_MAX = 255; + + private static final int FLAG_START_UPDATE_CALLED = 0x01; + private static final int FLAG_ADD_RANGE_CALLED = 0x02; + private static final int FLAG_FINISH_UPDATE_CALLED = 0x04; + + private static final int ALL_FLAGS_SET = FLAG_START_UPDATE_CALLED | FLAG_ADD_RANGE_CALLED | + FLAG_FINISH_UPDATE_CALLED; + + /** Dummy IntRangeManager for testing. */ + class TestIntRangeManager extends IntRangeManager { + ArrayList<SmsBroadcastConfigInfo> mConfigList = + new ArrayList<SmsBroadcastConfigInfo>(); + + int flags; + boolean finishUpdateReturnValue = true; + + /** + * Called when the list of enabled ranges has changed. This will be + * followed by zero or more calls to {@link #addRange} followed by + * a call to {@link #finishUpdate}. + */ + protected void startUpdate() { + mConfigList.clear(); + flags |= FLAG_START_UPDATE_CALLED; + } + + /** + * Called after {@link #startUpdate} to indicate a range of enabled + * values. + * @param startId the first id included in the range + * @param endId the last id included in the range + */ + protected void addRange(int startId, int endId, boolean selected) { + mConfigList.add(new SmsBroadcastConfigInfo(startId, endId, + SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, selected)); + flags |= FLAG_ADD_RANGE_CALLED; + } + + /** + * Called to indicate the end of a range update started by the + * previous call to {@link #startUpdate}. + */ + protected boolean finishUpdate() { + flags |= FLAG_FINISH_UPDATE_CALLED; + return finishUpdateReturnValue; + } + + /** Reset the object for the next test case. */ + void reset() { + flags = 0; + mConfigList.clear(); + } + } + + public void testEmptyRangeManager() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("expecting empty configlist", 0, testManager.mConfigList.size()); + } + + private void checkConfigInfo(SmsBroadcastConfigInfo info, int fromServiceId, + int toServiceId, int fromCodeScheme, int toCodeScheme, boolean selected) { + assertEquals("fromServiceId", fromServiceId, info.getFromServiceId()); + assertEquals("toServiceId", toServiceId, info.getToServiceId()); + assertEquals("fromCodeScheme", fromCodeScheme, info.getFromCodeScheme()); + assertEquals("toCodeScheme", toCodeScheme, info.getToCodeScheme()); + assertEquals("selected", selected, info.isSelected()); + } + + public void testAddSingleChannel() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("flags before test", 0, testManager.flags); + assertTrue("enabling range", testManager.enableRange(123, 123, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 123, 123, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 123, 123, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + } + + public void testRemoveSingleChannel() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertTrue("enabling range", testManager.enableRange(123, 123, "client1")); + assertEquals("flags after enable", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + testManager.reset(); + assertTrue("disabling range", testManager.disableRange(123, 123, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 123, 123, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", FLAG_START_UPDATE_CALLED | FLAG_FINISH_UPDATE_CALLED, + testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + } + + public void testRemoveBadChannel() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertFalse("disabling missing range", testManager.disableRange(123, 123, "client1")); + assertEquals("flags after test", 0, testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + } + + public void testAddTwoChannels() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("flags before test", 0, testManager.flags); + assertTrue("enabling range 1", testManager.enableRange(100, 120, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 120, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 2", testManager.enableRange(200, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 200, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 2, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 120, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 200, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + } + + public void testOverlappingChannels() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("flags before test", 0, testManager.flags); + assertTrue("enabling range 1", testManager.enableRange(100, 200, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 200, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 2", testManager.enableRange(150, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 201, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 1", testManager.disableRange(100, 200, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 149, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("disabling range 2", testManager.disableRange(150, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 150, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", FLAG_START_UPDATE_CALLED | FLAG_FINISH_UPDATE_CALLED, + testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + } + + public void testOverlappingChannels2() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("flags before test", 0, testManager.flags); + assertTrue("enabling range 1", testManager.enableRange(100, 200, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 200, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 2", testManager.enableRange(150, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 201, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 2", testManager.disableRange(150, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 201, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 200, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 1", testManager.disableRange(100, 200, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 100, 200, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + } + + public void testMultipleOverlappingChannels() { + TestIntRangeManager testManager = new TestIntRangeManager(); + assertEquals("flags before test", 0, testManager.flags); + assertTrue("enabling range 1", testManager.enableRange(67, 9999, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 67, 9999, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 2", testManager.enableRange(150, 250, "client2")); + assertEquals("flags after test", 0, testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + testManager.reset(); + assertTrue("enabling range 3", testManager.enableRange(25, 75, "client3")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 66, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 4", testManager.enableRange(12, 500, "client4")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 12, 24, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("enabling range 5", testManager.enableRange(8000, 9998, "client5")); + assertEquals("flags after test", 0, testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + testManager.reset(); + assertTrue("enabling range 6", testManager.enableRange(50000, 65535, "client6")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 2, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 12, 9999, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 1", testManager.disableRange(67, 9999, "client1")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 2, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 501, 7999, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + checkConfigInfo(testManager.mConfigList.get(1), 9999, 9999, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 3, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 12, 500, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 8000, 9998, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(2), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 4", testManager.disableRange(12, 500, "client4")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 3, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 12, 24, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + checkConfigInfo(testManager.mConfigList.get(1), 76, 149, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + checkConfigInfo(testManager.mConfigList.get(2), 251, 500, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 4, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 75, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 150, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(2), 8000, 9998, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(3), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 5", testManager.disableRange(8000, 9998, "client5")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 8000, 9998, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 3, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 75, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 150, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(2), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 6", testManager.disableRange(50000, 65535, "client6")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 50000, 65535, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 2, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 75, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + checkConfigInfo(testManager.mConfigList.get(1), 150, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 2", testManager.disableRange(150, 250, "client2")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 150, 250, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 75, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, true); + testManager.reset(); + assertTrue("disabling range 3", testManager.disableRange(25, 75, "client3")); + assertEquals("flags after test", ALL_FLAGS_SET, testManager.flags); + assertEquals("configlist size", 1, testManager.mConfigList.size()); + checkConfigInfo(testManager.mConfigList.get(0), 25, 75, SMS_CB_CODE_SCHEME_MIN, + SMS_CB_CODE_SCHEME_MAX, false); + testManager.reset(); + assertTrue("updating ranges", testManager.updateRanges()); + assertEquals("flags after test", FLAG_START_UPDATE_CALLED | FLAG_FINISH_UPDATE_CALLED, + testManager.flags); + assertEquals("configlist size", 0, testManager.mConfigList.size()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java b/tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java new file mode 100644 index 0000000..868c76d --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/MccTableTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.MccTable; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import android.util.Log; + +public class MccTableTest extends AndroidTestCase { + private final static String LOG_TAG = "GSM"; + + @SmallTest + public void testTimeZone() throws Exception { + assertEquals(MccTable.defaultTimeZoneForMcc(208), "ECT"); + assertEquals(MccTable.defaultTimeZoneForMcc(232), "Europe/Vienna"); + assertEquals(MccTable.defaultTimeZoneForMcc(655), "Africa/Johannesburg"); + assertEquals(MccTable.defaultTimeZoneForMcc(440), "Asia/Tokyo"); + assertEquals(MccTable.defaultTimeZoneForMcc(441), "Asia/Tokyo"); + assertEquals(MccTable.defaultTimeZoneForMcc(525), "Asia/Singapore"); + assertEquals(MccTable.defaultTimeZoneForMcc(240), null); // tz not defined, hence default + assertEquals(MccTable.defaultTimeZoneForMcc(0), null); // mcc not defined, hence default + assertEquals(MccTable.defaultTimeZoneForMcc(2000), null); // mcc not defined, hence default + } + + @SmallTest + public void testCountryCode() throws Exception { + assertEquals(MccTable.countryCodeForMcc(270), "lu"); + assertEquals(MccTable.countryCodeForMcc(202), "gr"); + assertEquals(MccTable.countryCodeForMcc(750), "fk"); + assertEquals(MccTable.countryCodeForMcc(646), "mg"); + assertEquals(MccTable.countryCodeForMcc(314), "us"); + assertEquals(MccTable.countryCodeForMcc(300), ""); // mcc not defined, hence default + assertEquals(MccTable.countryCodeForMcc(0), ""); // mcc not defined, hence default + assertEquals(MccTable.countryCodeForMcc(2000), ""); // mcc not defined, hence default + } + + @SmallTest + public void testLang() throws Exception { + assertEquals(MccTable.defaultLanguageForMcc(311), "en"); + assertEquals(MccTable.defaultLanguageForMcc(232), "de"); + assertEquals(MccTable.defaultLanguageForMcc(230), "cs"); + assertEquals(MccTable.defaultLanguageForMcc(204), "nl"); + assertEquals(MccTable.defaultLanguageForMcc(274), null); // lang not defined, hence default + assertEquals(MccTable.defaultLanguageForMcc(0), null); // mcc not defined, hence default + assertEquals(MccTable.defaultLanguageForMcc(2000), null); // mcc not defined, hence default + } + + @SmallTest + public void testSmDigits() throws Exception { + assertEquals(MccTable.smallestDigitsMccForMnc(312), 3); + assertEquals(MccTable.smallestDigitsMccForMnc(430), 2); + assertEquals(MccTable.smallestDigitsMccForMnc(365), 3); + assertEquals(MccTable.smallestDigitsMccForMnc(536), 2); + assertEquals(MccTable.smallestDigitsMccForMnc(352), 2); // sd not defined, hence default + assertEquals(MccTable.smallestDigitsMccForMnc(0), 2); // mcc not defined, hence default + assertEquals(MccTable.smallestDigitsMccForMnc(2000), 2); // mcc not defined, hence default + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/NeighboringCellInfoTest.java b/tests/telephonytests/src/com/android/internal/telephony/NeighboringCellInfoTest.java new file mode 100644 index 0000000..b63dc71 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/NeighboringCellInfoTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 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.internal.telephony; + +import android.os.Parcel; +import android.test.AndroidTestCase; +import android.telephony.NeighboringCellInfo; +import android.test. suitebuilder.annotation.SmallTest; + +import static android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN; +import static android.telephony.TelephonyManager.NETWORK_TYPE_EDGE; +import static android.telephony.TelephonyManager.NETWORK_TYPE_GPRS; +import static android.telephony.TelephonyManager.NETWORK_TYPE_UMTS; + +public class NeighboringCellInfoTest extends AndroidTestCase { + @SmallTest + public void testConstructor() { + int rssi = 31; + NeighboringCellInfo nc; + + nc = new NeighboringCellInfo(rssi, "FFFFFFF", NETWORK_TYPE_EDGE); + assertEquals(NETWORK_TYPE_EDGE, nc.getNetworkType()); + assertEquals(rssi, nc.getRssi()); + assertEquals(0xfff, nc.getLac()); + assertEquals(0xffff, nc.getCid()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getPsc()); + + nc = new NeighboringCellInfo(rssi, "1FF", NETWORK_TYPE_UMTS); + assertEquals(NETWORK_TYPE_UMTS, nc.getNetworkType()); + assertEquals(rssi, nc.getRssi()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getCid()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getLac()); + assertEquals(0x1ff, nc.getPsc()); + + nc = new NeighboringCellInfo(rssi, "1FF", NETWORK_TYPE_UNKNOWN); + assertEquals(NETWORK_TYPE_UNKNOWN, nc.getNetworkType()); + assertEquals(rssi, nc.getRssi()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getCid()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getLac()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getPsc()); + } + + @SmallTest + public void testParcel() { + int rssi = 20; + + NeighboringCellInfo nc = new NeighboringCellInfo(rssi, "12345678", NETWORK_TYPE_GPRS); + assertEquals(NETWORK_TYPE_GPRS, nc.getNetworkType()); + assertEquals(rssi, nc.getRssi()); + assertEquals(0x1234, nc.getLac()); + assertEquals(0x5678, nc.getCid()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nc.getPsc()); + + Parcel p = Parcel.obtain(); + p.setDataPosition(0); + nc.writeToParcel(p, 0); + + p.setDataPosition(0); + NeighboringCellInfo nw = new NeighboringCellInfo(p); + assertEquals(NETWORK_TYPE_GPRS, nw.getNetworkType()); + assertEquals(rssi, nw.getRssi()); + assertEquals(0x1234, nw.getLac()); + assertEquals(0x5678, nw.getCid()); + assertEquals(NeighboringCellInfo.UNKNOWN_CID, nw.getPsc()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberUtilsTest.java b/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberUtilsTest.java new file mode 100644 index 0000000..db670f8 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberUtilsTest.java @@ -0,0 +1,657 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.SpannableStringBuilder; +import android.telephony.PhoneNumberUtils; + +public class PhoneNumberUtilsTest extends AndroidTestCase { + + @SmallTest + public void testExtractNetworkPortion() throws Exception { + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortion("+17005554141") + ); + + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortion("+1 (700).555-4141") + ); + + assertEquals( + "17005554141", + PhoneNumberUtils.extractNetworkPortion("1 (700).555-4141") + ); + + // This may seem wrong, but it's probably ok + assertEquals( + "17005554141*#", + PhoneNumberUtils.extractNetworkPortion("1 (700).555-4141*#") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortion("1 (700).555-41NN") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortion("1 (700).555-41NN,1234") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortion("1 (700).555-41NN;1234") + ); + + // An MMI string is unperterbed, even though it contains a + // (valid in this case) embedded + + assertEquals( + "**21**17005554141#", + PhoneNumberUtils.extractNetworkPortion("**21**+17005554141#") + //TODO this is the correct result, although the above + //result has been returned since change 31776 + //"**21**+17005554141#" + ); + + assertEquals("", PhoneNumberUtils.extractNetworkPortion("")); + + assertEquals("", PhoneNumberUtils.extractNetworkPortion(",1234")); + + byte [] b = new byte[20]; + b[0] = (byte) 0x81; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xF0; + assertEquals("17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + b[0] = (byte) 0x80; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xF0; + assertEquals("17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + b[0] = (byte) 0x90; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xF0; + assertEquals("+17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + b[0] = (byte) 0x91; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xF0; + assertEquals("+17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + byte[] bRet = PhoneNumberUtils.networkPortionToCalledPartyBCD("+17005550020"); + assertEquals(7, bRet.length); + for (int i = 0; i < 7; i++) { + assertEquals(b[i], bRet[i]); + } + + bRet = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength("+17005550020"); + assertEquals(8, bRet.length); + assertEquals(bRet[0], 7); + for (int i = 1; i < 8; i++) { + assertEquals(b[i - 1], bRet[i]); + } + + bRet = PhoneNumberUtils.networkPortionToCalledPartyBCD("7005550020"); + assertEquals("7005550020", + PhoneNumberUtils.calledPartyBCDToString(bRet, 0, bRet.length)); + + b[0] = (byte) 0x81; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xB0; + assertEquals("17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + b[0] = (byte) 0x91; b[1] = (byte) 0x71; b[2] = (byte) 0x00; b[3] = (byte) 0x55; + b[4] = (byte) 0x05; b[5] = (byte) 0x20; b[6] = (byte) 0xB0; + assertEquals("+17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 7)); + + b[0] = (byte) 0x81; b[1] = (byte) 0x2A; b[2] = (byte) 0xB1; + assertEquals("*21#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 3)); + + b[0] = (byte) 0x81; b[1] = (byte) 0x2B; b[2] = (byte) 0xB1; + assertEquals("#21#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 3)); + + b[0] = (byte) 0x91; b[1] = (byte) 0x2A; b[2] = (byte) 0xB1; + assertEquals("*21#+", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 3)); + + b[0] = (byte) 0x81; b[1] = (byte) 0xAA; b[2] = (byte) 0x12; b[3] = (byte) 0xFB; + assertEquals("**21#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 4)); + + b[0] = (byte) 0x91; b[1] = (byte) 0xAA; b[2] = (byte) 0x12; b[3] = (byte) 0xFB; + assertEquals("**21#+", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 4)); + + b[0] = (byte) 0x81; b[1] = (byte) 0x9A; b[2] = (byte) 0xA9; b[3] = (byte) 0x71; + b[4] = (byte) 0x00; b[5] = (byte) 0x55; b[6] = (byte) 0x05; b[7] = (byte) 0x20; + b[8] = (byte) 0xB0; + assertEquals("*99*17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 9)); + + b[0] = (byte) 0x91; b[1] = (byte) 0x9A; b[2] = (byte) 0xA9; b[3] = (byte) 0x71; + b[4] = (byte) 0x00; b[5] = (byte) 0x55; b[6] = (byte) 0x05; b[7] = (byte) 0x20; + b[8] = (byte) 0xB0; + assertEquals("*99*+17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 9)); + + b[0] = (byte) 0x81; b[1] = (byte) 0xAA; b[2] = (byte) 0x12; b[3] = (byte) 0x1A; + b[4] = (byte) 0x07; b[5] = (byte) 0x50; b[6] = (byte) 0x55; b[7] = (byte) 0x00; + b[8] = (byte) 0x02; b[9] = (byte) 0xFB; + assertEquals("**21*17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 10)); + + b[0] = (byte) 0x91; b[1] = (byte) 0xAA; b[2] = (byte) 0x12; b[3] = (byte) 0x1A; + b[4] = (byte) 0x07; b[5] = (byte) 0x50; b[6] = (byte) 0x55; b[7] = (byte) 0x00; + b[8] = (byte) 0x02; b[9] = (byte) 0xFB; + assertEquals("**21*+17005550020#", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 10)); + + b[0] = (byte) 0x81; b[1] = (byte) 0x2A; b[2] = (byte) 0xA1; b[3] = (byte) 0x71; + b[4] = (byte) 0x00; b[5] = (byte) 0x55; b[6] = (byte) 0x05; b[7] = (byte) 0x20; + b[8] = (byte) 0xF0; + assertEquals("*21*17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 9)); + + b[0] = (byte) 0x91; b[1] = (byte) 0x2A; b[2] = (byte) 0xB1; b[3] = (byte) 0x71; + b[4] = (byte) 0x00; b[5] = (byte) 0x55; b[6] = (byte) 0x05; b[7] = (byte) 0x20; + b[8] = (byte) 0xF0; + assertEquals("*21#+17005550020", + PhoneNumberUtils.calledPartyBCDToString(b, 0, 9)); + + assertNull(PhoneNumberUtils.extractNetworkPortion(null)); + assertNull(PhoneNumberUtils.extractPostDialPortion(null)); + assertTrue(PhoneNumberUtils.compare(null, null)); + assertFalse(PhoneNumberUtils.compare(null, "123")); + assertFalse(PhoneNumberUtils.compare("123", null)); + assertNull(PhoneNumberUtils.toCallerIDMinMatch(null)); + assertNull(PhoneNumberUtils.getStrippedReversed(null)); + assertNull(PhoneNumberUtils.stringFromStringAndTOA(null, 1)); + } + + @SmallTest + public void testExtractNetworkPortionAlt() throws Exception { + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("+17005554141") + ); + + assertEquals( + "+17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("+1 (700).555-4141") + ); + + assertEquals( + "17005554141", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-4141") + ); + + // This may seem wrong, but it's probably ok + assertEquals( + "17005554141*#", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-4141*#") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN,1234") + ); + + assertEquals( + "170055541NN", + PhoneNumberUtils.extractNetworkPortionAlt("1 (700).555-41NN;1234") + ); + + // An MMI string is unperterbed, even though it contains a + // (valid in this case) embedded + + assertEquals( + "**21**+17005554141#", + PhoneNumberUtils.extractNetworkPortionAlt("**21**+17005554141#") + ); + + assertEquals( + "*31#+447966164208", + PhoneNumberUtils.extractNetworkPortionAlt("*31#+447966164208") + ); + + assertEquals( + "*31#+447966164208", + PhoneNumberUtils.extractNetworkPortionAlt("*31# (+44) 79 6616 4208") + ); + + assertEquals("", PhoneNumberUtils.extractNetworkPortionAlt("")); + + assertEquals("", PhoneNumberUtils.extractNetworkPortionAlt(",1234")); + + assertNull(PhoneNumberUtils.extractNetworkPortionAlt(null)); + } + + @SmallTest + public void testB() throws Exception { + assertEquals("", PhoneNumberUtils.extractPostDialPortion("+17005554141")); + assertEquals("", PhoneNumberUtils.extractPostDialPortion("+1 (700).555-4141")); + assertEquals("", PhoneNumberUtils.extractPostDialPortion("+1 (700).555-41NN")); + assertEquals(",1234", PhoneNumberUtils.extractPostDialPortion("+1 (700).555-41NN,1234")); + assertEquals(";1234", PhoneNumberUtils.extractPostDialPortion("+1 (700).555-41NN;1234")); + assertEquals(";1234,;N", + PhoneNumberUtils.extractPostDialPortion("+1 (700).555-41NN;1-2.34 ,;N")); + } + + @SmallTest + public void testCompare() throws Exception { + // this is odd + assertFalse(PhoneNumberUtils.compare("", "")); + + assertTrue(PhoneNumberUtils.compare("911", "911")); + assertFalse(PhoneNumberUtils.compare("911", "18005550911")); + assertTrue(PhoneNumberUtils.compare("5555", "5555")); + assertFalse(PhoneNumberUtils.compare("5555", "180055555555")); + + assertTrue(PhoneNumberUtils.compare("+17005554141", "+17005554141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "+1 (700).555-4141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "+1 (700).555-4141,1234")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "17005554141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "7005554141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "5554141")); + assertTrue(PhoneNumberUtils.compare("17005554141", "5554141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "01117005554141")); + assertTrue(PhoneNumberUtils.compare("+17005554141", "0017005554141")); + assertTrue(PhoneNumberUtils.compare("17005554141", "0017005554141")); + + + assertTrue(PhoneNumberUtils.compare("+17005554141", "**31#+17005554141")); + + assertFalse(PhoneNumberUtils.compare("+1 999 7005554141", "+1 7005554141")); + assertTrue(PhoneNumberUtils.compare("011 1 7005554141", "7005554141")); + + assertFalse(PhoneNumberUtils.compare("011 11 7005554141", "+17005554141")); + + assertFalse(PhoneNumberUtils.compare("+17005554141", "7085882300")); + + assertTrue(PhoneNumberUtils.compare("+44 207 792 3490", "0 207 792 3490")); + + assertFalse(PhoneNumberUtils.compare("+44 207 792 3490", "00 207 792 3490")); + assertFalse(PhoneNumberUtils.compare("+44 207 792 3490", "011 207 792 3490")); + + /***** FIXME's ******/ + // + // MMI header should be ignored + assertFalse(PhoneNumberUtils.compare("+17005554141", "**31#17005554141")); + + // It's too bad this is false + // +44 (0) 207 792 3490 is not a dialable number + // but it is commonly how European phone numbers are written + assertFalse(PhoneNumberUtils.compare("+44 207 792 3490", "+44 (0) 207 792 3490")); + + // The japanese international prefix, for example, messes us up + // But who uses a GSM phone in Japan? + assertFalse(PhoneNumberUtils.compare("+44 207 792 3490", "010 44 207 792 3490")); + + // The Australian one messes us up too + assertFalse(PhoneNumberUtils.compare("+44 207 792 3490", "0011 44 207 792 3490")); + + // The Russian trunk prefix messes us up, as does current + // Russian area codes (which bein with 0) + + assertFalse(PhoneNumberUtils.compare("+7(095)9100766", "8(095)9100766")); + + // 444 is not a valid country code, but + // matchIntlPrefixAndCC doesnt know this + assertTrue(PhoneNumberUtils.compare("+444 207 792 3490", "0 207 792 3490")); + + // compare SMS short code + assertTrue(PhoneNumberUtils.compare("404-04", "40404")); + } + + + @SmallTest + public void testToCallerIDIndexable() throws Exception { + assertEquals("1414555", PhoneNumberUtils.toCallerIDMinMatch("17005554141")); + assertEquals("1414555", PhoneNumberUtils.toCallerIDMinMatch("1-700-555-4141")); + assertEquals("1414555", PhoneNumberUtils.toCallerIDMinMatch("1-700-555-4141,1234")); + assertEquals("1414555", PhoneNumberUtils.toCallerIDMinMatch("1-700-555-4141;1234")); + + //this seems wrong, or at least useless + assertEquals("NN14555", PhoneNumberUtils.toCallerIDMinMatch("1-700-555-41NN")); + + //<shrug> -- these are all not useful, but not terribly wrong + assertEquals("", PhoneNumberUtils.toCallerIDMinMatch("")); + assertEquals("0032", PhoneNumberUtils.toCallerIDMinMatch("2300")); + assertEquals("0032+", PhoneNumberUtils.toCallerIDMinMatch("+2300")); + assertEquals("#130#*", PhoneNumberUtils.toCallerIDMinMatch("*#031#")); + } + + @SmallTest + public void testGetIndexable() throws Exception { + assertEquals("14145550071", PhoneNumberUtils.getStrippedReversed("1-700-555-4141")); + assertEquals("14145550071", PhoneNumberUtils.getStrippedReversed("1-700-555-4141,1234")); + assertEquals("14145550071", PhoneNumberUtils.getStrippedReversed("1-700-555-4141;1234")); + + //this seems wrong, or at least useless + assertEquals("NN145550071", PhoneNumberUtils.getStrippedReversed("1-700-555-41NN")); + + //<shrug> -- these are all not useful, but not terribly wrong + assertEquals("", PhoneNumberUtils.getStrippedReversed("")); + assertEquals("0032", PhoneNumberUtils.getStrippedReversed("2300")); + assertEquals("0032+", PhoneNumberUtils.getStrippedReversed("+2300")); + assertEquals("#130#*", PhoneNumberUtils.getStrippedReversed("*#031#")); + } + + @SmallTest + public void testNanpFormatting() { + SpannableStringBuilder number = new SpannableStringBuilder(); + number.append("8005551212"); + PhoneNumberUtils.formatNanpNumber(number); + assertEquals("800-555-1212", number.toString()); + + number.clear(); + number.append("800555121"); + PhoneNumberUtils.formatNanpNumber(number); + assertEquals("800-555-121", number.toString()); + + number.clear(); + number.append("555-1212"); + PhoneNumberUtils.formatNanpNumber(number); + assertEquals("555-1212", number.toString()); + + number.clear(); + number.append("800-55512"); + PhoneNumberUtils.formatNanpNumber(number); + assertEquals("800-555-12", number.toString()); + + number.clear(); + number.append("46645"); + PhoneNumberUtils.formatNanpNumber(number); + assertEquals("46645", number.toString()); + } + + @SmallTest + public void testConvertKeypadLettersToDigits() { + assertEquals("1-800-4664-411", + PhoneNumberUtils.convertKeypadLettersToDigits("1-800-GOOG-411")); + assertEquals("18004664411", + PhoneNumberUtils.convertKeypadLettersToDigits("1800GOOG411")); + assertEquals("1-800-466-4411", + PhoneNumberUtils.convertKeypadLettersToDigits("1-800-466-4411")); + assertEquals("18004664411", + PhoneNumberUtils.convertKeypadLettersToDigits("18004664411")); + assertEquals("222-333-444-555-666-7777-888-9999", + PhoneNumberUtils.convertKeypadLettersToDigits( + "ABC-DEF-GHI-JKL-MNO-PQRS-TUV-WXYZ")); + assertEquals("222-333-444-555-666-7777-888-9999", + PhoneNumberUtils.convertKeypadLettersToDigits( + "abc-def-ghi-jkl-mno-pqrs-tuv-wxyz")); + assertEquals("(800) 222-3334", + PhoneNumberUtils.convertKeypadLettersToDigits("(800) ABC-DEFG")); + } + + // To run this test, the device has to be registered with network + public void testCheckAndProcessPlusCode() { + assertEquals("0118475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+8475797000")); + assertEquals("18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+18475797000")); + assertEquals("0111234567", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+1234567")); + assertEquals("01123456700000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+23456700000")); + assertEquals("01111875767800", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+11875767800")); + assertEquals("8475797000,18475231753", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,+18475231753")); + assertEquals("0118475797000,18475231753", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+8475797000,+18475231753")); + assertEquals("8475797000;0118469312345", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000;+8469312345")); + assertEquals("8475797000,0111234567", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,+1234567")); + assertEquals("847597000;01111875767000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("847597000;+11875767000")); + assertEquals("8475797000,,0118469312345", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,,+8469312345")); + assertEquals("8475797000;,0118469312345", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000;,+8469312345")); + assertEquals("8475797000,;18475231753", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,;+18475231753")); + assertEquals("8475797000;,01111875767000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000;,+11875767000")); + assertEquals("8475797000,;01111875767000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,;+11875767000")); + assertEquals("8475797000,,,01111875767000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,,,+11875767000")); + assertEquals("8475797000;,,01111875767000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000;,,+11875767000")); + assertEquals("+;,8475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+;,8475797000")); + assertEquals("8475797000,", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("8475797000,")); + assertEquals("847+579-7000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("847+579-7000")); + assertEquals(",8475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode(",8475797000")); + assertEquals(";;8475797000,,", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode(";;8475797000,,")); + assertEquals("+this+is$weird;,+", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("+this+is$weird;,+")); + assertEquals("", + PhoneNumberUtils.cdmaCheckAndProcessPlusCode("")); + assertNull(PhoneNumberUtils.cdmaCheckAndProcessPlusCode(null)); + } + + @SmallTest + public void testCheckAndProcessPlusCodeByNumberFormat() { + assertEquals("18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeByNumberFormat("+18475797000", + PhoneNumberUtils.FORMAT_NANP,PhoneNumberUtils.FORMAT_NANP)); + assertEquals("+18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeByNumberFormat("+18475797000", + PhoneNumberUtils.FORMAT_NANP,PhoneNumberUtils.FORMAT_JAPAN)); + assertEquals("+18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeByNumberFormat("+18475797000", + PhoneNumberUtils.FORMAT_NANP,PhoneNumberUtils.FORMAT_UNKNOWN)); + assertEquals("+18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeByNumberFormat("+18475797000", + PhoneNumberUtils.FORMAT_JAPAN,PhoneNumberUtils.FORMAT_JAPAN)); + assertEquals("+18475797000", + PhoneNumberUtils.cdmaCheckAndProcessPlusCodeByNumberFormat("+18475797000", + PhoneNumberUtils.FORMAT_UNKNOWN,PhoneNumberUtils.FORMAT_UNKNOWN)); + } + + /** + * Basic checks for the VoiceMail number. + */ + @SmallTest + public void testWithNumberNotEqualToVoiceMail() throws Exception { + assertFalse(PhoneNumberUtils.isVoiceMailNumber("911")); + assertFalse(PhoneNumberUtils.isVoiceMailNumber("tel:911")); + assertFalse(PhoneNumberUtils.isVoiceMailNumber("+18001234567")); + assertFalse(PhoneNumberUtils.isVoiceMailNumber("")); + assertFalse(PhoneNumberUtils.isVoiceMailNumber(null)); + // This test fails on a device without a sim card + /*TelephonyManager mTelephonyManager = + (TelephonyManager)getContext().getSystemService(Context.TELEPHONY_SERVICE); + String mVoiceMailNumber = mTelephonyManager.getDefault().getVoiceMailNumber(); + assertTrue(PhoneNumberUtils.isVoiceMailNumber(mVoiceMailNumber)); + */ + } + + @SmallTest + public void testFormatNumberToE164() { + // Note: ISO 3166-1 only allows upper case country codes. + assertEquals("+16502910000", PhoneNumberUtils.formatNumberToE164("650 2910000", "US")); + assertNull(PhoneNumberUtils.formatNumberToE164("1234567", "US")); + assertEquals("+18004664114", PhoneNumberUtils.formatNumberToE164("800-GOOG-114", "US")); + } + + @SmallTest + public void testFormatNumber() { + assertEquals("(650) 291-0000", PhoneNumberUtils.formatNumber("650 2910000", "US")); + assertEquals("223-4567", PhoneNumberUtils.formatNumber("2234567", "US")); + assertEquals("011 86 10 8888 0000", + PhoneNumberUtils.formatNumber("011861088880000", "US")); + assertEquals("010 8888 0000", PhoneNumberUtils.formatNumber("01088880000", "CN")); + // formatNumber doesn't format alpha numbers, but keep them as they are. + assertEquals("800-GOOG-114", PhoneNumberUtils.formatNumber("800-GOOG-114", "US")); + } + + @SmallTest + public void testFormatNumber_LeadingStarAndHash() { + // Numbers with a leading '*' or '#' should be left unchanged. + assertEquals("*650 2910000", PhoneNumberUtils.formatNumber("*650 2910000", "US")); + assertEquals("#650 2910000", PhoneNumberUtils.formatNumber("#650 2910000", "US")); + assertEquals("*#650 2910000", PhoneNumberUtils.formatNumber("*#650 2910000", "US")); + assertEquals("#*650 2910000", PhoneNumberUtils.formatNumber("#*650 2910000", "US")); + assertEquals("#650*2910000", PhoneNumberUtils.formatNumber("#650*2910000", "US")); + assertEquals("#650*2910000", PhoneNumberUtils.formatNumber("#650*2910000", "US")); + assertEquals("##650 2910000", PhoneNumberUtils.formatNumber("##650 2910000", "US")); + assertEquals("**650 2910000", PhoneNumberUtils.formatNumber("**650 2910000", "US")); + } + + @SmallTest + public void testNormalizeNumber() { + assertEquals("6502910000", PhoneNumberUtils.normalizeNumber("650 2910000")); + assertEquals("1234567", PhoneNumberUtils.normalizeNumber("12,3#4*567")); + assertEquals("8004664114", PhoneNumberUtils.normalizeNumber("800-GOOG-114")); + assertEquals("+16502910000", PhoneNumberUtils.normalizeNumber("+1 650 2910000")); + } + + @SmallTest + public void testFormatDailabeNumber() { + // Using the phoneNumberE164's country code + assertEquals("(650) 291-0000", + PhoneNumberUtils.formatNumber("6502910000", "+16502910000", "CN")); + // Using the default country code for a phone number containing the IDD + assertEquals("011 86 10 8888 0000", + PhoneNumberUtils.formatNumber("011861088880000", "+861088880000", "US")); + assertEquals("00 86 10 8888 0000", + PhoneNumberUtils.formatNumber("00861088880000", "+861088880000", "GB")); + assertEquals("+86 10 8888 0000", + PhoneNumberUtils.formatNumber("+861088880000", "+861088880000", "GB")); + // Wrong default country, so no formatting is done + assertEquals("011861088880000", + PhoneNumberUtils.formatNumber("011861088880000", "+861088880000", "GB")); + // The phoneNumberE164 is null + assertEquals("(650) 291-0000", PhoneNumberUtils.formatNumber("6502910000", null, "US")); + // The given number has a country code. + assertEquals("+1 650-291-0000", PhoneNumberUtils.formatNumber("+16502910000", null, "CN")); + // The given number was formatted. + assertEquals("650-291-0000", PhoneNumberUtils.formatNumber("650-291-0000", null, "US")); + // A valid Polish number should be formatted. + assertEquals("506 128 687", PhoneNumberUtils.formatNumber("506128687", null, "PL")); + // An invalid Polish number should be left as it is. Note Poland doesn't use '0' as a + // national prefix; therefore, the leading '0' makes the number invalid. + assertEquals("0506128687", PhoneNumberUtils.formatNumber("0506128687", null, "PL")); + // Wrong default country, so no formatting is done + assertEquals("011861088880000", + PhoneNumberUtils.formatNumber("011861088880000", "", "GB")); + } + + @SmallTest + public void testIsEmergencyNumber() { + // There are two parallel sets of tests here: one for the + // regular isEmergencyNumber() method, and the other for + // isPotentialEmergencyNumber(). + // + // (The difference is that isEmergencyNumber() will return true + // only if the specified number exactly matches an actual + // emergency number, but isPotentialEmergencyNumber() will + // return true if the specified number simply starts with the + // same digits as any actual emergency number.) + + // Tests for isEmergencyNumber(): + assertTrue(PhoneNumberUtils.isEmergencyNumber("911", "US")); + assertTrue(PhoneNumberUtils.isEmergencyNumber("112", "US")); + // The next two numbers are not valid phone numbers in the US, + // so do not count as emergency numbers (but they *are* "potential" + // emergency numbers; see below.) + assertFalse(PhoneNumberUtils.isEmergencyNumber("91112345", "US")); + assertFalse(PhoneNumberUtils.isEmergencyNumber("11212345", "US")); + // A valid mobile phone number from Singapore shouldn't be classified as an emergency number + // in Singapore, as 911 is not an emergency number there. + assertFalse(PhoneNumberUtils.isEmergencyNumber("91121234", "SG")); + // A valid fixed-line phone number from Brazil shouldn't be classified as an emergency number + // in Brazil, as 112 is not an emergency number there. + assertFalse(PhoneNumberUtils.isEmergencyNumber("1121234567", "BR")); + // A valid local phone number from Brazil shouldn't be classified as an emergency number in + // Brazil. + assertFalse(PhoneNumberUtils.isEmergencyNumber("91112345", "BR")); + + // Tests for isPotentialEmergencyNumber(): + // These first two are obviously emergency numbers: + assertTrue(PhoneNumberUtils.isPotentialEmergencyNumber("911", "US")); + assertTrue(PhoneNumberUtils.isPotentialEmergencyNumber("112", "US")); + // The next two numbers are not valid phone numbers in the US, but can be used to trick the + // system to dial 911 and 112, which are emergency numbers in the US. For the purpose of + // addressing that, they are also classified as "potential" emergency numbers in the US. + assertTrue(PhoneNumberUtils.isPotentialEmergencyNumber("91112345", "US")); + assertTrue(PhoneNumberUtils.isPotentialEmergencyNumber("11212345", "US")); + + // A valid mobile phone number from Singapore shouldn't be classified as an emergency number + // in Singapore, as 911 is not an emergency number there. + // This test fails on devices that have ecclist property preloaded with 911. + // assertFalse(PhoneNumberUtils.isPotentialEmergencyNumber("91121234", "SG")); + + // A valid fixed-line phone number from Brazil shouldn't be classified as an emergency number + // in Brazil, as 112 is not an emergency number there. + assertFalse(PhoneNumberUtils.isPotentialEmergencyNumber("1121234567", "BR")); + // A valid local phone number from Brazil shouldn't be classified as an emergency number in + // Brazil. + assertFalse(PhoneNumberUtils.isPotentialEmergencyNumber("91112345", "BR")); + } + + @SmallTest + public void testStripSeparators() { + // Smoke tests which should never fail. + assertEquals("1234567890", PhoneNumberUtils.stripSeparators("1234567890")); + assertEquals("911", PhoneNumberUtils.stripSeparators("911")); + assertEquals("112", PhoneNumberUtils.stripSeparators("112")); + + // Separators should be removed, while '+' or any other digits should not. + assertEquals("+16502910000", PhoneNumberUtils.stripSeparators("+1 (650) 291-0000")); + + // WAIT, PAUSE should *not* be stripped + assertEquals("+16502910000,300;", + PhoneNumberUtils.stripSeparators("+1 (650) 291-0000, 300;")); + } + + @SmallTest + public void testConvertAndStrip() { + // Smoke tests which should never fail. + assertEquals("1234567890", PhoneNumberUtils.convertAndStrip("1234567890")); + assertEquals("911", PhoneNumberUtils.convertAndStrip("911")); + assertEquals("112", PhoneNumberUtils.convertAndStrip("112")); + + // It should convert keypad characters into digits, and strip separators + assertEquals("22233344455566677778889999", + PhoneNumberUtils.convertAndStrip("ABC DEF GHI JKL MNO PQR STUV WXYZ")); + + // Test real cases. + assertEquals("18004664411", PhoneNumberUtils.convertAndStrip("1-800-GOOG-411")); + assertEquals("8002223334", PhoneNumberUtils.convertAndStrip("(800) ABC-DEFG")); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java b/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java new file mode 100644 index 0000000..a6a0fce --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/PhoneNumberWatcherTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 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 com.android.internal.telephony; + +import android.telephony.PhoneNumberFormattingTextWatcher; +import android.test.AndroidTestCase; +import android.text.Editable; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.TextWatcher; + +public class PhoneNumberWatcherTest extends AndroidTestCase { + public void testAppendChars() { + final String multiChars = "65012345"; + final String formatted1 = "(650) 123-45"; + TextWatcher textWatcher = getTextWatcher(); + SpannableStringBuilder number = new SpannableStringBuilder(); + // Append more than one chars + textWatcher.beforeTextChanged(number, 0, 0, multiChars.length()); + number.append(multiChars); + Selection.setSelection(number, number.length()); + textWatcher.onTextChanged(number, 0, 0, number.length()); + textWatcher.afterTextChanged(number); + assertEquals(formatted1, number.toString()); + assertEquals(formatted1.length(), Selection.getSelectionEnd(number)); + // Append one chars + final char appendChar = '6'; + final String formatted2 = "(650) 123-456"; + int len = number.length(); + textWatcher.beforeTextChanged(number, number.length(), 0, 1); + number.append(appendChar); + Selection.setSelection(number, number.length()); + textWatcher.onTextChanged(number, len, 0, 1); + textWatcher.afterTextChanged(number); + assertEquals(formatted2, number.toString()); + assertEquals(formatted2.length(), Selection.getSelectionEnd(number)); + } + + public void testRemoveLastChars() { + final String init = "65012345678"; + final String result1 = "(650) 123-4567"; + TextWatcher textWatcher = getTextWatcher(); + // Remove the last char. + SpannableStringBuilder number = new SpannableStringBuilder(init); + int len = number.length(); + textWatcher.beforeTextChanged(number, len - 1, 1, 0); + number.delete(len - 1, len); + Selection.setSelection(number, number.length()); + textWatcher.onTextChanged(number, number.length() - 1, 1, 0); + textWatcher.afterTextChanged(number); + assertEquals(result1, number.toString()); + assertEquals(result1.length(), Selection.getSelectionEnd(number)); + // Remove last 5 chars + final String result2 = "650-123"; + textWatcher.beforeTextChanged(number, number.length() - 4, 4, 0); + number.delete(number.length() - 5, number.length()); + Selection.setSelection(number, number.length()); + textWatcher.onTextChanged(number, number.length(), 4, 0); + textWatcher.afterTextChanged(number); + assertEquals(result2, number.toString()); + assertEquals(result2.length(), Selection.getSelectionEnd(number)); + } + + public void testInsertChars() { + final String init = "650-23"; + final String expected1 = "650-123"; + TextWatcher textWatcher = getTextWatcher(); + + // Insert one char + SpannableStringBuilder number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 3, 0, 1); + number.insert(3, "1"); // 6501-23 + Selection.setSelection(number, 4); // make the cursor at right of 1 + textWatcher.onTextChanged(number, 3, 0, 1); + textWatcher.afterTextChanged(number); + assertEquals(expected1, number.toString()); + // the cursor should still at the right of '1' + assertEquals(5, Selection.getSelectionEnd(number)); + + // Insert multiple chars + final String expected2 = "(650) 145-6723"; + textWatcher.beforeTextChanged(number, 5, 0, 4); + number.insert(5, "4567"); // change to 650-1456723 + Selection.setSelection(number, 9); // the cursor is at the right of '7'. + textWatcher.onTextChanged(number, 7, 0, 4); + textWatcher.afterTextChanged(number); + assertEquals(expected2, number.toString()); + // the cursor should be still at the right of '7' + assertEquals(12, Selection.getSelectionEnd(number)); + } + + public void testStopFormatting() { + final String init = "(650) 123"; + final String expected1 = "(650) 123 4"; + TextWatcher textWatcher = getTextWatcher(); + + // Append space + SpannableStringBuilder number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 9, 0, 2); + number.insert(9, " 4"); // (6501) 23 4 + Selection.setSelection(number, number.length()); // make the cursor at right of 4 + textWatcher.onTextChanged(number, 9, 0, 2); + textWatcher.afterTextChanged(number); + assertEquals(expected1, number.toString()); + // the cursor should still at the right of '1' + assertEquals(expected1.length(), Selection.getSelectionEnd(number)); + + // Delete a ')' + final String expected2 ="(650 123"; + textWatcher = getTextWatcher(); + number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 4, 1, 0); + number.delete(4, 5); // (6501 23 4 + Selection.setSelection(number, 5); // make the cursor at right of 1 + textWatcher.onTextChanged(number, 4, 1, 0); + textWatcher.afterTextChanged(number); + assertEquals(expected2, number.toString()); + // the cursor should still at the right of '1' + assertEquals(5, Selection.getSelectionEnd(number)); + + // Insert a hyphen + final String expected3 ="(650) 12-3"; + textWatcher = getTextWatcher(); + number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 8, 0, 1); + number.insert(8, "-"); // (650) 12-3 + Selection.setSelection(number, 9); // make the cursor at right of - + textWatcher.onTextChanged(number, 8, 0, 1); + textWatcher.afterTextChanged(number); + assertEquals(expected3, number.toString()); + // the cursor should still at the right of '-' + assertEquals(9, Selection.getSelectionEnd(number)); + } + + public void testRestartFormatting() { + final String init = "(650) 123"; + final String expected1 = "(650) 123 4"; + TextWatcher textWatcher = getTextWatcher(); + + // Append space + SpannableStringBuilder number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 9, 0, 2); + number.insert(9, " 4"); // (650) 123 4 + Selection.setSelection(number, number.length()); // make the cursor at right of 4 + textWatcher.onTextChanged(number, 9, 0, 2); + textWatcher.afterTextChanged(number); + assertEquals(expected1, number.toString()); + // the cursor should still at the right of '4' + assertEquals(expected1.length(), Selection.getSelectionEnd(number)); + + // Clear the current string, and start formatting again. + int len = number.length(); + textWatcher.beforeTextChanged(number, 0, len, 0); + number.delete(0, len); + textWatcher.onTextChanged(number, 0, len, 0); + textWatcher.afterTextChanged(number); + + final String expected2 = "650-1234"; + number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 9, 0, 1); + number.insert(9, "4"); // (650) 1234 + Selection.setSelection(number, number.length()); // make the cursor at right of 4 + textWatcher.onTextChanged(number, 9, 0, 1); + textWatcher.afterTextChanged(number); + assertEquals(expected2, number.toString()); + // the cursor should still at the right of '4' + assertEquals(expected2.length(), Selection.getSelectionEnd(number)); + } + + public void testTextChangedByOtherTextWatcher() { + final TextWatcher cleanupTextWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + s.clear(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, + int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, + int count) { + } + }; + final String init = "(650) 123"; + final String expected1 = ""; + TextWatcher textWatcher = getTextWatcher(); + + SpannableStringBuilder number = new SpannableStringBuilder(init); + textWatcher.beforeTextChanged(number, 5, 0, 1); + number.insert(5, "4"); // (6504) 123 + Selection.setSelection(number, 5); // make the cursor at right of 4 + textWatcher.onTextChanged(number, 5, 0, 1); + number.setSpan(cleanupTextWatcher, 0, number.length(), 0); + textWatcher.afterTextChanged(number); + assertEquals(expected1, number.toString()); + } + + /** + * Test the case where some other component is auto-completing what the user is typing + */ + public void testAutoCompleteWithFormattedNumber() { + String init = "650-1"; + String expected = "+1-650-123-4567"; // Different formatting than ours + testReplacement(init, expected, expected); + } + + /** + * Test the case where some other component is auto-completing what the user is typing + */ + public void testAutoCompleteWithFormattedNameAndNumber() { + String init = "650-1"; + String expected = "Test User <650-123-4567>"; + testReplacement(init, expected, expected); + } + + /** + * Test the case where some other component is auto-completing what the user is typing + */ + public void testAutoCompleteWithNumericNameAndNumber() { + String init = "650"; + String expected = "2nd Test User <650-123-4567>"; + testReplacement(init, expected, expected); + } + + /** + * Test the case where some other component is auto-completing what the user is typing + */ + public void testAutoCompleteWithUnformattedNumber() { + String init = "650-1"; + String expected = "6501234567"; + testReplacement(init, expected, expected); + } + + /** + * Test the case where some other component is auto-completing what the user is typing, where + * the deleted text doesn't have any formatting and neither does the replacement text: in this + * case the replacement text should be formatted by the PhoneNumberFormattingTextWatcher. + */ + public void testAutoCompleteUnformattedWithUnformattedNumber() { + String init = "650"; + String replacement = "6501234567"; + String expected = "(650) 123-4567"; + testReplacement(init, replacement, expected); + + String init2 = "650"; + String replacement2 = "16501234567"; + String expected2 = "1 650-123-4567"; + testReplacement(init2, replacement2, expected2); + } + + /** + * Helper method for testing replacing the entire string with another string + * @param init The initial string + * @param expected + */ + private void testReplacement(String init, String replacement, String expected) { + TextWatcher textWatcher = getTextWatcher(); + + SpannableStringBuilder number = new SpannableStringBuilder(init); + + // Replace entire text with the given values + textWatcher.beforeTextChanged(number, 0, init.length(), replacement.length()); + number.replace(0, init.length(), replacement, 0, replacement.length()); + Selection.setSelection(number, replacement.length()); // move the cursor to the end + textWatcher.onTextChanged(number, 0, init.length(), replacement.length()); + textWatcher.afterTextChanged(number); + + assertEquals(expected, number.toString()); + // the cursor should be still at the end + assertEquals(expected.length(), Selection.getSelectionEnd(number)); + } + + private TextWatcher getTextWatcher() { + return new PhoneNumberFormattingTextWatcher("US"); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/SMSDispatcherTest.java b/tests/telephonytests/src/com/android/internal/telephony/SMSDispatcherTest.java new file mode 100644 index 0000000..8a66614 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/SMSDispatcherTest.java @@ -0,0 +1,105 @@ +/* + * 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 com.android.internal.telephony; + +import android.test.suitebuilder.annotation.MediumTest; +import com.android.internal.telephony.TestPhoneNotifier; +import com.android.internal.telephony.gsm.SmsMessage; +import com.android.internal.telephony.test.SimulatedCommands; +import com.android.internal.telephony.test.SimulatedRadioControl; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.Suppress; + +import java.util.Iterator; + +/** + * {@hide} + */ +public class SMSDispatcherTest extends AndroidTestCase { + @MediumTest + public void testCMT1() throws Exception { + SmsMessage sms; + SmsHeader header; + + String[] lines = new String[2]; + + lines[0] = "+CMT: ,158"; + lines[1] = "07914140279510F6440A8111110301003BF56080426101748A8C0B05040B" + + "8423F000035502010106276170706C69636174696F6E2F766E642E776170" + + "2E6D6D732D6D65737361676500AF848D0185B4848C8298524F347839776F" + + "7547514D4141424C3641414141536741415A4B554141414141008D908918" + + "802B31363530323438363137392F545950453D504C4D4E008A808E028000" + + "88058103093A8083687474703A2F2F36"; + + sms = SmsMessage.newFromCMT(lines); + header = sms.getUserDataHeader(); + assertNotNull(header); + assertNotNull(sms.getUserData()); + assertNotNull(header.concatRef); + assertEquals(header.concatRef.refNumber, 85); + assertEquals(header.concatRef.msgCount, 2); + assertEquals(header.concatRef.seqNumber, 1); + assertEquals(header.concatRef.isEightBits, true); + assertNotNull(header.portAddrs); + assertEquals(header.portAddrs.destPort, 2948); + assertEquals(header.portAddrs.origPort, 9200); + assertEquals(header.portAddrs.areEightBits, false); + } + + @MediumTest + public void testCMT2() throws Exception { + SmsMessage sms; + SmsHeader header; + + String[] lines = new String[2]; + + lines[0] = "+CMT: ,77"; + lines[1] = "07914140279510F6440A8111110301003BF56080426101848A3B0B05040B8423F" + + "00003550202362E3130322E3137312E3135302F524F347839776F7547514D4141" + + "424C3641414141536741415A4B55414141414100"; + + sms = SmsMessage.newFromCMT(lines); + header = sms.getUserDataHeader(); + assertNotNull(header); + assertNotNull(sms.getUserData()); + assertNotNull(header.concatRef); + assertEquals(header.concatRef.refNumber, 85); + assertEquals(header.concatRef.msgCount, 2); + assertEquals(header.concatRef.seqNumber, 2); + assertEquals(header.concatRef.isEightBits, true); + assertNotNull(header.portAddrs); + assertEquals(header.portAddrs.destPort, 2948); + assertEquals(header.portAddrs.origPort, 9200); + assertEquals(header.portAddrs.areEightBits, false); + } + + @MediumTest + public void testEfRecord() throws Exception { + SmsMessage sms; + + String s = "03029111000c9194981492631000f269206190022000a053e4534a05358bd3" + + "69f05804259da0219418a40641536a110a0aea408080604028180e888462c1" + + "50341c0f484432a1542c174c46b3e1743c9f9068442a994ea8946ac56ab95e" + + "b0986c46abd96eb89c6ec7ebf97ec0a070482c1a8fc8a472c96c3a9fd0a874" + + "4aad5aafd8ac76cbed7abfe0b0784c2e9bcfe8b47acd6ebbdff0b87c4eafdb" + + "eff8bc7ecfeffbffffffffffffffffffffffffffff"; + byte[] data = IccUtils.hexStringToBytes(s); + + sms = SmsMessage.createFromEfRecord(1, data); + assertNotNull(sms.getMessageBody()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/SimPhoneBookTest.java b/tests/telephonytests/src/com/android/internal/telephony/SimPhoneBookTest.java new file mode 100644 index 0000000..609e768 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/SimPhoneBookTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.ServiceManager; +import android.test.suitebuilder.annotation.Suppress; + +import java.util.List; + +import junit.framework.TestCase; + +@Suppress +public class SimPhoneBookTest extends TestCase { + + public void testBasic() throws Exception { + IIccPhoneBook simPhoneBook = + IIccPhoneBook.Stub.asInterface(ServiceManager.getService("simphonebook")); + assertNotNull(simPhoneBook); + + int size[] = simPhoneBook.getAdnRecordsSize(IccConstants.EF_ADN); + assertNotNull(size); + assertEquals(3, size.length); + assertEquals(size[0] * size[2], size[1]); + assertTrue(size[2] >= 100); + + List<AdnRecord> adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + // do it twice cause the second time shall read from cache only + adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + assertNotNull(adnRecordList); + + // Test for phone book update + int adnIndex, listIndex = 0; + AdnRecord originalAdn = null; + // We need to maintain the state of the SIM before and after the test. + // Since this test doesn't mock the SIM we try to get a valid ADN record, + // for 3 tries and if this fails, we bail out. + for (adnIndex = 3 ; adnIndex >= 1; adnIndex--) { + listIndex = adnIndex - 1; // listIndex is zero based. + originalAdn = adnRecordList.get(listIndex); + assertNotNull("Original Adn is Null.", originalAdn); + assertNotNull("Original Adn alpha tag is null.", originalAdn.getAlphaTag()); + assertNotNull("Original Adn number is null.", originalAdn.getNumber()); + + if (originalAdn.getNumber().length() > 0 && + originalAdn.getAlphaTag().length() > 0) { + break; + } + } + if (adnIndex == 0) return; + + AdnRecord emptyAdn = new AdnRecord("", ""); + AdnRecord firstAdn = new AdnRecord("John", "4085550101"); + AdnRecord secondAdn = new AdnRecord("Andy", "6505550102"); + String pin2 = null; + + // udpate by index + boolean success = simPhoneBook.updateAdnRecordsInEfByIndex(IccConstants.EF_ADN, + firstAdn.getAlphaTag(), firstAdn.getNumber(), adnIndex, pin2); + adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + AdnRecord tmpAdn = adnRecordList.get(listIndex); + assertTrue(success); + assertTrue(firstAdn.isEqual(tmpAdn)); + + // replace by search + success = simPhoneBook.updateAdnRecordsInEfBySearch(IccConstants.EF_ADN, + firstAdn.getAlphaTag(), firstAdn.getNumber(), + secondAdn.getAlphaTag(), secondAdn.getNumber(), pin2); + adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + tmpAdn = adnRecordList.get(listIndex); + assertTrue(success); + assertFalse(firstAdn.isEqual(tmpAdn)); + assertTrue(secondAdn.isEqual(tmpAdn)); + + // erase be search + success = simPhoneBook.updateAdnRecordsInEfBySearch(IccConstants.EF_ADN, + secondAdn.getAlphaTag(), secondAdn.getNumber(), + emptyAdn.getAlphaTag(), emptyAdn.getNumber(), pin2); + adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + tmpAdn = adnRecordList.get(listIndex); + assertTrue(success); + assertTrue(tmpAdn.isEmpty()); + + // restore the orginial adn + success = simPhoneBook.updateAdnRecordsInEfByIndex(IccConstants.EF_ADN, + originalAdn.getAlphaTag(), originalAdn.getNumber(), adnIndex, + pin2); + adnRecordList = simPhoneBook.getAdnRecordsInEf(IccConstants.EF_ADN); + tmpAdn = adnRecordList.get(listIndex); + assertTrue(success); + assertTrue(originalAdn.isEqual(tmpAdn)); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/SimSmsTest.java b/tests/telephonytests/src/com/android/internal/telephony/SimSmsTest.java new file mode 100644 index 0000000..1609680 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/SimSmsTest.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import android.os.ServiceManager; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.Suppress; + +import java.util.List; + +import junit.framework.TestCase; + +public class SimSmsTest extends TestCase { + + @MediumTest + @Suppress // TODO: suppress this test for now since it doesn't work on the emulator + public void testBasic() throws Exception { + + ISms sms = ISms.Stub.asInterface(ServiceManager.getService("isms")); + assertNotNull(sms); + + List<SmsRawData> records = sms.getAllMessagesFromIccEf(); + assertNotNull(records); + assertTrue(records.size() >= 0); + + int firstNullIndex = -1; + int firstValidIndex = -1; + byte[] pdu = null; + for (int i = 0; i < records.size(); i++) { + SmsRawData data = records.get(i); + if (data != null && firstValidIndex == -1) { + firstValidIndex = i; + pdu = data.getBytes(); + } + if (data == null && firstNullIndex == -1) { + firstNullIndex = i; + } + if (firstNullIndex != -1 && firstValidIndex != -1) { + break; + } + } + if (firstNullIndex == -1 || firstValidIndex == -1) + return; + assertNotNull(pdu); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/SimUtilsTest.java b/tests/telephonytests/src/com/android/internal/telephony/SimUtilsTest.java new file mode 100644 index 0000000..ef62d85 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/SimUtilsTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.gsm.SimTlv; +import com.android.internal.telephony.IccUtils; +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + + +public class SimUtilsTest extends TestCase { + + @SmallTest + public void testBasic() throws Exception { + byte[] data, data2; + + /* + * bcdToString() + */ + + // An EF[ICCID] record + data = IccUtils.hexStringToBytes("981062400510444868f2"); + assertEquals("8901260450014484862", IccUtils.bcdToString(data, 0, data.length)); + + // skip the first and last bytes + assertEquals("0126045001448486", IccUtils.bcdToString(data, 1, data.length - 2)); + + // Stops on invalid BCD value + data = IccUtils.hexStringToBytes("98E062400510444868f2"); + assertEquals("890", IccUtils.bcdToString(data, 0, data.length)); + + // skip the high nibble 'F' since some PLMNs have it + data = IccUtils.hexStringToBytes("98F062400510444868f2"); + assertEquals("890260450014484862", IccUtils.bcdToString(data, 0, data.length)); + + /* + * gsmBcdByteToInt() + */ + + assertEquals(98, IccUtils.gsmBcdByteToInt((byte) 0x89)); + + // Out of range is treated as 0 + assertEquals(8, IccUtils.gsmBcdByteToInt((byte) 0x8c)); + + /* + * cdmaBcdByteToInt() + */ + + assertEquals(89, IccUtils.cdmaBcdByteToInt((byte) 0x89)); + + // Out of range is treated as 0 + assertEquals(80, IccUtils.cdmaBcdByteToInt((byte) 0x8c)); + + /* + * adnStringFieldToString() + */ + + + data = IccUtils.hexStringToBytes("00566f696365204d61696c07918150367742f3ffffffffffff"); + // Again, skip prepended 0 + // (this is an EF[ADN] record) + assertEquals("Voice Mail", IccUtils.adnStringFieldToString(data, 1, data.length - 15)); + + data = IccUtils.hexStringToBytes("809673539A5764002F004DFFFFFFFFFF"); + // (this is from an EF[ADN] record) + assertEquals("\u9673\u539A\u5764/M", IccUtils.adnStringFieldToString(data, 0, data.length)); + + data = IccUtils.hexStringToBytes("810A01566fec6365204de0696cFFFFFF"); + // (this is made up to test since I don't have a real one) + assertEquals("Vo\u00ECce M\u00E0il", IccUtils.adnStringFieldToString(data, 0, data.length)); + + data = IccUtils.hexStringToBytes("820505302D82d32d31"); + // Example from 3GPP TS 11.11 V18.1.3.0 annex B + assertEquals("-\u0532\u0583-1", IccUtils.adnStringFieldToString(data, 0, data.length)); + } + +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java b/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java new file mode 100644 index 0000000..b848657 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/SmsMessageBodyTest.java @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +import android.telephony.SmsMessage; +import android.telephony.TelephonyManager; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; +import android.test.suitebuilder.annotation.MediumTest; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import com.android.internal.telephony.SmsConstants; + +import java.util.Random; + +/** + * Test cases to verify selection of the optimal 7 bit encoding tables + * (for all combinations of enabled national language tables) for messages + * containing Turkish, Spanish, Portuguese, Greek, and other symbols + * present in the GSM default and national language tables defined in + * 3GPP TS 23.038. Also verifies correct SMS encoding for CDMA, which only + * supports the GSM 7 bit default alphabet, ASCII 8 bit, and UCS-2. + * Tests both encoding variations: unsupported characters mapped to space, + * and unsupported characters force entire message to UCS-2. + */ +public class SmsMessageBodyTest extends AndroidTestCase { + private static final String TAG = "SmsMessageBodyTest"; + + // ASCII chars in the GSM 7 bit default alphabet + private static final String sAsciiChars = "@$_ !\"#%&'()*+,-./0123456789" + + ":;<=>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\n\r"; + + // Unicode chars in the GSM 7 bit default alphabet and both locking shift tables + private static final String sGsmDefaultChars = "\u00a3\u00a5\u00e9\u00c7\u0394\u00c9" + + "\u00dc\u00a7\u00fc\u00e0"; + + // Unicode chars in the GSM 7 bit default table and Turkish locking shift tables + private static final String sGsmDefaultAndTurkishTables = "\u00f9\u00f2\u00c5\u00e5\u00df" + + "\u00a4\u00c4\u00d6\u00d1\u00e4\u00f6\u00f1"; + + // Unicode chars in the GSM 7 bit default table but not the locking shift tables + private static final String sGsmDefaultTableOnly = "\u00e8\u00ec\u00d8\u00f8\u00c6\u00e6" + + "\u00a1\u00bf"; + + // ASCII chars in the GSM default extension table + private static final String sGsmExtendedAsciiChars = "{}[]\f"; + + // chars in GSM default extension table and Portuguese locking shift table + private static final String sGsmExtendedPortugueseLocking = "^\\|~"; + + // Euro currency symbol + private static final String sGsmExtendedEuroSymbol = "\u20ac"; + + // CJK ideographs, Hiragana, Katakana, full width letters, Cyrillic, etc. + private static final String sUnicodeChars = "\u4e00\u4e01\u4e02\u4e03" + + "\u4e04\u4e05\u4e06\u4e07\u4e08\u4e09\u4e0a\u4e0b\u4e0c\u4e0d" + + "\u4e0e\u4e0f\u3041\u3042\u3043\u3044\u3045\u3046\u3047\u3048" + + "\u30a1\u30a2\u30a3\u30a4\u30a5\u30a6\u30a7\u30a8" + + "\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18" + + "\uff70\uff71\uff72\uff73\uff74\uff75\uff76\uff77\uff78" + + "\u0400\u0401\u0402\u0403\u0404\u0405\u0406\u0407\u0408" + + "\u00a2\u00a9\u00ae\u2122"; + + // chars in Turkish single shift and locking shift tables + private static final String sTurkishChars = "\u0131\u011e\u011f\u015e\u015f\u0130"; + + // chars in Spanish single shift table and Portuguese single and locking shift tables + private static final String sPortugueseAndSpanishChars = "\u00c1\u00e1\u00cd\u00ed" + + "\u00d3\u00f3\u00da\u00fa"; + + // chars in all national language tables but not in the standard GSM alphabets + private static final String sNationalLanguageTablesOnly = "\u00e7"; + + // chars in Portuguese single shift and locking shift tables + private static final String sPortugueseChars = "\u00ea\u00d4\u00f4\u00c0\u00c2\u00e2" + + "\u00ca\u00c3\u00d5\u00e3\u00f5"; + + // chars in Portuguese locking shift table only + private static final String sPortugueseLockingShiftChars = "\u00aa\u221e\u00ba`"; + + // Greek letters in GSM alphabet missing from Portuguese locking and single shift tables + private static final String sGreekLettersNotInPortugueseTables = "\u039b\u039e"; + + // Greek letters in GSM alphabet and Portuguese single shift (but not locking shift) table + private static final String sGreekLettersInPortugueseShiftTable = + "\u03a6\u0393\u03a9\u03a0\u03a8\u03a3\u0398"; + + // List of classes of characters in SMS tables + private static final String[] sCharacterClasses = { + sGsmExtendedAsciiChars, + sGsmExtendedPortugueseLocking, + sGsmDefaultChars, + sGsmDefaultAndTurkishTables, + sGsmDefaultTableOnly, + sGsmExtendedEuroSymbol, + sUnicodeChars, + sTurkishChars, + sPortugueseChars, + sPortugueseLockingShiftChars, + sPortugueseAndSpanishChars, + sGreekLettersNotInPortugueseTables, + sGreekLettersInPortugueseShiftTable, + sNationalLanguageTablesOnly, + sAsciiChars + }; + + private static final int sNumCharacterClasses = sCharacterClasses.length; + + // For each character class, whether it is present in a particular char table. + // First three entries are locking shift tables, followed by four single shift tables + private static final boolean[][] sCharClassPresenceInTables = { + // ASCII chars in all GSM extension tables + {false, false, false, true, true, true, true}, + // ASCII chars in all GSM extension tables and Portuguese locking shift table + {false, false, true, true, true, true, true}, + // non-ASCII chars in GSM default alphabet and all locking tables + {true, true, true, false, false, false, false}, + // non-ASCII chars in GSM default alphabet and Turkish locking shift table + {true, true, false, false, false, false, false}, + // non-ASCII chars in GSM default alphabet table only + {true, false, false, false, false, false, false}, + // Euro symbol is present in several tables + {false, true, true, true, true, true, true}, + // Unicode characters not present in any 7 bit tables + {false, false, false, false, false, false, false}, + // Characters specific to Turkish language + {false, true, false, false, true, false, false}, + // Characters in Portuguese single shift and locking shift tables + {false, false, true, false, false, false, true}, + // Characters in Portuguese locking shift table only + {false, false, true, false, false, false, false}, + // Chars in Spanish single shift and Portuguese single and locking shift tables + {false, false, true, false, false, true, true}, + // Greek letters in GSM default alphabet missing from Portuguese tables + {true, true, false, false, false, false, false}, + // Greek letters in GSM alphabet and Portuguese single shift table + {true, true, false, false, false, false, true}, + // Chars in all national language tables but not the standard GSM tables + {false, true, true, false, true, true, true}, + // ASCII chars in GSM default alphabet + {true, true, true, false, false, false, false} + }; + + private static final int sTestLengthCount = 12; + + private static final int[] sSeptetTestLengths = + { 0, 1, 2, 80, 159, 160, 161, 240, 305, 306, 307, 320}; + + private static final int[] sUnicodeTestLengths = + { 0, 1, 2, 35, 69, 70, 71, 100, 133, 134, 135, 160}; + + private static final int[] sTestMsgCounts = + { 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3}; + + private static final int[] sSeptetUnitsRemaining = + {160, 159, 158, 80, 1, 0, 145, 66, 1, 0, 152, 139}; + + private static final int[] sUnicodeUnitsRemaining = + { 70, 69, 68, 35, 1, 0, 63, 34, 1, 0, 66, 41}; + + // Combinations of enabled GSM national language single shift tables + private static final int[][] sEnabledSingleShiftTables = { + {}, // GSM default alphabet only + {1}, // Turkish (single shift only) + {1}, // Turkish (single and locking shift) + {2}, // Spanish + {3}, // Portuguese (single shift only) + {3}, // Portuguese (single and locking shift) + {1, 2}, // Turkish + Spanish (single shift only) + {1, 2}, // Turkish + Spanish (single and locking shift) + {1, 3}, // Turkish + Portuguese (single shift only) + {1, 3}, // Turkish + Portuguese (single and locking shift) + {2, 3}, // Spanish + Portuguese (single shift only) + {2, 3}, // Spanish + Portuguese (single and locking shift) + {1, 2, 3}, // Turkish, Spanish, Portuguese (single shift only) + {1, 2, 3}, // Turkish, Spanish, Portuguese (single and locking shift) + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} // all language tables + }; + + // Combinations of enabled GSM national language locking shift tables + private static final int[][] sEnabledLockingShiftTables = { + {}, // GSM default alphabet only + {}, // Turkish (single shift only) + {1}, // Turkish (single and locking shift) + {}, // Spanish (no locking shift table) + {}, // Portuguese (single shift only) + {3}, // Portuguese (single and locking shift) + {}, // Turkish + Spanish (single shift only) + {1}, // Turkish + Spanish (single and locking shift) + {}, // Turkish + Portuguese (single shift only) + {1, 3}, // Turkish + Portuguese (single and locking shift) + {}, // Spanish + Portuguese (single shift only) + {3}, // Spanish + Portuguese (single and locking shift) + {}, // Turkish, Spanish, Portuguese (single shift only) + {1, 3}, // Turkish, Spanish, Portuguese (single and locking shift) + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13} // all language tables + }; + + // LanguagePair counter indexes to check for each entry above + private static final int[][] sLanguagePairIndexesByEnabledIndex = { + {0}, // default tables only + {0, 1}, // Turkish (single shift only) + {0, 1, 4, 5}, // Turkish (single and locking shift) + {0, 2}, // Spanish + {0, 3}, // Portuguese (single shift only) + {0, 3, 8, 11}, // Portuguese (single and locking shift) + {0, 1, 2}, // Turkish + Spanish (single shift only) + {0, 1, 2, 4, 5, 6}, // Turkish + Spanish (single and locking shift) + {0, 1, 3}, // Turkish + Portuguese (single shift only) + {0, 1, 3, 4, 5, 7, 8, 9, 11}, // Turkish + Portuguese (single and locking shift) + {0, 2, 3}, // Spanish + Portuguese (single shift only) + {0, 2, 3, 8, 10, 11}, // Spanish + Portuguese (single and locking shift) + {0, 1, 2, 3}, // all languages (single shift only) + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, // all languages (single and locking shift) + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} // all languages (no Indic chars in test) + }; + + /** + * User data header requires one octet for length. Count as one septet, because + * all combinations of header elements below will have at least one free bit + * when padding to the nearest septet boundary. + */ + private static final int UDH_SEPTET_COST_LENGTH = 1; + + /** + * Using a non-default language locking shift table OR single shift table + * requires a user data header of 3 octets, or 4 septets, plus UDH length. + */ + private static final int UDH_SEPTET_COST_ONE_SHIFT_TABLE = 4; + + /** + * Using a non-default language locking shift table AND single shift table + * requires a user data header of 6 octets, or 7 septets, plus UDH length. + */ + private static final int UDH_SEPTET_COST_TWO_SHIFT_TABLES = 7; + + /** + * Multi-part messages require a user data header of 5 octets, or 6 septets, + * plus UDH length. + */ + private static final int UDH_SEPTET_COST_CONCATENATED_MESSAGE = 6; + + @SmallTest + public void testCalcLengthAscii() throws Exception { + StringBuilder sb = new StringBuilder(320); + int[] values = {0, 0, 0, SmsConstants.ENCODING_7BIT, 0, 0}; + int startPos = 0; + int asciiCharsLen = sAsciiChars.length(); + + for (int i = 0; i < sTestLengthCount; i++) { + int len = sSeptetTestLengths[i]; + assertTrue(sb.length() <= len); + + while (sb.length() < len) { + int addCount = len - sb.length(); + int endPos = (asciiCharsLen - startPos > addCount) ? + (startPos + addCount) : asciiCharsLen; + sb.append(sAsciiChars, startPos, endPos); + startPos = (endPos == asciiCharsLen) ? 0 : endPos; + } + assertEquals(len, sb.length()); + + String testStr = sb.toString(); + values[0] = sTestMsgCounts[i]; + values[1] = len; + values[2] = sSeptetUnitsRemaining[i]; + + callGsmLengthMethods(testStr, false, values); + callGsmLengthMethods(testStr, true, values); + callCdmaLengthMethods(testStr, false, values); + callCdmaLengthMethods(testStr, true, values); + } + } + + @SmallTest + public void testCalcLengthUnicode() throws Exception { + StringBuilder sb = new StringBuilder(160); + int[] values = {0, 0, 0, SmsConstants.ENCODING_16BIT, 0, 0}; + int[] values7bit = {1, 0, 0, SmsConstants.ENCODING_7BIT, 0, 0}; + int startPos = 0; + int unicodeCharsLen = sUnicodeChars.length(); + + // start with length 1: empty string uses ENCODING_7BIT + for (int i = 1; i < sTestLengthCount; i++) { + int len = sUnicodeTestLengths[i]; + assertTrue(sb.length() <= len); + + while (sb.length() < len) { + int addCount = len - sb.length(); + int endPos = (unicodeCharsLen - startPos > addCount) ? + (startPos + addCount) : unicodeCharsLen; + sb.append(sUnicodeChars, startPos, endPos); + startPos = (endPos == unicodeCharsLen) ? 0 : endPos; + } + assertEquals(len, sb.length()); + + String testStr = sb.toString(); + values[0] = sTestMsgCounts[i]; + values[1] = len; + values[2] = sUnicodeUnitsRemaining[i]; + values7bit[1] = len; + values7bit[2] = SmsConstants.MAX_USER_DATA_SEPTETS - len; + + callGsmLengthMethods(testStr, false, values); + callCdmaLengthMethods(testStr, false, values); + callGsmLengthMethods(testStr, true, values7bit); + callCdmaLengthMethods(testStr, true, values7bit); + } + } + + private static class LanguagePair { + // index is 2 for Portuguese locking shift because there is no Spanish locking shift table + private final int langTableIndex; + private final int langShiftTableIndex; + int length; + int missingChars7bit; + + LanguagePair(int langTable, int langShiftTable) { + langTableIndex = langTable; + langShiftTableIndex = langShiftTable; + } + + void clear() { + length = 0; + missingChars7bit = 0; + } + + void addChar(boolean[] charClassTableRow) { + if (charClassTableRow[langTableIndex]) { + length++; + } else if (charClassTableRow[3 + langShiftTableIndex]) { + length += 2; + } else { + length++; // use ' ' for unmapped char in 7 bit only mode + missingChars7bit++; + } + } + } + + private static class CounterHelper { + LanguagePair[] mCounters; + int[] mStatsCounters; + int mUnicodeCounter; + + CounterHelper() { + mCounters = new LanguagePair[12]; + mStatsCounters = new int[12]; + for (int i = 0; i < 12; i++) { + mCounters[i] = new LanguagePair(i/4, i%4); + } + } + + void clear() { + // Note: don't clear stats counters + for (int i = 0; i < 12; i++) { + mCounters[i].clear(); + } + } + + void addChar(int charClass) { + boolean[] charClassTableRow = sCharClassPresenceInTables[charClass]; + for (int i = 0; i < 12; i++) { + mCounters[i].addChar(charClassTableRow); + } + } + + void fillData(int enabledLangsIndex, boolean use7bitOnly, int[] values, int length) { + int[] languagePairs = sLanguagePairIndexesByEnabledIndex[enabledLangsIndex]; + int minNumSeptets = Integer.MAX_VALUE; + int minNumSeptetsWithHeader = Integer.MAX_VALUE; + int minNumMissingChars = Integer.MAX_VALUE; + int langIndex = -1; + int langShiftIndex = -1; + for (int i : languagePairs) { + LanguagePair pair = mCounters[i]; + int udhLength = 0; + if (i != 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + if (i < 4 || i % 4 == 0) { + udhLength += UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength += UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } + } + int numSeptetsWithHeader; + if (pair.length > (SmsConstants.MAX_USER_DATA_SEPTETS - udhLength)) { + if (udhLength == 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + } + udhLength += UDH_SEPTET_COST_CONCATENATED_MESSAGE; + int septetsPerPart = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength; + int msgCount = (pair.length + septetsPerPart - 1) / septetsPerPart; + numSeptetsWithHeader = udhLength * msgCount + pair.length; + } else { + numSeptetsWithHeader = udhLength + pair.length; + } + + if (use7bitOnly) { + if (pair.missingChars7bit < minNumMissingChars || (pair.missingChars7bit == + minNumMissingChars && numSeptetsWithHeader < minNumSeptetsWithHeader)) { + minNumSeptets = pair.length; + minNumSeptetsWithHeader = numSeptetsWithHeader; + minNumMissingChars = pair.missingChars7bit; + langIndex = pair.langTableIndex; + langShiftIndex = pair.langShiftTableIndex; + } + } else { + if (pair.missingChars7bit == 0 && numSeptetsWithHeader < minNumSeptetsWithHeader) { + minNumSeptets = pair.length; + minNumSeptetsWithHeader = numSeptetsWithHeader; + langIndex = pair.langTableIndex; + langShiftIndex = pair.langShiftTableIndex; + } + } + } + if (langIndex == -1) { + // nothing matches, use values for Unicode + int byteCount = length * 2; + if (byteCount > SmsConstants.MAX_USER_DATA_BYTES) { + values[0] = (byteCount + SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER - 1) / + SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; + values[2] = ((values[0] * SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER) - + byteCount) / 2; + } else { + values[0] = 1; + values[2] = (SmsConstants.MAX_USER_DATA_BYTES - byteCount) / 2; + } + values[1] = length; + values[3] = SmsConstants.ENCODING_16BIT; + values[4] = 0; + values[5] = 0; + mUnicodeCounter++; + } else { + int udhLength = 0; + if (langIndex != 0 || langShiftIndex != 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + if (langIndex == 0 || langShiftIndex == 0) { + udhLength += UDH_SEPTET_COST_ONE_SHIFT_TABLE; + } else { + udhLength += UDH_SEPTET_COST_TWO_SHIFT_TABLES; + } + } + int msgCount; + if (minNumSeptets > (SmsConstants.MAX_USER_DATA_SEPTETS - udhLength)) { + if (udhLength == 0) { + udhLength = UDH_SEPTET_COST_LENGTH; + } + udhLength += UDH_SEPTET_COST_CONCATENATED_MESSAGE; + int septetsPerPart = SmsConstants.MAX_USER_DATA_SEPTETS - udhLength; + msgCount = (minNumSeptets + septetsPerPart - 1) / septetsPerPart; + } else { + msgCount = 1; + } + values[0] = msgCount; + values[1] = minNumSeptets; + values[2] = (values[0] * (SmsConstants.MAX_USER_DATA_SEPTETS - udhLength)) - + minNumSeptets; + values[3] = SmsConstants.ENCODING_7BIT; + values[4] = (langIndex == 2 ? 3 : langIndex); // Portuguese is code 3, index 2 + values[5] = langShiftIndex; + assertEquals("minNumSeptetsWithHeader", minNumSeptetsWithHeader, + udhLength * msgCount + minNumSeptets); + mStatsCounters[langIndex * 4 + langShiftIndex]++; + } + } + + void printStats() { + Log.d(TAG, "Unicode selection count: " + mUnicodeCounter); + for (int i = 0; i < 12; i++) { + Log.d(TAG, "Language pair index " + i + " count: " + mStatsCounters[i]); + } + } + } + + @LargeTest + public void testCalcLengthMixed7bit() throws Exception { + StringBuilder sb = new StringBuilder(320); + CounterHelper ch = new CounterHelper(); + Random r = new Random(0x4321); // use the same seed for reproducibility + int[] expectedValues = new int[6]; + int[] origLockingShiftTables = GsmAlphabet.getEnabledLockingShiftTables(); + int[] origSingleShiftTables = GsmAlphabet.getEnabledSingleShiftTables(); + int enabledLanguagesTestCases = sEnabledSingleShiftTables.length; + long startTime = System.currentTimeMillis(); + + // Repeat for 10 test runs + for (int run = 0; run < 10; run++) { + sb.setLength(0); + ch.clear(); + int unicodeOnlyCount = 0; + + // Test incrementally from 1 to 320 character random messages + for (int i = 1; i < 320; i++) { + // 1% chance to add from each special character class, else add an ASCII char + int charClass = r.nextInt(100); + if (charClass >= sNumCharacterClasses) { + charClass = sNumCharacterClasses - 1; // last class is ASCII + } + int classLength = sCharacterClasses[charClass].length(); + char nextChar = sCharacterClasses[charClass].charAt(r.nextInt(classLength)); + sb.append(nextChar); + ch.addChar(charClass); + +// if (i % 20 == 0) { +// Log.d(TAG, "test string: " + sb); +// } + + // Test string against all combinations of enabled languages + boolean unicodeOnly = true; + for (int j = 0; j < enabledLanguagesTestCases; j++) { + GsmAlphabet.setEnabledSingleShiftTables(sEnabledSingleShiftTables[j]); + GsmAlphabet.setEnabledLockingShiftTables(sEnabledLockingShiftTables[j]); + ch.fillData(j, false, expectedValues, i); + if (expectedValues[3] == SmsConstants.ENCODING_7BIT) { + unicodeOnly = false; + } + callGsmLengthMethods(sb, false, expectedValues); + // test 7 bit only mode + ch.fillData(j, true, expectedValues, i); + callGsmLengthMethods(sb, true, expectedValues); + } + // after 10 iterations with a Unicode-only string, skip to next test string + // so we can spend more time testing strings that do encode into 7 bits. + if (unicodeOnly && ++unicodeOnlyCount == 10) { +// Log.d(TAG, "Unicode only: skipping to next test string"); + break; + } + } + } + ch.printStats(); + Log.d(TAG, "Completed in " + (System.currentTimeMillis() - startTime) + " ms"); + GsmAlphabet.setEnabledLockingShiftTables(origLockingShiftTables); + GsmAlphabet.setEnabledSingleShiftTables(origSingleShiftTables); + } + + private void callGsmLengthMethods(CharSequence msgBody, boolean use7bitOnly, + int[] expectedValues) + { + // deprecated GSM-specific method + int[] values = android.telephony.gsm.SmsMessage.calculateLength(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], values[0]); + assertEquals("codeUnitCount", expectedValues[1], values[1]); + assertEquals("codeUnitsRemaining", expectedValues[2], values[2]); + assertEquals("codeUnitSize", expectedValues[3], values[3]); + + int activePhone = TelephonyManager.getDefault().getPhoneType(); + if (TelephonyManager.PHONE_TYPE_GSM == activePhone) { + values = android.telephony.SmsMessage.calculateLength(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], values[0]); + assertEquals("codeUnitCount", expectedValues[1], values[1]); + assertEquals("codeUnitsRemaining", expectedValues[2], values[2]); + assertEquals("codeUnitSize", expectedValues[3], values[3]); + } + + GsmAlphabet.TextEncodingDetails ted = + com.android.internal.telephony.gsm.SmsMessage.calculateLength(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], ted.msgCount); + assertEquals("codeUnitCount", expectedValues[1], ted.codeUnitCount); + assertEquals("codeUnitsRemaining", expectedValues[2], ted.codeUnitsRemaining); + assertEquals("codeUnitSize", expectedValues[3], ted.codeUnitSize); + assertEquals("languageTable", expectedValues[4], ted.languageTable); + assertEquals("languageShiftTable", expectedValues[5], ted.languageShiftTable); + } + + private void callCdmaLengthMethods(CharSequence msgBody, boolean use7bitOnly, + int[] expectedValues) + { + int activePhone = TelephonyManager.getDefault().getPhoneType(); + if (TelephonyManager.PHONE_TYPE_CDMA == activePhone) { + int[] values = android.telephony.SmsMessage.calculateLength(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], values[0]); + assertEquals("codeUnitCount", expectedValues[1], values[1]); + assertEquals("codeUnitsRemaining", expectedValues[2], values[2]); + assertEquals("codeUnitSize", expectedValues[3], values[3]); + } + + GsmAlphabet.TextEncodingDetails ted = + com.android.internal.telephony.cdma.SmsMessage.calculateLength(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], ted.msgCount); + assertEquals("codeUnitCount", expectedValues[1], ted.codeUnitCount); + assertEquals("codeUnitsRemaining", expectedValues[2], ted.codeUnitsRemaining); + assertEquals("codeUnitSize", expectedValues[3], ted.codeUnitSize); + + ted = com.android.internal.telephony.cdma.sms.BearerData.calcTextEncodingDetails(msgBody, use7bitOnly); + assertEquals("msgCount", expectedValues[0], ted.msgCount); + assertEquals("codeUnitCount", expectedValues[1], ted.codeUnitCount); + assertEquals("codeUnitsRemaining", expectedValues[2], ted.codeUnitsRemaining); + assertEquals("codeUnitSize", expectedValues[3], ted.codeUnitSize); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/TelephonyUtilsTest.java b/tests/telephonytests/src/com/android/internal/telephony/TelephonyUtilsTest.java new file mode 100644 index 0000000..3757017 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/TelephonyUtilsTest.java @@ -0,0 +1,219 @@ +/** + * Copyright (C) 2009 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.internal.telephony; + +import com.android.internal.telephony.RetryManager; +import junit.framework.TestCase; +import android.test.suitebuilder.annotation.SmallTest; + +public class TelephonyUtilsTest extends TestCase { + + /** + * After first creating the RetryManager + * isRetryNeeded should be false and the time 0 + */ + @SmallTest + public void testRetryManagerEmpty() throws Exception { + RetryManager rm = new RetryManager(); + + assertEquals(0, rm.getRetryCount()); + assertFalse(rm.isRetryForever()); + assertFalse(rm.isRetryNeeded()); + assertEquals(0, rm.getRetryCount()); + assertEquals(0, rm.getRetryTimer()); + + rm.increaseRetryCount(); + assertFalse(rm.isRetryForever()); + assertFalse(rm.isRetryNeeded()); + assertEquals(0, rm.getRetryCount()); + assertEquals(0, rm.getRetryTimer()); + + rm.setRetryCount(123); + assertFalse(rm.isRetryForever()); + assertFalse(rm.isRetryNeeded()); + assertEquals(0, rm.getRetryCount()); + assertEquals(0, rm.getRetryTimer()); + + rm.retryForeverUsingLastTimeout(); + assertTrue(rm.isRetryForever()); + assertTrue(rm.isRetryNeeded()); + assertEquals(0, rm.getRetryCount()); + assertEquals(0, rm.getRetryTimer()); + + rm.setRetryCount(2); + assertFalse(rm.isRetryForever()); + assertFalse(rm.isRetryNeeded()); + assertEquals(0, rm.getRetryCount()); + assertEquals(0, rm.getRetryTimer()); + } + + /** + * A simple test and that randomization is doing something. + */ + @SmallTest + public void testRetryManagerSimplest() throws Exception { + RetryManager rm = new RetryManager(); + + assertTrue(rm.configure(1, 500, 10)); + int loops = 10; + int count = 0; + for (int i = 0; i < loops; i++) { + assertTrue(rm.isRetryNeeded()); + int time = rm.getRetryTimer(); + assertTrue((time >= 500) && (time < 600)); + if (time == 500) { + count++; + } + } + assertFalse(count == loops); + rm.increaseRetryCount(); + assertFalse(rm.isRetryNeeded()); + rm.setRetryCount(0); + assertTrue(rm.isRetryNeeded()); + } + + /** + * Test multiple values using simple configuration. + */ + @SmallTest + public void testRetryManagerSimple() throws Exception { + RetryManager rm = new RetryManager(); + + assertTrue(rm.configure(3, 1000, 0)); + assertTrue(rm.isRetryNeeded()); + assertEquals(1000, rm.getRetryTimer()); + assertEquals(rm.getRetryTimer(), 1000); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + assertEquals(1000, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + assertEquals(1000, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertFalse(rm.isRetryNeeded()); + assertEquals(1000, rm.getRetryTimer()); + } + + /** + * Test string configuration, simplest + */ + @SmallTest + public void testRetryManageSimpleString() throws Exception { + RetryManager rm = new RetryManager(); + + assertTrue(rm.configure("101")); + assertTrue(rm.isRetryNeeded()); + assertEquals(101, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertFalse(rm.isRetryNeeded()); + } + + /** + * Test infinite retires + */ + @SmallTest + public void testRetryManageInfinite() throws Exception { + RetryManager rm = new RetryManager(); + + assertTrue(rm.configure("1000,2000,3000,max_retries=infinite")); + assertTrue(rm.isRetryNeeded()); + assertEquals(1000, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + assertEquals(2000, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + // All others are 3000 and isRetryNeeded is always true + for (int i=0; i < 100; i++) { + assertEquals(3000, rm.getRetryTimer()); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + } + } + + /** + * Test string configuration using all options and with quotes. + */ + @SmallTest + public void testRetryManageString() throws Exception { + RetryManager rm = new RetryManager(); + int time; + + assertTrue(rm.configure( + "\"max_retries=4, default_randomization=100,1000, 2000 :200 , 3000\"")); + assertTrue(rm.isRetryNeeded()); + time = rm.getRetryTimer(); + assertTrue((time >= 1000) && (time < 1100)); + + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + time = rm.getRetryTimer(); + assertTrue((time >= 2000) && (time < 2200)); + + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + time = rm.getRetryTimer(); + assertTrue((time >= 3000) && (time < 3100)); + + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + time = rm.getRetryTimer(); + assertTrue((time >= 3000) && (time < 3100)); + + rm.increaseRetryCount(); + assertFalse(rm.isRetryNeeded()); + } + + /** + * Test string configuration using all options. + */ + @SmallTest + public void testRetryManageForever() throws Exception { + RetryManager rm = new RetryManager(); + int time; + + assertTrue(rm.configure("1000, 2000, 3000")); + assertTrue(rm.isRetryNeeded()); + assertFalse(rm.isRetryForever()); + assertEquals(0, rm.getRetryCount()); + assertEquals(1000, rm.getRetryTimer()); + + rm.retryForeverUsingLastTimeout(); + rm.increaseRetryCount(); + rm.increaseRetryCount(); + rm.increaseRetryCount(); + assertTrue(rm.isRetryNeeded()); + assertTrue(rm.isRetryForever()); + assertEquals(3, rm.getRetryCount()); + assertEquals(3000, rm.getRetryTimer()); + + rm.setRetryCount(1); + assertTrue(rm.isRetryNeeded()); + assertFalse(rm.isRetryForever()); + assertEquals(1, rm.getRetryCount()); + assertEquals(2000, rm.getRetryTimer()); + + rm.retryForeverUsingLastTimeout(); + assertTrue(rm.isRetryNeeded()); + assertTrue(rm.isRetryForever()); + rm.resetRetryCount(); + assertTrue(rm.isRetryNeeded()); + assertFalse(rm.isRetryForever()); + assertEquals(0, rm.getRetryCount()); + assertEquals(1000, rm.getRetryTimer()); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/TestPhoneNotifier.java b/tests/telephonytests/src/com/android/internal/telephony/TestPhoneNotifier.java new file mode 100644 index 0000000..b8f0568 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/TestPhoneNotifier.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2006 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.internal.telephony; + +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import android.telephony.CellInfo; + +/** + * Stub class used for unit tests + */ + +public class TestPhoneNotifier implements PhoneNotifier { + public TestPhoneNotifier() { + } + + public void notifyPhoneState(Phone sender) { + } + + public void notifyServiceState(Phone sender) { + } + + public void notifyCellLocation(Phone sender) { + } + + public void notifySignalStrength(Phone sender) { + } + + public void notifyMessageWaitingChanged(Phone sender) { + } + + public void notifyCallForwardingChanged(Phone sender) { + } + + public void notifyDataConnection(Phone sender, String reason, String apnType) { + } + + public void notifyDataConnection(Phone sender, String reason, String apnType, + PhoneConstants.DataState state) { + } + + public void notifyDataConnectionFailed(Phone sender, String reason, String apnType) { + } + + public void notifyDataActivity(Phone sender) { + } + + public void notifyOtaspChanged(Phone sender, int otaspMode) { + } + + public void notifyCellInfo(Phone sender, CellInfo cellInfo) { + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/Wap230WspContentTypeTest.java b/tests/telephonytests/src/com/android/internal/telephony/Wap230WspContentTypeTest.java new file mode 100644 index 0000000..d31b294 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/Wap230WspContentTypeTest.java @@ -0,0 +1,853 @@ +/* + * Copyright (C) 2010 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.internal.telephony; + +import com.android.internal.telephony.WspTypeDecoder; +import com.android.internal.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class Wap230WspContentTypeTest extends TestCase { + + public static final Map<Integer, String> WELL_KNOWN_SHORT_MIME_TYPES + = new HashMap<Integer, String>(); + public static final Map<Integer, String> WELL_KNOWN_LONG_MIME_TYPES + = new HashMap<Integer, String>(); + public static final Map<Integer, String> WELL_KNOWN_PARAMETERS + = new HashMap<Integer, String>(); + + static { + WELL_KNOWN_SHORT_MIME_TYPES.put(0x00, "*/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x01, "text/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x02, "text/html"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x03, "text/plain"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x04, "text/x-hdml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x05, "text/x-ttml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x06, "text/x-vCalendar"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x07, "text/x-vCard"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x08, "text/vnd.wap.wml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x09, "text/vnd.wap.wmlscript"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0A, "text/vnd.wap.wta-event"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0B, "multipart/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0C, "multipart/mixed"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0D, "multipart/form-data"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0E, "multipart/byterantes"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x0F, "multipart/alternative"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x10, "application/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x11, "application/java-vm"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x12, "application/x-www-form-urlencoded"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x13, "application/x-hdmlc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x14, "application/vnd.wap.wmlc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x15, "application/vnd.wap.wmlscriptc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x16, "application/vnd.wap.wta-eventc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x17, "application/vnd.wap.uaprof"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x18, "application/vnd.wap.wtls-ca-certificate"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x19, "application/vnd.wap.wtls-user-certificate"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1A, "application/x-x509-ca-cert"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1B, "application/x-x509-user-cert"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1C, "image/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1D, "image/gif"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1E, "image/jpeg"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x1F, "image/tiff"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x20, "image/png"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x21, "image/vnd.wap.wbmp"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x22, "application/vnd.wap.multipart.*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x23, "application/vnd.wap.multipart.mixed"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x24, "application/vnd.wap.multipart.form-data"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x25, "application/vnd.wap.multipart.byteranges"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x26, "application/vnd.wap.multipart.alternative"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x27, "application/xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x28, "text/xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x29, "application/vnd.wap.wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2A, "application/x-x968-cross-cert"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2B, "application/x-x968-ca-cert"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2C, "application/x-x968-user-cert"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2D, "text/vnd.wap.si"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2E, "application/vnd.wap.sic"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x2F, "text/vnd.wap.sl"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x30, "application/vnd.wap.slc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x31, "text/vnd.wap.co"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x32, "application/vnd.wap.coc"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x33, "application/vnd.wap.multipart.related"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x34, "application/vnd.wap.sia"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x35, "text/vnd.wap.connectivity-xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x36, "application/vnd.wap.connectivity-wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x37, "application/pkcs7-mime"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x38, "application/vnd.wap.hashed-certificate"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x39, "application/vnd.wap.signed-certificate"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3A, "application/vnd.wap.cert-response"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3B, "application/xhtml+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3C, "application/wml+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3D, "text/css"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3E, "application/vnd.wap.mms-message"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x3F, "application/vnd.wap.rollover-certificate"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x40, "application/vnd.wap.locc+wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x41, "application/vnd.wap.loc+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x42, "application/vnd.syncml.dm+wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x43, "application/vnd.syncml.dm+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x44, "application/vnd.syncml.notification"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x45, "application/vnd.wap.xhtml+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x46, "application/vnd.wv.csp.cir"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x47, "application/vnd.oma.dd+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x48, "application/vnd.oma.drm.message"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x49, "application/vnd.oma.drm.content"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4A, "application/vnd.oma.drm.rights+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4B, "application/vnd.oma.drm.rights+wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4C, "application/vnd.wv.csp+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4D, "application/vnd.wv.csp+wbxml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4E, "application/vnd.syncml.ds.notification"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x4F, "audio/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x50, "video/*"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x51, "application/vnd.oma.dd2+xml"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x52, "application/mikey"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x53, "application/vnd.oma.dcd"); + WELL_KNOWN_SHORT_MIME_TYPES.put(0x54, "application/vnd.oma.dcdc"); + + WELL_KNOWN_LONG_MIME_TYPES.put(0x0201, "application/vnd.uplanet.cacheop-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0202, "application/vnd.uplanet.signal"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0203, "application/vnd.uplanet.alert-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0204, "application/vnd.uplanet.list-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0205, "application/vnd.uplanet.listcmd-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0206, "application/vnd.uplanet.channel-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0207, "application/vnd.uplanet.provisioning-status-uri"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0208, "x-wap.multipart/vnd.uplanet.header-set"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0209, "application/vnd.uplanet.bearer-choice-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x020A, "application/vnd.phonecom.mmc-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x020B, "application/vnd.nokia.syncset+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x020C, "image/x-up-wpng"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0300, "application/iota.mmc-wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0301, "application/iota.mmc-xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0302, "application/vnd.syncml+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0303, "application/vnd.syncml+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0304, "text/vnd.wap.emn+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0305, "text/calendar"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0306, "application/vnd.omads-email+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0307, "application/vnd.omads-file+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0308, "application/vnd.omads-folder+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0309, "text/directory;profile=vCard"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030A, "application/vnd.wap.emn+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030B, "application/vnd.nokia.ipdc-purchase-response"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030C, "application/vnd.motorola.screen3+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030D, "application/vnd.motorola.screen3+gzip"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030E, "application/vnd.cmcc.setting+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x030F, "application/vnd.cmcc.bombing+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0310, "application/vnd.docomo.pf"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0311, "application/vnd.docomo.ub"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0312, "application/vnd.omaloc-supl-init"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0313, "application/vnd.oma.group-usage-list+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0314, "application/oma-directory+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0315, "application/vnd.docomo.pf2"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0316, "application/vnd.oma.drm.roap-trigger+wbxml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0317, "application/vnd.sbm.mid2"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0318, "application/vnd.wmf.bootstrap"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x0319, "application/vnc.cmcc.dcd+xml"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x031A, "application/vnd.sbm.cid"); + WELL_KNOWN_LONG_MIME_TYPES.put(0x031B, "application/vnd.oma.bcast.provisioningtrigger"); + + WELL_KNOWN_PARAMETERS.put(0x00, "Q"); + WELL_KNOWN_PARAMETERS.put(0x01, "Charset"); + WELL_KNOWN_PARAMETERS.put(0x02, "Level"); + WELL_KNOWN_PARAMETERS.put(0x03, "Type"); + WELL_KNOWN_PARAMETERS.put(0x07, "Differences"); + WELL_KNOWN_PARAMETERS.put(0x08, "Padding"); + WELL_KNOWN_PARAMETERS.put(0x09, "Type"); + WELL_KNOWN_PARAMETERS.put(0x0E, "Max-Age"); + WELL_KNOWN_PARAMETERS.put(0x10, "Secure"); + WELL_KNOWN_PARAMETERS.put(0x11, "SEC"); + WELL_KNOWN_PARAMETERS.put(0x12, "MAC"); + WELL_KNOWN_PARAMETERS.put(0x13, "Creation-date"); + WELL_KNOWN_PARAMETERS.put(0x14, "Modification-date"); + WELL_KNOWN_PARAMETERS.put(0x15, "Read-date"); + WELL_KNOWN_PARAMETERS.put(0x16, "Size"); + WELL_KNOWN_PARAMETERS.put(0x17, "Name"); + WELL_KNOWN_PARAMETERS.put(0x18, "Filename"); + WELL_KNOWN_PARAMETERS.put(0x19, "Start"); + WELL_KNOWN_PARAMETERS.put(0x1A, "Start-info"); + WELL_KNOWN_PARAMETERS.put(0x1B, "Comment"); + WELL_KNOWN_PARAMETERS.put(0x1C, "Domain"); + WELL_KNOWN_PARAMETERS.put(0x1D, "Path"); + + } + + final int WSP_DEFINED_SHORT_MIME_TYPE_COUNT = 85; + final int WSP_DEFINED_LONG_MIME_TYPE_COUNT = 85; + + private static final byte WSP_STRING_TERMINATOR = 0x00; + private static final byte WSP_SHORT_INTEGER_MASK = (byte) 0x80; + private static final byte WSP_LENGTH_QUOTE = 0x1F; + private static final byte WSP_QUOTE = 0x22; + + private static final short LONG_MIME_TYPE_OMA_DIRECTORY_XML = 0x0314; + private static final short LONG_MIME_TYPE_UNASSIGNED = 0x052C; + + private static final byte SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE = 0x3F; + private static final byte SHORT_MIME_TYPE_UNASSIGNED = 0x60; + + private static final String STRING_MIME_TYPE_ROLLOVER_CERTIFICATE + = "application/vnd.wap.rollover-certificate"; + + private static final byte TYPED_PARAM_Q = 0x00; + private static final byte TYPED_PARAM_DOMAIN = 0x1C; + private static final byte PARAM_UNASSIGNED = 0x42; + private static final byte PARAM_NO_VALUE = 0x00; + private static final byte TYPED_PARAM_SEC = 0x11; + private static final byte TYPED_PARAM_MAC = 0x12; + + public void testHasExpectedNumberOfShortMimeTypes() { + assertEquals(WSP_DEFINED_SHORT_MIME_TYPE_COUNT, WELL_KNOWN_SHORT_MIME_TYPES.size()); + } + + public void testHasExpectedNumberOfLongMimeTypes() { + assertEquals(WSP_DEFINED_LONG_MIME_TYPE_COUNT, WELL_KNOWN_LONG_MIME_TYPES.size()); + } + + public void testWellKnownShortIntegerMimeTypeValues() { + + for (int value : Wap230WspContentTypeTest.WELL_KNOWN_SHORT_MIME_TYPES.keySet()) { + WspTypeDecoder unit = new WspTypeDecoder( + HexDump.toByteArray((byte) (value | WSP_SHORT_INTEGER_MASK))); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + int wellKnownValue = (int) unit.getValue32(); + assertEquals(Wap230WspContentTypeTest.WELL_KNOWN_SHORT_MIME_TYPES.get(value), mimeType); + assertEquals(value, wellKnownValue); + assertEquals(1, unit.getDecodedDataLength()); + } + } + + public void testWellKnownLongIntegerMimeTypeValues() { + byte headerLength = 3; + byte typeLength = 2; + for (int value : Wap230WspContentTypeTest.WELL_KNOWN_SHORT_MIME_TYPES.keySet()) { + byte[] data = new byte[10]; + data[0] = headerLength; + data[1] = typeLength; + data[2] = (byte) (value >> 8); + data[3] = (byte) (value & 0xFF); + WspTypeDecoder unit = new WspTypeDecoder(data); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + int wellKnownValue = (int) unit.getValue32(); + assertEquals(Wap230WspContentTypeTest.WELL_KNOWN_SHORT_MIME_TYPES.get(value), mimeType); + assertEquals(value, wellKnownValue); + assertEquals(4, unit.getDecodedDataLength()); + } + } + + public void testDecodeReturnsFalse_WhenOnlyAZeroBytePresent() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x00); + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertFalse(unit.decodeContentType(0)); + } + + public void testConstrainedMediaExtensionMedia() throws Exception { + + String testType = "application/wibble"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(testType.getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + assertEquals(testType, mimeType); + assertEquals(-1, unit.getValue32()); + assertEquals(19, unit.getDecodedDataLength()); + } + + public void testGeneralFormShortLengthExtensionMedia() throws Exception { + + String testType = "12345678901234567890123456789"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(testType.length() + 1); + out.write(testType.getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + assertEquals(testType, mimeType); + assertEquals(-1, unit.getValue32()); + assertEquals(31, unit.getDecodedDataLength()); + } + + public void testGeneralFormShortLengthWellKnownShortInteger() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x01); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(2, unit.getDecodedDataLength()); + + } + + public void testGeneralFormShortLengthWellKnownShortIntegerWithUnknownValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x01); + out.write(SHORT_MIME_TYPE_UNASSIGNED | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + assertNull(mimeType); + assertEquals(SHORT_MIME_TYPE_UNASSIGNED, unit.getValue32()); + assertEquals(2, unit.getDecodedDataLength()); + + } + + public void testGeneralFormShortLengthWellKnownLongInteger() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x03); // header length + out.write(0x02); // type length (2 octets) + out.write(LONG_MIME_TYPE_OMA_DIRECTORY_XML >> 8); + out.write(LONG_MIME_TYPE_OMA_DIRECTORY_XML & 0xFF); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals("application/oma-directory+xml", mimeType); + assertEquals(LONG_MIME_TYPE_OMA_DIRECTORY_XML, unit.getValue32()); + assertEquals(4, unit.getDecodedDataLength()); + } + + public void testGeneralFormShortLengthWellKnownLongIntegerWithUnknownValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(0x03); // Value-length, short-length + out.write(0x02); // long-integer length (2 octets) + out.write(LONG_MIME_TYPE_UNASSIGNED >> 8); + out.write(LONG_MIME_TYPE_UNASSIGNED & 0xFF); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertNull(mimeType); + assertEquals(LONG_MIME_TYPE_UNASSIGNED, unit.getValue32()); + assertEquals(4, unit.getDecodedDataLength()); + + } + + public void testGeneralFormLengthQuoteWellKnownShortInteger() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(0x01); // Length as UINTVAR + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(3, unit.getDecodedDataLength()); + + } + + public void testGeneralFormLengthQuoteWellKnownShortIntegerWithUnknownValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(0x01); // Length as UINTVAR + out.write(SHORT_MIME_TYPE_UNASSIGNED | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + assertNull(mimeType); + assertEquals(SHORT_MIME_TYPE_UNASSIGNED, unit.getValue32()); + assertEquals(3, unit.getDecodedDataLength()); + } + + public void testGeneralFormLengthQuoteWellKnownLongInteger() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(0x03); // Length as UINTVAR + out.write(0x02); // long-integer length (2 octets) + out.write(LONG_MIME_TYPE_OMA_DIRECTORY_XML >> 8); + out.write(LONG_MIME_TYPE_OMA_DIRECTORY_XML & 0xFF); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals("application/oma-directory+xml", mimeType); + assertEquals(LONG_MIME_TYPE_OMA_DIRECTORY_XML, unit.getValue32()); + assertEquals(5, unit.getDecodedDataLength()); + + } + + public void testGeneralFormLengthQuoteWellKnownLongIntegerWithUnknownValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(0x03); // Length as UINTVAR + out.write(0x02); // long-integer length (2 octets) + out.write(LONG_MIME_TYPE_UNASSIGNED >> 8); + out.write(LONG_MIME_TYPE_UNASSIGNED & 0xFF); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertNull(mimeType); + assertEquals(LONG_MIME_TYPE_UNASSIGNED, unit.getValue32()); + assertEquals(5, unit.getDecodedDataLength()); + + } + + public void testGeneralFormLengthQuoteExtensionMedia() throws Exception { + + String testType = "application/wibble"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(testType.length() + 1); // Length as UINTVAR + + out.write(testType.getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(testType, mimeType); + assertEquals(-1, unit.getValue32()); + assertEquals(21, unit.getDecodedDataLength()); + + } + + public void testGeneralFormLengthQuoteExtensionMediaWithNiceLongMimeType() throws Exception { + + String testType = + "01234567890123456789012345678901234567890123456789012345678901234567890123456789" + +"01234567890123456789012345678901234567890123456789012345678901234567890123456789"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + + out.write(WSP_LENGTH_QUOTE); + out.write(0x81); // Length as UINTVAR (161 decimal, 0xA1), 2 bytes + out.write(0x21); + + out.write(testType.getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(testType, mimeType); + assertEquals(-1, unit.getValue32()); + assertEquals(164, unit.getDecodedDataLength()); + + } + + public void testConstrainedMediaExtensionMediaWithSpace() throws Exception { + + String testType = " application/wibble"; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(testType.getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(testType, mimeType); + assertEquals(-1, unit.getValue32()); + assertEquals(20, unit.getDecodedDataLength()); + + } + + public void testTypedParamWellKnownShortIntegerNoValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x03); // Value-length, short-length + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_DOMAIN | WSP_SHORT_INTEGER_MASK); + out.write(PARAM_NO_VALUE); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + + assertEquals(4, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals(null, params.get("Domain")); + + } + + public void testTypedParamWellKnownShortIntegerTokenText() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x14); // Value-length, short-length + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_DOMAIN | WSP_SHORT_INTEGER_MASK); + out.write("wdstechnology.com".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + + assertEquals(out.toByteArray().length, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("wdstechnology.com", params.get("Domain")); + + } + + public void testTypedParamWellKnownLongIntegerTokenText() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x15); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(0x01); + out.write(TYPED_PARAM_DOMAIN); + out.write("wdstechnology.com".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + + assertEquals(22, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("wdstechnology.com", params.get("Domain")); + + } + + public void testTypedParamWellKnownShortIntegerQuotedText() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x15); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_DOMAIN | WSP_SHORT_INTEGER_MASK); + out.write(WSP_QUOTE); + out.write("wdstechnology.com".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(0x3F, unit.getValue32()); + assertEquals(22, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("wdstechnology.com", params.get("Domain")); + + } + + public void testTypedParamWellKnownShortIntegerCompactIntegerValue() { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x3); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_SEC | WSP_SHORT_INTEGER_MASK); + out.write(0x01 | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(0x3F, unit.getValue32()); + assertEquals(4, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("1", params.get("SEC")); + + } + + public void testTypedParamWellKnownShortIntegerMultipleParameters() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x0B); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_SEC | WSP_SHORT_INTEGER_MASK); + out.write(0x01 | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_MAC | WSP_SHORT_INTEGER_MASK); + out.write(WSP_QUOTE); + out.write("imapc".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(12, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("1", params.get("SEC")); + assertEquals("imapc", params.get("MAC")); + } + + public void testUntypedParamIntegerValueShortInteger() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x0A); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); // EOS + out.write(0x45 | WSP_SHORT_INTEGER_MASK); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(11, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("69", params.get("MYPARAM")); + } + + public void testUntypedParamIntegerValueLongInteger() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x0C); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + out.write(0x02); // Short Length + out.write(0x42); // Long Integer byte 1 + out.write(0x69); // Long Integer byte 2 + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(0x3F, unit.getValue32()); + assertEquals(13, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("17001", params.get("MYPARAM")); + } + + public void testUntypedParamTextNoValue() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x0A); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + out.write(PARAM_NO_VALUE); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(11, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals(null, params.get("MYPARAM")); + + } + + public void testUntypedParamTextTokenText() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x11); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + out.write("myvalue".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(18, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("myvalue", params.get("MYPARAM")); + } + + public void testUntypedParamTextQuotedString() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x11); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + out.write(WSP_QUOTE); + out.write("myvalue".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + assertEquals(19, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("myvalue", params.get("MYPARAM")); + + } + + public void testDecodesReturnsFalse_ForParamWithMissingValue() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x09); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write("MYPARAM".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertFalse(unit.decodeContentType(0)); + } + + public void testTypedParamTextQValue() { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x04); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(TYPED_PARAM_Q); + out.write(0x83); // Q value byte 1 + out.write(0x31); // Q value byte 2 + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(0x3F, unit.getValue32()); + assertEquals(5, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("433", params.get("Q")); + + } + + public void testTypedParamUnassignedWellKnownShortIntegerTokenText() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x14); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(PARAM_UNASSIGNED | WSP_SHORT_INTEGER_MASK); + out.write("wdstechnology.com".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + + assertEquals(21, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("wdstechnology.com", params.get("unassigned/0x42")); + + } + + public void testTypedParamUnassignedWellKnownLongIntegerTokenText() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x15); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(0x01); // Short-length of well-known parameter token + out.write(PARAM_UNASSIGNED); + out.write("wdstechnology.com".getBytes("US-ASCII")); + out.write(WSP_STRING_TERMINATOR); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertTrue(unit.decodeContentType(0)); + + String mimeType = unit.getValueString(); + + assertEquals(STRING_MIME_TYPE_ROLLOVER_CERTIFICATE, mimeType); + assertEquals(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE, unit.getValue32()); + + assertEquals(22, unit.getDecodedDataLength()); + + Map<String, String> params = unit.getContentParameters(); + assertEquals("wdstechnology.com", params.get("unassigned/0x42")); + } + + public void testDecodesReturnsFalse_WhenParamValueNotTerminated() throws Exception { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x15); + out.write(SHORT_MIME_TYPE_ROLLOVER_CERTIFICATE | WSP_SHORT_INTEGER_MASK); + out.write(0x01); + out.write(PARAM_UNASSIGNED); + out.write("wdstechnology.com".getBytes("US-ASCII")); + + WspTypeDecoder unit = new WspTypeDecoder(out.toByteArray()); + assertFalse(unit.decodeContentType(0)); + } +}
\ No newline at end of file diff --git a/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java b/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java new file mode 100644 index 0000000..d2faceb --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java @@ -0,0 +1,746 @@ +/* + * Copyright (C) 2012 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.internal.telephony.cdma; + +import android.os.Parcel; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbMessage; +import android.telephony.cdma.CdmaSmsCbProgramData; +import android.test.AndroidTestCase; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.cdma.sms.BearerData; +import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.BitwiseOutputStream; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * Test cases for basic SmsCbMessage operation for CDMA. + */ +public class CdmaSmsCbTest extends AndroidTestCase { + + /* Copy of private subparameter identifier constants from BearerData class. */ + private static final byte SUBPARAM_MESSAGE_IDENTIFIER = (byte) 0x00; + private static final byte SUBPARAM_USER_DATA = (byte) 0x01; + private static final byte SUBPARAM_PRIORITY_INDICATOR = (byte) 0x08; + private static final byte SUBPARAM_LANGUAGE_INDICATOR = (byte) 0x0D; + private static final byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA = 0x12; + + /** + * Initialize a Parcel for an incoming CDMA cell broadcast. The caller will write the + * bearer data and then convert it to an SmsMessage. + * @param serviceCategory the CDMA service category + * @return the initialized Parcel + */ + private static Parcel createBroadcastParcel(int serviceCategory) { + Parcel p = Parcel.obtain(); + + p.writeInt(SmsEnvelope.TELESERVICE_NOT_SET); + p.writeByte((byte) 1); // non-zero for MESSAGE_TYPE_BROADCAST + p.writeInt(serviceCategory); + + // dummy address (RIL may generate a different dummy address for broadcasts) + p.writeInt(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); // sAddress.digit_mode + p.writeInt(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); // sAddress.number_mode + p.writeInt(CdmaSmsAddress.TON_UNKNOWN); // sAddress.number_type + p.writeInt(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY); // sAddress.number_plan + p.writeByte((byte) 0); // sAddress.number_of_digits + p.writeInt((byte) 0); // sSubAddress.subaddressType + p.writeByte((byte) 0); // sSubAddress.odd + p.writeByte((byte) 0); // sSubAddress.number_of_digits + return p; + } + + /** + * Initialize a BitwiseOutputStream with the CDMA bearer data subparameters except for + * user data. The caller will append the user data and add it to the parcel. + * @param messageId the 16-bit message identifier + * @param priority message priority + * @param language message language code + * @return the initialized BitwiseOutputStream + */ + private static BitwiseOutputStream createBearerDataStream(int messageId, int priority, + int language) throws BitwiseOutputStream.AccessException { + BitwiseOutputStream bos = new BitwiseOutputStream(10); + bos.write(8, SUBPARAM_MESSAGE_IDENTIFIER); + bos.write(8, 3); // length: 3 bytes + bos.write(4, BearerData.MESSAGE_TYPE_DELIVER); + bos.write(8, ((messageId >>> 8) & 0xff)); + bos.write(8, (messageId & 0xff)); + bos.write(1, 0); // no User Data Header + bos.write(3, 0); // reserved + + if (priority != -1) { + bos.write(8, SUBPARAM_PRIORITY_INDICATOR); + bos.write(8, 1); // length: 1 byte + bos.write(2, (priority & 0x03)); + bos.write(6, 0); // reserved + } + + if (language != -1) { + bos.write(8, SUBPARAM_LANGUAGE_INDICATOR); + bos.write(8, 1); // length: 1 byte + bos.write(8, (language & 0xff)); + } + + return bos; + } + + /** + * Write the bearer data array to the parcel, then return a new SmsMessage from the parcel. + * @param p the parcel containing the CDMA SMS headers + * @param bearerData the bearer data byte array to append to the parcel + * @return the new SmsMessage created from the parcel + */ + private static SmsMessage createMessageFromParcel(Parcel p, byte[] bearerData) { + p.writeInt(bearerData.length); + for (byte b : bearerData) { + p.writeByte(b); + } + p.setDataPosition(0); // reset position for reading + SmsMessage message = SmsMessage.newFromParcel(p); + p.recycle(); + return message; + } + + /** + * Create a parcel for an incoming CMAS broadcast, then return a new SmsMessage created + * from the parcel. + * @param serviceCategory the CDMA service category + * @param messageId the 16-bit message identifier + * @param priority message priority + * @param language message language code + * @param body message body + * @param cmasCategory CMAS category (or -1 to skip adding CMAS type 1 elements record) + * @param responseType CMAS response type + * @param severity CMAS severity + * @param urgency CMAS urgency + * @param certainty CMAS certainty + * @return the newly created SmsMessage object + */ + private static SmsMessage createCmasSmsMessage(int serviceCategory, int messageId, int priority, + int language, int encoding, String body, int cmasCategory, int responseType, + int severity, int urgency, int certainty) throws Exception { + BitwiseOutputStream cmasBos = new BitwiseOutputStream(10); + cmasBos.write(8, 0); // CMAE protocol version 0 + + if (body != null) { + cmasBos.write(8, 0); // Type 0 elements (alert text) + encodeBody(encoding, body, true, cmasBos); + } + + if (cmasCategory != SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN) { + cmasBos.write(8, 1); // Type 1 elements + cmasBos.write(8, 4); // length: 4 bytes + cmasBos.write(8, (cmasCategory & 0xff)); + cmasBos.write(8, (responseType & 0xff)); + cmasBos.write(4, (severity & 0x0f)); + cmasBos.write(4, (urgency & 0x0f)); + cmasBos.write(4, (certainty & 0x0f)); + cmasBos.write(4, 0); // pad to octet boundary + } + + byte[] cmasUserData = cmasBos.toByteArray(); + + Parcel p = createBroadcastParcel(serviceCategory); + BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language); + + bos.write(8, SUBPARAM_USER_DATA); + bos.write(8, cmasUserData.length + 2); // add 2 bytes for msg_encoding and num_fields + bos.write(5, UserData.ENCODING_OCTET); + bos.write(8, cmasUserData.length); + bos.writeByteArray(cmasUserData.length * 8, cmasUserData); + bos.write(3, 0); // pad to byte boundary + + return createMessageFromParcel(p, bos.toByteArray()); + } + + /** + * Create a parcel for an incoming CDMA cell broadcast, then return a new SmsMessage created + * from the parcel. + * @param serviceCategory the CDMA service category + * @param messageId the 16-bit message identifier + * @param priority message priority + * @param language message language code + * @param encoding user data encoding method + * @param body the message body + * @return the newly created SmsMessage object + */ + private static SmsMessage createBroadcastSmsMessage(int serviceCategory, int messageId, + int priority, int language, int encoding, String body) throws Exception { + Parcel p = createBroadcastParcel(serviceCategory); + BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language); + + bos.write(8, SUBPARAM_USER_DATA); + encodeBody(encoding, body, false, bos); + + return createMessageFromParcel(p, bos.toByteArray()); + } + + /** + * Append the message length, encoding, and body to the BearerData output stream. + * This is used for writing the User Data subparameter for non-CMAS broadcasts and for + * writing the alert text for CMAS broadcasts. + * @param encoding one of the CDMA UserData encoding values + * @param body the message body + * @param isCmasRecord true if this is a CMAS type 0 elements record; false for user data + * @param bos the BitwiseOutputStream to write to + * @throws Exception on any encoding error + */ + private static void encodeBody(int encoding, String body, boolean isCmasRecord, + BitwiseOutputStream bos) throws Exception { + if (encoding == UserData.ENCODING_7BIT_ASCII || encoding == UserData.ENCODING_IA5) { + int charCount = body.length(); + int recordBits = (charCount * 7) + 5; // add 5 bits for char set field + int recordOctets = (recordBits + 7) / 8; // round up to octet boundary + int padBits = (recordOctets * 8) - recordBits; + + if (!isCmasRecord) { + recordOctets++; // add 8 bits for num_fields + } + + bos.write(8, recordOctets); + bos.write(5, (encoding & 0x1f)); + + if (!isCmasRecord) { + bos.write(8, charCount); + } + + for (int i = 0; i < charCount; i++) { + bos.write(7, body.charAt(i)); + } + + bos.write(padBits, 0); // pad to octet boundary + } else if (encoding == UserData.ENCODING_GSM_7BIT_ALPHABET + || encoding == UserData.ENCODING_GSM_DCS) { + // convert to 7-bit packed encoding with septet count in index 0 of byte array + byte[] encodedBody = GsmAlphabet.stringToGsm7BitPacked(body); + + int charCount = encodedBody[0]; // septet count + int recordBits = (charCount * 7) + 5; // add 5 bits for char set field + int recordOctets = (recordBits + 7) / 8; // round up to octet boundary + int padBits = (recordOctets * 8) - recordBits; + + if (!isCmasRecord) { + recordOctets++; // add 8 bits for num_fields + if (encoding == UserData.ENCODING_GSM_DCS) { + recordOctets++; // add 8 bits for DCS (message type) + } + } + + bos.write(8, recordOctets); + bos.write(5, (encoding & 0x1f)); + + if (!isCmasRecord && encoding == UserData.ENCODING_GSM_DCS) { + bos.write(8, 0); // GSM DCS: 7 bit default alphabet, no msg class + } + + if (!isCmasRecord) { + bos.write(8, charCount); + } + byte[] bodySeptets = Arrays.copyOfRange(encodedBody, 1, encodedBody.length); + bos.writeByteArray(charCount * 7, bodySeptets); + bos.write(padBits, 0); // pad to octet boundary + } else if (encoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) { + // 6 bit packed encoding with 0x20 offset (ASCII 0x20 - 0x60) + int charCount = body.length(); + int recordBits = (charCount * 6) + 21; // add 21 bits for header fields + int recordOctets = (recordBits + 7) / 8; // round up to octet boundary + int padBits = (recordOctets * 8) - recordBits; + + bos.write(8, recordOctets); + + bos.write(5, (encoding & 0x1f)); + bos.write(8, UserData.IS91_MSG_TYPE_SHORT_MESSAGE); + bos.write(8, charCount); + + for (int i = 0; i < charCount; i++) { + bos.write(6, ((int) body.charAt(i) - 0x20)); + } + + bos.write(padBits, 0); // pad to octet boundary + } else { + byte[] encodedBody; + switch (encoding) { + case UserData.ENCODING_UNICODE_16: + encodedBody = body.getBytes("UTF-16BE"); + break; + + case UserData.ENCODING_SHIFT_JIS: + encodedBody = body.getBytes("Shift_JIS"); + break; + + case UserData.ENCODING_KOREAN: + encodedBody = body.getBytes("KSC5601"); + break; + + case UserData.ENCODING_LATIN_HEBREW: + encodedBody = body.getBytes("ISO-8859-8"); + break; + + case UserData.ENCODING_LATIN: + default: + encodedBody = body.getBytes("ISO-8859-1"); + break; + } + int charCount = body.length(); // use actual char count for num fields + int recordOctets = encodedBody.length + 1; // add 1 byte for encoding and pad bits + if (!isCmasRecord) { + recordOctets++; // add 8 bits for num_fields + } + bos.write(8, recordOctets); + bos.write(5, (encoding & 0x1f)); + if (!isCmasRecord) { + bos.write(8, charCount); + } + bos.writeByteArray(encodedBody.length * 8, encodedBody); + bos.write(3, 0); // pad to octet boundary + } + } + + private static final String TEST_TEXT = "This is a test CDMA cell broadcast message..." + + "678901234567890123456789012345678901234567890"; + + private static final String PRES_ALERT = + "THE PRESIDENT HAS ISSUED AN EMERGENCY ALERT. CHECK LOCAL MEDIA FOR MORE DETAILS"; + + private static final String EXTREME_ALERT = "FLASH FLOOD WARNING FOR SOUTH COCONINO COUNTY" + + " - NORTH CENTRAL ARIZONA UNTIL 415 PM MST"; + + private static final String SEVERE_ALERT = "SEVERE WEATHER WARNING FOR SOMERSET COUNTY" + + " - NEW JERSEY UNTIL 415 PM MST"; + + private static final String AMBER_ALERT = + "AMBER ALERT:Mountain View,CA VEH'07 Blue Honda Civic CA LIC 5ABC123"; + + private static final String MONTHLY_TEST_ALERT = "This is a test of the emergency alert system." + + " This is only a test. 89012345678901234567890"; + + private static final String IS91_TEXT = "IS91 SHORT MSG"; // max length 14 chars + + /** + * Verify that the SmsCbMessage has the correct values for CDMA. + * @param cbMessage the message to test + */ + private static void verifyCbValues(SmsCbMessage cbMessage) { + assertEquals(SmsCbMessage.MESSAGE_FORMAT_3GPP2, cbMessage.getMessageFormat()); + assertEquals(SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, cbMessage.getGeographicalScope()); + assertEquals(false, cbMessage.isEtwsMessage()); // ETWS on CDMA not currently supported + } + + private static void doTestNonEmergencyBroadcast(int encoding) throws Exception { + SmsMessage msg = createBroadcastSmsMessage(123, 456, BearerData.PRIORITY_NORMAL, + BearerData.LANGUAGE_ENGLISH, encoding, TEST_TEXT); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + verifyCbValues(cbMessage); + assertEquals(123, cbMessage.getServiceCategory()); + assertEquals(456, cbMessage.getSerialNumber()); + assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority()); + assertEquals("en", cbMessage.getLanguageCode()); + assertEquals(TEST_TEXT, cbMessage.getMessageBody()); + assertEquals(false, cbMessage.isEmergencyMessage()); + assertEquals(false, cbMessage.isCmasMessage()); + } + + public void testNonEmergencyBroadcast7bitAscii() throws Exception { + doTestNonEmergencyBroadcast(UserData.ENCODING_7BIT_ASCII); + } + + public void testNonEmergencyBroadcast7bitGsm() throws Exception { + doTestNonEmergencyBroadcast(UserData.ENCODING_GSM_7BIT_ALPHABET); + } + + public void testNonEmergencyBroadcast16bitUnicode() throws Exception { + doTestNonEmergencyBroadcast(UserData.ENCODING_UNICODE_16); + } + + public void testNonEmergencyBroadcastIs91Extended() throws Exception { + // IS-91 doesn't support language or priority subparameters, max 14 chars text + SmsMessage msg = createBroadcastSmsMessage(987, 654, -1, -1, + UserData.ENCODING_IS91_EXTENDED_PROTOCOL, IS91_TEXT); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + verifyCbValues(cbMessage); + assertEquals(987, cbMessage.getServiceCategory()); + assertEquals(654, cbMessage.getSerialNumber()); + assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority()); + assertEquals(null, cbMessage.getLanguageCode()); + assertEquals(IS91_TEXT, cbMessage.getMessageBody()); + assertEquals(false, cbMessage.isEmergencyMessage()); + assertEquals(false, cbMessage.isCmasMessage()); + } + + private static void doTestCmasBroadcast(int serviceCategory, int messageClass, String body) + throws Exception { + SmsMessage msg = createCmasSmsMessage( + serviceCategory, 1234, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH, + UserData.ENCODING_7BIT_ASCII, body, -1, -1, -1, -1, -1); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + verifyCbValues(cbMessage); + assertEquals(serviceCategory, cbMessage.getServiceCategory()); + assertEquals(1234, cbMessage.getSerialNumber()); + assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority()); + assertEquals("en", cbMessage.getLanguageCode()); + assertEquals(body, cbMessage.getMessageBody()); + assertEquals(true, cbMessage.isEmergencyMessage()); + assertEquals(true, cbMessage.isCmasMessage()); + SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo(); + assertEquals(messageClass, cmasInfo.getMessageClass()); + assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory()); + assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType()); + assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity()); + assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency()); + assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty()); + } + + public void testCmasPresidentialAlert() throws Exception { + doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, + SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, PRES_ALERT); + } + + public void testCmasExtremeAlert() throws Exception { + doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, + SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, EXTREME_ALERT); + } + + public void testCmasSevereAlert() throws Exception { + doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, + SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT, SEVERE_ALERT); + } + + public void testCmasAmberAlert() throws Exception { + doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, + SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY, AMBER_ALERT); + } + + public void testCmasTestMessage() throws Exception { + doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE, + SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST, MONTHLY_TEST_ALERT); + } + + public void testCmasExtremeAlertType1Elements() throws Exception { + SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, + 5678, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH, + UserData.ENCODING_7BIT_ASCII, EXTREME_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_ENV, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, + SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + verifyCbValues(cbMessage); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, + cbMessage.getServiceCategory()); + assertEquals(5678, cbMessage.getSerialNumber()); + assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority()); + assertEquals("en", cbMessage.getLanguageCode()); + assertEquals(EXTREME_ALERT, cbMessage.getMessageBody()); + assertEquals(true, cbMessage.isEmergencyMessage()); + assertEquals(true, cbMessage.isCmasMessage()); + SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo(); + assertEquals(SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, cmasInfo.getMessageClass()); + assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_ENV, cmasInfo.getCategory()); + assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, cmasInfo.getResponseType()); + assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, cmasInfo.getSeverity()); + assertEquals(SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, cmasInfo.getUrgency()); + assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY, cmasInfo.getCertainty()); + } + + // VZW requirement is to discard message with unsupported charset. Verify that we return null + // for this unsupported character set. + public void testCmasUnsupportedCharSet() throws Exception { + SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, + 12345, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH, + UserData.ENCODING_GSM_DCS, EXTREME_ALERT, -1, -1, -1, -1, -1); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + assertNull("expected null for unsupported charset", cbMessage); + } + + // VZW requirement is to discard message with unsupported charset. Verify that we return null + // for this unsupported character set. + public void testCmasUnsupportedCharSet2() throws Exception { + SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, + 67890, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH, + UserData.ENCODING_KOREAN, EXTREME_ALERT, -1, -1, -1, -1, -1); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + assertNull("expected null for unsupported charset", cbMessage); + } + + // VZW requirement is to discard message without record type 0. The framework will decode it + // and the app will discard it. + public void testCmasNoRecordType0() throws Exception { + SmsMessage msg = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, 1234, + BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH, + UserData.ENCODING_7BIT_ASCII, null, -1, -1, -1, -1, -1); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + verifyCbValues(cbMessage); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, + cbMessage.getServiceCategory()); + assertEquals(1234, cbMessage.getSerialNumber()); + assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority()); + assertEquals("en", cbMessage.getLanguageCode()); + assertEquals(null, cbMessage.getMessageBody()); + assertEquals(true, cbMessage.isEmergencyMessage()); + assertEquals(true, cbMessage.isCmasMessage()); + SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo(); + assertEquals(SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, cmasInfo.getMessageClass()); + assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory()); + assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType()); + assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity()); + assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency()); + assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty()); + } + + // Make sure we don't throw an exception if we feed completely random data to BearerStream. + public void testRandomBearerStreamData() { + Random r = new Random(54321); + for (int run = 0; run < 1000; run++) { + int len = r.nextInt(140); + byte[] data = new byte[len]; + for (int i = 0; i < len; i++) { + data[i] = (byte) r.nextInt(256); + } + // Log.d("CdmaSmsCbTest", "trying random bearer data run " + run + " length " + len); + try { + int category = 0x0ff0 + r.nextInt(32); // half CMAS, half non-CMAS + Parcel p = createBroadcastParcel(category); + SmsMessage msg = createMessageFromParcel(p, data); + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + // with random input, cbMessage will almost always be null (log when it isn't) + if (cbMessage != null) { + Log.d("CdmaSmsCbTest", "success: " + cbMessage); + } + } catch (Exception e) { + Log.d("CdmaSmsCbTest", "exception thrown", e); + fail("Exception in decoder at run " + run + " length " + len + ": " + e); + } + } + } + + // Make sure we don't throw an exception if we put random data in the UserData subparam. + public void testRandomUserData() { + Random r = new Random(94040); + for (int run = 0; run < 1000; run++) { + int category = 0x0ff0 + r.nextInt(32); // half CMAS, half non-CMAS + Parcel p = createBroadcastParcel(category); + int len = r.nextInt(140); + // Log.d("CdmaSmsCbTest", "trying random user data run " + run + " length " + len); + + try { + BitwiseOutputStream bos = createBearerDataStream(r.nextInt(65536), r.nextInt(4), + r.nextInt(256)); + + bos.write(8, SUBPARAM_USER_DATA); + bos.write(8, len); + + for (int i = 0; i < len; i++) { + bos.write(8, r.nextInt(256)); + } + + SmsMessage msg = createMessageFromParcel(p, bos.toByteArray()); + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + } catch (Exception e) { + Log.d("CdmaSmsCbTest", "exception thrown", e); + fail("Exception in decoder at run " + run + " length " + len + ": " + e); + } + } + } + + /** + * Initialize a Parcel for incoming Service Category Program Data teleservice. The caller will + * write the bearer data and then convert it to an SmsMessage. + * @return the initialized Parcel + */ + private static Parcel createServiceCategoryProgramDataParcel() { + Parcel p = Parcel.obtain(); + + p.writeInt(SmsEnvelope.TELESERVICE_SCPT); + p.writeByte((byte) 0); // non-zero for MESSAGE_TYPE_BROADCAST + p.writeInt(0); + + // dummy address (RIL may generate a different dummy address for broadcasts) + p.writeInt(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); // sAddress.digit_mode + p.writeInt(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); // sAddress.number_mode + p.writeInt(CdmaSmsAddress.TON_UNKNOWN); // sAddress.number_type + p.writeInt(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY); // sAddress.number_plan + p.writeByte((byte) 0); // sAddress.number_of_digits + p.writeInt((byte) 0); // sSubAddress.subaddressType + p.writeByte((byte) 0); // sSubAddress.odd + p.writeByte((byte) 0); // sSubAddress.number_of_digits + return p; + } + + private static final String CAT_EXTREME_THREAT = "Extreme Threat to Life and Property"; + private static final String CAT_SEVERE_THREAT = "Severe Threat to Life and Property"; + private static final String CAT_AMBER_ALERTS = "AMBER Alerts"; + + public void testServiceCategoryProgramDataAddCategory() throws Exception { + Parcel p = createServiceCategoryProgramDataParcel(); + BitwiseOutputStream bos = createBearerDataStream(123, -1, -1); + + int categoryNameLength = CAT_EXTREME_THREAT.length(); + int subparamLengthBits = (53 + (categoryNameLength * 7)); + int subparamLengthBytes = (subparamLengthBits + 7) / 8; + int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits; + + bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA); + bos.write(8, subparamLengthBytes); + bos.write(5, UserData.ENCODING_7BIT_ASCII); + + bos.write(4, CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 100); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT); + + bos.write(8, categoryNameLength); + for (int i = 0; i < categoryNameLength; i++) { + bos.write(7, CAT_EXTREME_THREAT.charAt(i)); + } + bos.write(subparamPadBits, 0); + + SmsMessage msg = createMessageFromParcel(p, bos.toByteArray()); + assertNotNull(msg); + msg.parseSms(); + List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData(); + assertNotNull(programDataList); + assertEquals(1, programDataList.size()); + CdmaSmsCbProgramData programData = programDataList.get(0); + assertEquals(CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, programData.getCategory()); + assertEquals(CAT_EXTREME_THREAT, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(100, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT, programData.getAlertOption()); + } + + public void testServiceCategoryProgramDataDeleteTwoCategories() throws Exception { + Parcel p = createServiceCategoryProgramDataParcel(); + BitwiseOutputStream bos = createBearerDataStream(456, -1, -1); + + int category1NameLength = CAT_SEVERE_THREAT.length(); + int category2NameLength = CAT_AMBER_ALERTS.length(); + + int subparamLengthBits = (101 + (category1NameLength * 7) + (category2NameLength * 7)); + int subparamLengthBytes = (subparamLengthBits + 7) / 8; + int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits; + + bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA); + bos.write(8, subparamLengthBytes); + bos.write(5, UserData.ENCODING_7BIT_ASCII); + + bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 0); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT); + + bos.write(8, category1NameLength); + for (int i = 0; i < category1NameLength; i++) { + bos.write(7, CAT_SEVERE_THREAT.charAt(i)); + } + + bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 0); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT); + + bos.write(8, category2NameLength); + for (int i = 0; i < category2NameLength; i++) { + bos.write(7, CAT_AMBER_ALERTS.charAt(i)); + } + + bos.write(subparamPadBits, 0); + + SmsMessage msg = createMessageFromParcel(p, bos.toByteArray()); + assertNotNull(msg); + msg.parseSms(); + List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData(); + assertNotNull(programDataList); + assertEquals(2, programDataList.size()); + + CdmaSmsCbProgramData programData = programDataList.get(0); + assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, programData.getCategory()); + assertEquals(CAT_SEVERE_THREAT, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(0, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption()); + + programData = programDataList.get(1); + assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, + programData.getCategory()); + assertEquals(CAT_AMBER_ALERTS, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(0, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption()); + } + + private static final byte[] CMAS_TEST_BEARER_DATA = { + 0x00, 0x03, 0x1C, 0x78, 0x00, 0x01, 0x59, 0x02, (byte) 0xB8, 0x00, 0x02, 0x10, (byte) 0xAA, + 0x68, (byte) 0xD3, (byte) 0xCD, 0x06, (byte) 0x9E, 0x68, 0x30, (byte) 0xA0, (byte) 0xE9, + (byte) 0x97, (byte) 0x9F, 0x44, 0x1B, (byte) 0xF3, 0x20, (byte) 0xE9, (byte) 0xA3, + 0x2A, 0x08, 0x7B, (byte) 0xF6, (byte) 0xED, (byte) 0xCB, (byte) 0xCB, 0x1E, (byte) 0x9C, + 0x3B, 0x10, 0x4D, (byte) 0xDF, (byte) 0x8B, 0x4E, + (byte) 0xCC, (byte) 0xA8, 0x20, (byte) 0xEC, (byte) 0xCB, (byte) 0xCB, (byte) 0xA2, 0x0A, + 0x7E, 0x79, (byte) 0xF4, (byte) 0xCB, (byte) 0xB5, 0x72, 0x0A, (byte) 0x9A, 0x34, + (byte) 0xF3, 0x41, (byte) 0xA7, (byte) 0x9A, 0x0D, (byte) 0xFB, (byte) 0xB6, 0x79, 0x41, + (byte) 0x85, 0x07, 0x4C, (byte) 0xBC, (byte) 0xFA, 0x2E, 0x00, 0x08, 0x20, 0x58, 0x38, + (byte) 0x88, (byte) 0x80, 0x10, 0x54, 0x06, 0x38, 0x20, 0x60, + 0x30, (byte) 0xA8, (byte) 0x81, (byte) 0x90, 0x20, 0x08 + }; + + // Test case for CMAS test message received on the Sprint network. + public void testDecodeRawBearerData() throws Exception { + Parcel p = createBroadcastParcel(SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE); + SmsMessage msg = createMessageFromParcel(p, CMAS_TEST_BEARER_DATA); + + SmsCbMessage cbMessage = msg.parseBroadcastSms(); + assertNotNull("expected non-null for bearer data", cbMessage); + assertEquals("geoScope", cbMessage.getGeographicalScope(), 1); + assertEquals("serialNumber", cbMessage.getSerialNumber(), 51072); + assertEquals("serviceCategory", cbMessage.getServiceCategory(), + SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE); + assertEquals("payload", cbMessage.getMessageBody(), + "This is a test of the Commercial Mobile Alert System. This is only a test."); + + SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo(); + assertNotNull("expected non-null for CMAS info", cmasInfo); + assertEquals("category", cmasInfo.getCategory(), SmsCbCmasInfo.CMAS_CATEGORY_OTHER); + assertEquals("responseType", cmasInfo.getResponseType(), + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_NONE); + assertEquals("severity", cmasInfo.getSeverity(), SmsCbCmasInfo.CMAS_SEVERITY_SEVERE); + assertEquals("urgency", cmasInfo.getUrgency(), SmsCbCmasInfo.CMAS_URGENCY_EXPECTED); + assertEquals("certainty", cmasInfo.getCertainty(), SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java b/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java new file mode 100644 index 0000000..bb37b65 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/cdma/sms/CdmaSmsTest.java @@ -0,0 +1,887 @@ +/* + * Copyright (C) 2006 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.internal.telephony.cdma.sms; + +import android.telephony.TelephonyManager; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.SmsHeader; +import com.android.internal.telephony.cdma.SmsMessage; +import com.android.internal.telephony.cdma.sms.BearerData; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; +import com.android.internal.util.BitwiseInputStream; +import com.android.internal.util.BitwiseOutputStream; +import com.android.internal.util.HexDump; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import android.util.Log; + +import java.util.ArrayList; + +public class CdmaSmsTest extends AndroidTestCase { + private final static String LOG_TAG = "XXX CdmaSmsTest XXX"; + + @SmallTest + public void testCdmaSmsAddrParsing() throws Exception { + CdmaSmsAddress addr = CdmaSmsAddress.parse("6502531000"); + assertEquals(addr.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 10); + assertEquals(addr.origBytes.length, 10); + byte[] data = {6, 5, 10, 2, 5, 3, 1, 10, 10, 10}; + for (int i = 0; i < data.length; i++) { + assertEquals(addr.origBytes[i], data[i]); + } + addr = CdmaSmsAddress.parse("(650) 253-1000"); + assertEquals(addr.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 10); + assertEquals(addr.origBytes.length, 10); + byte[] data2 = {6, 5, 10, 2, 5, 3, 1, 10, 10, 10}; + for (int i = 0; i < data2.length; i++) { + assertEquals(addr.origBytes[i], data2[i]); + } + addr = CdmaSmsAddress.parse("650.253.1000"); + assertEquals(addr.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 10); + assertEquals(addr.origBytes.length, 10); + byte[] data5 = {6, 5, 10, 2, 5, 3, 1, 10, 10, 10}; + for (int i = 0; i < data2.length; i++) { + assertEquals(addr.origBytes[i], data5[i]); + } + addr = CdmaSmsAddress.parse("(+886) 917 222 555"); + assertEquals(addr.ton, CdmaSmsAddress.TON_INTERNATIONAL_OR_IP); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 12); + assertEquals(addr.origBytes.length, 12); + byte[] data3 = {8, 8, 6, 9, 1, 7, 2, 2, 2, 5, 5, 5}; + for (int i = 0; i < data3.length; i++) { + assertEquals(addr.origBytes[i], data3[i]); + } + addr = CdmaSmsAddress.parse("(650) *253-1000 #600"); + byte[] data4 = {6, 5, 10, 11, 2, 5, 3, 1, 10, 10, 10, 12, 6, 10, 10}; + for (int i = 0; i < data4.length; i++) { + assertEquals(addr.origBytes[i], data4[i]); + } + String input = "x@y.com,a@b.com"; + addr = CdmaSmsAddress.parse(input); + assertEquals(addr.ton, CdmaSmsAddress.TON_NATIONAL_OR_EMAIL); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 15); + assertEquals(addr.origBytes.length, 15); + assertEquals(new String(addr.origBytes), input); + addr = CdmaSmsAddress.parse("foo bar"); + assertEquals(addr.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(addr.digitMode, CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR); + assertEquals(addr.numberMode, CdmaSmsAddress.NUMBER_MODE_DATA_NETWORK); + assertEquals(addr.numberOfDigits, 6); + assertEquals(addr.origBytes.length, 6); + assertEquals(new String(addr.origBytes), "foobar"); + addr = CdmaSmsAddress.parse("f\noo\tb a\rr"); + assertEquals(new String(addr.origBytes), "foobar"); + assertEquals(CdmaSmsAddress.parse("f\u0000oo bar"), null); + assertEquals(CdmaSmsAddress.parse("f\u0007oo bar"), null); + assertEquals(CdmaSmsAddress.parse("f\u0080oo bar"), null); + assertEquals(CdmaSmsAddress.parse("f\u1ECFboo\u001fbar"), null); + assertEquals(CdmaSmsAddress.parse("f\u0080oo bar"), null); + } + + @SmallTest + public void testUserData7bitGsm() throws Exception { + String pdu = "00031040900112488ea794e074d69e1b7392c270326cde9e98"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals("Test standard SMS", bearerData.userData.payloadStr); + } + + @SmallTest + public void testUserData7bitAscii() throws Exception { + String pdu = "0003100160010610262d5ab500"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals("bjjj", bearerData.userData.payloadStr); + } + + @SmallTest + public void testUserData7bitAsciiTwo() throws Exception { + String pdu = "00031001d00109104539b4d052ebb3d0"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals("SMS Rulz", bearerData.userData.payloadStr); + } + + @SmallTest + public void testUserDataIa5() throws Exception { + String pdu = "00031002100109184539b4d052ebb3d0"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals("SMS Rulz", bearerData.userData.payloadStr); + } + + @SmallTest + public void testUserData7bitAsciiFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "Test standard SMS"; + userData.msgEncoding = UserData.ENCODING_7BIT_ASCII; + userData.msgEncodingSet = true; + bearerData.userData = userData; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(BearerData.MESSAGE_TYPE_DELIVER, revBearerData.messageType); + assertEquals(0, revBearerData.messageId); + assertEquals(false, revBearerData.hasUserDataHeader); + assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); + assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "Test \u007f standard \u0000 SMS"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals("Test standard SMS", revBearerData.userData.payloadStr); + userData.payloadStr = "Test \n standard \r SMS"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + } + + @SmallTest + public void testUserData7bitGsmFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "Test standard SMS"; + userData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET; + userData.msgEncodingSet = true; + bearerData.userData = userData; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(BearerData.MESSAGE_TYPE_DELIVER, revBearerData.messageType); + assertEquals(0, revBearerData.messageId); + assertEquals(false, revBearerData.hasUserDataHeader); + assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); + assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "1234567"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789012345678901234567890" + + "1234567890"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "Test \u007f illegal \u0000 SMS chars"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals("Test illegal SMS chars", revBearerData.userData.payloadStr); + userData.payloadStr = "More @ testing\nis great^|^~woohoo"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0xEE; + concatRef.msgCount = 2; + concatRef.seqNumber = 2; + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + byte[] encodedHeader = SmsHeader.toByteArray(smsHeader); + userData.userDataHeader = smsHeader; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + SmsHeader decodedHeader = revBearerData.userData.userDataHeader; + assertEquals(decodedHeader.concatRef.refNumber, concatRef.refNumber); + assertEquals(decodedHeader.concatRef.msgCount, concatRef.msgCount); + assertEquals(decodedHeader.concatRef.seqNumber, concatRef.seqNumber); + } + + @SmallTest + public void testUserDataUtf16Feedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "\u0160u\u1E5B\u0301r\u1ECFg\uD835\uDC1At\u00E9\u4E002\u3042"; + userData.msgEncoding = UserData.ENCODING_UNICODE_16; + userData.msgEncodingSet = true; + bearerData.userData = userData; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(BearerData.MESSAGE_TYPE_DELIVER, revBearerData.messageType); + assertEquals(0, revBearerData.messageId); + assertEquals(false, revBearerData.hasUserDataHeader); + assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); + assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.msgEncoding = UserData.ENCODING_OCTET; + userData.msgEncodingSet = false; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(BearerData.MESSAGE_TYPE_DELIVER, revBearerData.messageType); + assertEquals(0, revBearerData.messageId); + assertEquals(false, revBearerData.hasUserDataHeader); + assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding); + assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = "1234567"; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + userData.payloadStr = ""; + revBearerData = BearerData.decode(BearerData.encode(bearerData)); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + } + + @SmallTest + public void testMonolithicOne() throws Exception { + String pdu = "0003200010010410168d2002010503060812011101590501c706069706180000000701c108" + + "01c00901800a01e00b01030c01c00d01070e05039acc13880f018011020566"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals(bearerData.messageType, BearerData.MESSAGE_TYPE_SUBMIT); + assertEquals(bearerData.messageId, 1); + assertEquals(bearerData.priority, BearerData.PRIORITY_EMERGENCY); + assertEquals(bearerData.privacy, BearerData.PRIVACY_CONFIDENTIAL); + assertEquals(bearerData.userAckReq, true); + assertEquals(bearerData.readAckReq, true); + assertEquals(bearerData.deliveryAckReq, true); + assertEquals(bearerData.reportReq, false); + assertEquals(bearerData.numberOfMessages, 3); + assertEquals(bearerData.alert, BearerData.ALERT_HIGH_PRIO); + assertEquals(bearerData.language, BearerData.LANGUAGE_HEBREW); + assertEquals(bearerData.callbackNumber.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(bearerData.callbackNumber.numberMode, + CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(bearerData.callbackNumber.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(bearerData.callbackNumber.numberPlan, CdmaSmsAddress.NUMBERING_PLAN_UNKNOWN); + assertEquals(bearerData.callbackNumber.numberOfDigits, 7); + assertEquals(bearerData.callbackNumber.address, "3598271"); + assertEquals(bearerData.displayMode, BearerData.DISPLAY_MODE_USER); + assertEquals(bearerData.depositIndex, 1382); + assertEquals(bearerData.userResponseCode, 5); + assertEquals(bearerData.msgCenterTimeStamp.year, 2008); + assertEquals(bearerData.msgCenterTimeStamp.month, 11); + assertEquals(bearerData.msgCenterTimeStamp.monthDay, 1); + assertEquals(bearerData.msgCenterTimeStamp.hour, 11); + assertEquals(bearerData.msgCenterTimeStamp.minute, 1); + assertEquals(bearerData.msgCenterTimeStamp.second, 59); + assertEquals(bearerData.validityPeriodAbsolute, null); + assertEquals(bearerData.validityPeriodRelative, 193); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.year, 1997); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.month, 5); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.monthDay, 18); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.hour, 0); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.minute, 0); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.second, 0); + assertEquals(bearerData.deferredDeliveryTimeRelative, 199); + assertEquals(bearerData.hasUserDataHeader, false); + assertEquals(bearerData.userData.msgEncoding, UserData.ENCODING_7BIT_ASCII); + assertEquals(bearerData.userData.numFields, 2); + assertEquals(bearerData.userData.payloadStr, "hi"); + } + + @SmallTest + public void testMonolithicTwo() throws Exception { + String pdu = "0003200010010410168d200201050306081201110159050192060697061800000007013d0" + + "801c00901800a01e00b01030c01c00d01070e05039acc13880f018011020566"; + BearerData bearerData = BearerData.decode(HexDump.hexStringToByteArray(pdu)); + assertEquals(bearerData.messageType, BearerData.MESSAGE_TYPE_SUBMIT); + assertEquals(bearerData.messageId, 1); + assertEquals(bearerData.priority, BearerData.PRIORITY_EMERGENCY); + assertEquals(bearerData.privacy, BearerData.PRIVACY_CONFIDENTIAL); + assertEquals(bearerData.userAckReq, true); + assertEquals(bearerData.readAckReq, true); + assertEquals(bearerData.deliveryAckReq, true); + assertEquals(bearerData.reportReq, false); + assertEquals(bearerData.numberOfMessages, 3); + assertEquals(bearerData.alert, BearerData.ALERT_HIGH_PRIO); + assertEquals(bearerData.language, BearerData.LANGUAGE_HEBREW); + assertEquals(bearerData.callbackNumber.digitMode, CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF); + assertEquals(bearerData.callbackNumber.numberMode, + CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK); + assertEquals(bearerData.callbackNumber.ton, CdmaSmsAddress.TON_UNKNOWN); + assertEquals(bearerData.callbackNumber.numberPlan, CdmaSmsAddress.NUMBERING_PLAN_UNKNOWN); + assertEquals(bearerData.callbackNumber.numberOfDigits, 7); + assertEquals(bearerData.callbackNumber.address, "3598271"); + assertEquals(bearerData.displayMode, BearerData.DISPLAY_MODE_USER); + assertEquals(bearerData.depositIndex, 1382); + assertEquals(bearerData.userResponseCode, 5); + assertEquals(bearerData.msgCenterTimeStamp.year, 2008); + assertEquals(bearerData.msgCenterTimeStamp.month, 11); + assertEquals(bearerData.msgCenterTimeStamp.monthDay, 1); + assertEquals(bearerData.msgCenterTimeStamp.hour, 11); + assertEquals(bearerData.msgCenterTimeStamp.minute, 1); + assertEquals(bearerData.msgCenterTimeStamp.second, 59); + assertEquals(bearerData.validityPeriodAbsolute, null); + assertEquals(bearerData.validityPeriodRelative, 61); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.year, 1997); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.month, 5); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.monthDay, 18); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.hour, 0); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.minute, 0); + assertEquals(bearerData.deferredDeliveryTimeAbsolute.second, 0); + assertEquals(bearerData.deferredDeliveryTimeRelative, 146); + assertEquals(bearerData.hasUserDataHeader, false); + assertEquals(bearerData.userData.msgEncoding, UserData.ENCODING_7BIT_ASCII); + assertEquals(bearerData.userData.numFields, 2); + assertEquals(bearerData.userData.payloadStr, "hi"); + } + + @SmallTest + public void testUserDataHeaderConcatRefFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 55; + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0xEE; + concatRef.msgCount = 2; + concatRef.seqNumber = 2; + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + byte[] encodedHeader = SmsHeader.toByteArray(smsHeader); + SmsHeader decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef.refNumber, concatRef.refNumber); + assertEquals(decodedHeader.concatRef.msgCount, concatRef.msgCount); + assertEquals(decodedHeader.concatRef.seqNumber, concatRef.seqNumber); + assertEquals(decodedHeader.concatRef.isEightBits, concatRef.isEightBits); + assertEquals(decodedHeader.portAddrs, null); + UserData userData = new UserData(); + userData.payloadStr = "User Data Header (UDH) feedback test"; + userData.userDataHeader = smsHeader; + bearerData.userData = userData; + byte[] encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + decodedHeader = revBearerData.userData.userDataHeader; + assertEquals(decodedHeader.concatRef.refNumber, concatRef.refNumber); + assertEquals(decodedHeader.concatRef.msgCount, concatRef.msgCount); + assertEquals(decodedHeader.concatRef.seqNumber, concatRef.seqNumber); + assertEquals(decodedHeader.concatRef.isEightBits, concatRef.isEightBits); + assertEquals(decodedHeader.portAddrs, null); + } + + @SmallTest + public void testUserDataHeaderIllegalConcatRef() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 55; + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0x10; + concatRef.msgCount = 0; + concatRef.seqNumber = 2; + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + byte[] encodedHeader = SmsHeader.toByteArray(smsHeader); + SmsHeader decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef, null); + concatRef.isEightBits = false; + encodedHeader = SmsHeader.toByteArray(smsHeader); + decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef, null); + concatRef.msgCount = 1; + concatRef.seqNumber = 2; + encodedHeader = SmsHeader.toByteArray(smsHeader); + decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef, null); + concatRef.msgCount = 1; + concatRef.seqNumber = 0; + encodedHeader = SmsHeader.toByteArray(smsHeader); + decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef, null); + concatRef.msgCount = 2; + concatRef.seqNumber = 1; + encodedHeader = SmsHeader.toByteArray(smsHeader); + decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef.msgCount, 2); + assertEquals(decodedHeader.concatRef.seqNumber, 1); + } + + @SmallTest + public void testUserDataHeaderMixedFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 42; + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0x34; + concatRef.msgCount = 5; + concatRef.seqNumber = 2; + concatRef.isEightBits = false; + SmsHeader.PortAddrs portAddrs = new SmsHeader.PortAddrs(); + portAddrs.destPort = 88; + portAddrs.origPort = 66; + portAddrs.areEightBits = false; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + smsHeader.portAddrs = portAddrs; + byte[] encodedHeader = SmsHeader.toByteArray(smsHeader); + SmsHeader decodedHeader = SmsHeader.fromByteArray(encodedHeader); + assertEquals(decodedHeader.concatRef.refNumber, concatRef.refNumber); + assertEquals(decodedHeader.concatRef.msgCount, concatRef.msgCount); + assertEquals(decodedHeader.concatRef.seqNumber, concatRef.seqNumber); + assertEquals(decodedHeader.concatRef.isEightBits, concatRef.isEightBits); + assertEquals(decodedHeader.portAddrs.destPort, portAddrs.destPort); + assertEquals(decodedHeader.portAddrs.origPort, portAddrs.origPort); + assertEquals(decodedHeader.portAddrs.areEightBits, portAddrs.areEightBits); + UserData userData = new UserData(); + userData.payloadStr = "User Data Header (UDH) feedback test"; + userData.userDataHeader = smsHeader; + bearerData.userData = userData; + byte[] encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + decodedHeader = revBearerData.userData.userDataHeader; + assertEquals(decodedHeader.concatRef.refNumber, concatRef.refNumber); + assertEquals(decodedHeader.concatRef.msgCount, concatRef.msgCount); + assertEquals(decodedHeader.concatRef.seqNumber, concatRef.seqNumber); + assertEquals(decodedHeader.concatRef.isEightBits, concatRef.isEightBits); + assertEquals(decodedHeader.portAddrs.destPort, portAddrs.destPort); + assertEquals(decodedHeader.portAddrs.origPort, portAddrs.origPort); + assertEquals(decodedHeader.portAddrs.areEightBits, portAddrs.areEightBits); + } + + @SmallTest + public void testReplyOption() throws Exception { + String pdu1 = "0003104090011648b6a794e0705476bf77bceae934fe5f6d94d87450080a0180"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals("Test Acknowledgement 1", bd1.userData.payloadStr); + assertEquals(true, bd1.userAckReq); + assertEquals(false, bd1.deliveryAckReq); + assertEquals(false, bd1.readAckReq); + assertEquals(false, bd1.reportReq); + String pdu2 = "0003104090011648b6a794e0705476bf77bceae934fe5f6d94d87490080a0140"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals("Test Acknowledgement 2", bd2.userData.payloadStr); + assertEquals(false, bd2.userAckReq); + assertEquals(true, bd2.deliveryAckReq); + assertEquals(false, bd2.readAckReq); + assertEquals(false, bd2.reportReq); + String pdu3 = "0003104090011648b6a794e0705476bf77bceae934fe5f6d94d874d0080a0120"; + BearerData bd3 = BearerData.decode(HexDump.hexStringToByteArray(pdu3)); + assertEquals("Test Acknowledgement 3", bd3.userData.payloadStr); + assertEquals(false, bd3.userAckReq); + assertEquals(false, bd3.deliveryAckReq); + assertEquals(true, bd3.readAckReq); + assertEquals(false, bd3.reportReq); + String pdu4 = "0003104090011648b6a794e0705476bf77bceae934fe5f6d94d87510080a0110"; + BearerData bd4 = BearerData.decode(HexDump.hexStringToByteArray(pdu4)); + assertEquals("Test Acknowledgement 4", bd4.userData.payloadStr); + assertEquals(false, bd4.userAckReq); + assertEquals(false, bd4.deliveryAckReq); + assertEquals(false, bd4.readAckReq); + assertEquals(true, bd4.reportReq); + } + + @SmallTest + public void testReplyOptionFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test reply option"; + bearerData.userData = userData; + bearerData.userAckReq = true; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(true, revBearerData.userAckReq); + assertEquals(false, revBearerData.deliveryAckReq); + assertEquals(false, revBearerData.readAckReq); + assertEquals(false, revBearerData.reportReq); + bearerData.userAckReq = false; + bearerData.deliveryAckReq = true; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(false, revBearerData.userAckReq); + assertEquals(true, revBearerData.deliveryAckReq); + assertEquals(false, revBearerData.readAckReq); + assertEquals(false, revBearerData.reportReq); + bearerData.deliveryAckReq = false; + bearerData.readAckReq = true; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(false, revBearerData.userAckReq); + assertEquals(false, revBearerData.deliveryAckReq); + assertEquals(true, revBearerData.readAckReq); + assertEquals(false, revBearerData.reportReq); + bearerData.readAckReq = false; + bearerData.reportReq = true; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(false, revBearerData.userAckReq); + assertEquals(false, revBearerData.deliveryAckReq); + assertEquals(false, revBearerData.readAckReq); + assertEquals(true, revBearerData.reportReq); + } + + @SmallTest + public void testNumberOfMessages() throws Exception { + // Note that the message text below does not properly reflect + // the message count. The author of these messages was + // apparently unaware that the values are bcd encoded, and the + // values being tested against (not the ones in the message + // text) are actually correct. + String pdu1 = "000310409001124896a794e07595f69f199540ea759a0dc8e00b0163"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals("Test Voice mail 99", bd1.userData.payloadStr); + assertEquals(63, bd1.numberOfMessages); + String pdu2 = "00031040900113489ea794e07595f69f199540ea759a0988c0600b0164"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals("Test Voice mail 100", bd2.userData.payloadStr); + assertEquals(64, bd2.numberOfMessages); + } + + @SmallTest + public void testCallbackNum() throws Exception { + String pdu1 = "00031040900112488ea794e070d436cb638bc5e035ce2f97900e06910431323334"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals("Test Callback nbr", bd1.userData.payloadStr); + assertEquals(CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR, bd1.callbackNumber.digitMode); + assertEquals(CdmaSmsAddress.TON_INTERNATIONAL_OR_IP, bd1.callbackNumber.ton); + assertEquals(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK, bd1.callbackNumber.numberMode); + assertEquals(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY, bd1.callbackNumber.numberPlan); + assertEquals("1234", bd1.callbackNumber.address); + } + + @SmallTest + public void testCallbackNumDtmf() throws Exception { + String pdu1 = "00031002300109104539b4d052ebb3d00e07052d4c90a55080"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals("SMS Rulz", bd1.userData.payloadStr); + assertEquals(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF, bd1.callbackNumber.digitMode); + assertEquals(CdmaSmsAddress.TON_UNKNOWN, bd1.callbackNumber.ton); + assertEquals(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK, bd1.callbackNumber.numberMode); + assertEquals(CdmaSmsAddress.NUMBERING_PLAN_UNKNOWN, bd1.callbackNumber.numberPlan); + assertEquals("5099214001", bd1.callbackNumber.address); + } + + @SmallTest + public void testCallbackNumFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test callback number"; + bearerData.userData = userData; + CdmaSmsAddress addr = new CdmaSmsAddress(); + addr.digitMode = CdmaSmsAddress.DIGIT_MODE_8BIT_CHAR; + addr.ton = CdmaSmsAddress.TON_NATIONAL_OR_EMAIL; + addr.numberMode = CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK; + addr.numberPlan = CdmaSmsAddress.NUMBERING_PLAN_UNKNOWN; + addr.address = "8005551212"; + addr.numberOfDigits = (byte)addr.address.length(); + bearerData.callbackNumber = addr; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + CdmaSmsAddress revAddr = revBearerData.callbackNumber; + assertEquals(addr.digitMode, revAddr.digitMode); + assertEquals(addr.ton, revAddr.ton); + assertEquals(addr.numberMode, revAddr.numberMode); + assertEquals(addr.numberPlan, revAddr.numberPlan); + assertEquals(addr.numberOfDigits, revAddr.numberOfDigits); + assertEquals(addr.address, revAddr.address); + addr.address = "8*55#1012"; + addr.numberOfDigits = (byte)addr.address.length(); + addr.digitMode = CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + revAddr = revBearerData.callbackNumber; + assertEquals(addr.digitMode, revAddr.digitMode); + assertEquals(addr.numberOfDigits, revAddr.numberOfDigits); + assertEquals(addr.address, revAddr.address); + } + + @SmallTest + public void testPrivacyIndicator() throws Exception { + String pdu1 = "0003104090010c485f4194dfea34becf61b840090140"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.privacy, BearerData.PRIVACY_RESTRICTED); + String pdu2 = "0003104090010c485f4194dfea34becf61b840090180"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals(bd2.privacy, BearerData.PRIVACY_CONFIDENTIAL); + String pdu3 = "0003104090010c485f4194dfea34becf61b8400901c0"; + BearerData bd3 = BearerData.decode(HexDump.hexStringToByteArray(pdu3)); + assertEquals(bd3.privacy, BearerData.PRIVACY_SECRET); + } + + @SmallTest + public void testPrivacyIndicatorFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test privacy indicator"; + bearerData.userData = userData; + bearerData.privacy = BearerData.PRIVACY_SECRET; + bearerData.privacyIndicatorSet = true; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.privacyIndicatorSet, true); + assertEquals(revBearerData.privacy, BearerData.PRIVACY_SECRET); + bearerData.privacy = BearerData.PRIVACY_RESTRICTED; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.privacy, BearerData.PRIVACY_RESTRICTED); + } + + @SmallTest + public void testMsgDeliveryAlert() throws Exception { + String pdu1 = "0003104090010d4866a794e07055965b91d040300c0100"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.alert, 0); + assertEquals(bd1.userData.payloadStr, "Test Alert 0"); + String pdu2 = "0003104090010d4866a794e07055965b91d140300c0140"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals(bd2.alert, 1); + assertEquals(bd2.userData.payloadStr, "Test Alert 1"); + String pdu3 = "0003104090010d4866a794e07055965b91d240300c0180"; + BearerData bd3 = BearerData.decode(HexDump.hexStringToByteArray(pdu3)); + assertEquals(bd3.alert, 2); + assertEquals(bd3.userData.payloadStr, "Test Alert 2"); + String pdu4 = "0003104090010d4866a794e07055965b91d340300c01c0"; + BearerData bd4 = BearerData.decode(HexDump.hexStringToByteArray(pdu4)); + assertEquals(bd4.alert, 3); + assertEquals(bd4.userData.payloadStr, "Test Alert 3"); + String pdu5 = "00031000000126114F4CBCFA20DB979F3C39F2A0C9976" + + "69ED979794187665E5D1028EFA7A6840E1062D3D39A900C028000"; + BearerData bd5 = BearerData.decode(HexDump.hexStringToByteArray(pdu5)); + assertEquals(bd5.alert, BearerData.ALERT_MEDIUM_PRIO); + assertEquals(bd5.userData.payloadStr, "test message delivery alert (with 8 bits)"); + String pdu6 = "00031000000126114F4CBCFA20DB979F3C39F2A0C9976" + + "69ED979794187665E5D1028EFA7A6840C1062D3D39A900C00"; + BearerData bd6 = BearerData.decode(HexDump.hexStringToByteArray(pdu6)); + assertEquals(bd6.userData.payloadStr, "test message delivery alert (with 0 bits)"); + assertEquals(bd6.alertIndicatorSet, false); + } + + @SmallTest + public void testMiscParams() throws Exception { + String pdu1 = "00031002400109104539b4d052ebb3d00c0180"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.alert, BearerData.ALERT_MEDIUM_PRIO); + assertEquals(bd1.userData.payloadStr, "SMS Rulz"); + String pdu2 = "00031002500109104539b4d052ebb3d00801800901c0"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals(bd2.priority, BearerData.PRIORITY_URGENT); + assertEquals(bd2.privacy, BearerData.PRIVACY_SECRET); + assertEquals(bd2.userData.payloadStr, "SMS Rulz"); + String pdu3 = "00031002600109104539b4d052ebb3d00901400c01c0"; + BearerData bd3 = BearerData.decode(HexDump.hexStringToByteArray(pdu3)); + assertEquals(bd3.privacy, BearerData.PRIVACY_RESTRICTED); + assertEquals(bd3.alert, BearerData.ALERT_HIGH_PRIO); + assertEquals(bd3.userData.payloadStr, "SMS Rulz"); + String pdu4 = "00031002700109104539b4d052ebb3d00f0105"; + BearerData bd4 = BearerData.decode(HexDump.hexStringToByteArray(pdu4)); + assertEquals(bd4.displayMode, BearerData.DISPLAY_MODE_IMMEDIATE); + assertEquals(bd4.userData.payloadStr, "SMS Rulz"); + } + @SmallTest + public void testMsgDeliveryAlertFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test message delivery alert"; + bearerData.userData = userData; + bearerData.alert = BearerData.ALERT_MEDIUM_PRIO; + bearerData.alertIndicatorSet = true; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.alertIndicatorSet, true); + assertEquals(revBearerData.alert, bearerData.alert); + bearerData.alert = BearerData.ALERT_HIGH_PRIO; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.alertIndicatorSet, true); + assertEquals(revBearerData.alert, bearerData.alert); + } + + @SmallTest + public void testLanguageIndicator() throws Exception { + String pdu1 = "0003104090011748bea794e0731436ef3bd7c2e0352eef27a1c263fe58080d0101"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.userData.payloadStr, "Test Language indicator"); + assertEquals(bd1.language, BearerData.LANGUAGE_ENGLISH); + String pdu2 = "0003104090011748bea794e0731436ef3bd7c2e0352eef27a1c263fe58080d0106"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals(bd2.userData.payloadStr, "Test Language indicator"); + assertEquals(bd2.language, BearerData.LANGUAGE_CHINESE); + } + + @SmallTest + public void testLanguageIndicatorFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test language indicator"; + bearerData.userData = userData; + bearerData.language = BearerData.LANGUAGE_ENGLISH; + bearerData.languageIndicatorSet = true; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.languageIndicatorSet, true); + assertEquals(revBearerData.language, bearerData.language); + bearerData.language = BearerData.LANGUAGE_KOREAN; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.languageIndicatorSet, true); + assertEquals(revBearerData.language, bearerData.language); + } + + @SmallTest + public void testDisplayMode() throws Exception { + String pdu1 = "0003104090010c485f4194dfea34becf61b8400f0100"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.displayMode, BearerData.DISPLAY_MODE_IMMEDIATE); + String pdu2 = "0003104090010c485f4194dfea34becf61b8400f0140"; + BearerData bd2 = BearerData.decode(HexDump.hexStringToByteArray(pdu2)); + assertEquals(bd2.displayMode, BearerData.DISPLAY_MODE_DEFAULT); + String pdu3 = "0003104090010c485f4194dfea34becf61b8400f0180"; + BearerData bd3 = BearerData.decode(HexDump.hexStringToByteArray(pdu3)); + assertEquals(bd3.displayMode, BearerData.DISPLAY_MODE_USER); + } + + @SmallTest + public void testDisplayModeFeedback() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 0; + bearerData.hasUserDataHeader = false; + UserData userData = new UserData(); + userData.payloadStr = "test display mode"; + bearerData.userData = userData; + bearerData.displayMode = BearerData.DISPLAY_MODE_IMMEDIATE; + bearerData.displayModeSet = true; + byte []encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.displayModeSet, true); + assertEquals(revBearerData.displayMode, bearerData.displayMode); + bearerData.displayMode = BearerData.DISPLAY_MODE_USER; + encodedSms = BearerData.encode(bearerData); + revBearerData = BearerData.decode(encodedSms); + assertEquals(revBearerData.userData.payloadStr, userData.payloadStr); + assertEquals(revBearerData.displayModeSet, true); + assertEquals(revBearerData.displayMode, bearerData.displayMode); + } + + @SmallTest + public void testIs91() throws Exception { + String pdu1 = "000320001001070c2039acc13880"; + BearerData bd1 = BearerData.decode(HexDump.hexStringToByteArray(pdu1)); + assertEquals(bd1.callbackNumber.address, "3598271"); + String pdu4 = "000320001001080c283c314724b34e"; + BearerData bd4 = BearerData.decode(HexDump.hexStringToByteArray(pdu4)); + assertEquals(bd4.userData.payloadStr, "ABCDEFG"); + } + + @SmallTest + public void testUserDataHeaderWithEightCharMsg() throws Exception { + BearerData bearerData = new BearerData(); + bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER; + bearerData.messageId = 55; + SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); + concatRef.refNumber = 0xEE; + concatRef.msgCount = 2; + concatRef.seqNumber = 2; + concatRef.isEightBits = true; + SmsHeader smsHeader = new SmsHeader(); + smsHeader.concatRef = concatRef; + UserData userData = new UserData(); + userData.payloadStr = "01234567"; + userData.userDataHeader = smsHeader; + bearerData.userData = userData; + byte[] encodedSms = BearerData.encode(bearerData); + BearerData revBearerData = BearerData.decode(encodedSms); + assertEquals(userData.payloadStr, revBearerData.userData.payloadStr); + } + + @SmallTest + public void testFragmentText() throws Exception { + boolean isCdmaPhone = (TelephonyManager.getDefault().getPhoneType() == + TelephonyManager.PHONE_TYPE_CDMA); + // Valid 160 character ASCII text. + String text1 = "123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789["; + TextEncodingDetails ted = SmsMessage.calculateLength(text1, false); + assertEquals(ted.msgCount, 1); + assertEquals(ted.codeUnitCount, 160); + assertEquals(ted.codeUnitSize, 1); + if (isCdmaPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text1); + assertEquals(fragments.size(), 1); + } + + /* + This is not a valid test: we will never encode a single-segment + EMS message. Leaving this here, since we may try to support + this in the future. + + // Valid 160 character GSM text -- the last character is + // non-ASCII, and so this will currently generate a singleton + // EMS message, which is not necessarily supported by Verizon. + String text2 = "123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789\u00a3"; // Trailing pound-currency sign. + ted = SmsMessage.calculateLength(text2, false); + assertEquals(ted.msgCount, 1); + assertEquals(ted.codeUnitCount, 160); + assertEquals(ted.codeUnitSize, 1); + if (isCdmaPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text2); + assertEquals(fragments.size(), 1); + } + */ + + // *IF* we supported single-segment EMS, this text would result in a + // single fragment with 7-bit encoding. But we don't, so this text + // results in three fragments of 16-bit encoding. + String text2 = "123456789012345678901234567890123456789012345678901234567890" + + "1234567890123456789012345678901234567890123456789012345678901234567890" + + "12345678901234567890123456789\u00a3"; // Trailing pound-currency sign. + ted = SmsMessage.calculateLength(text2, false); + assertEquals(3, ted.msgCount); + assertEquals(160, ted.codeUnitCount); + assertEquals(3, ted.codeUnitSize); + if (isCdmaPhone) { + ArrayList<String> fragments = android.telephony.SmsMessage.fragmentText(text2); + assertEquals(3, fragments.size()); + } + + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMPhoneTest.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMPhoneTest.java new file mode 100644 index 0000000..699f113 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMPhoneTest.java @@ -0,0 +1,1933 @@ +/* + * 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 GSMTestHandler.ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.telephony.gsm; + +import android.os.AsyncResult; +import android.os.Handler; +import android.os.Message; +import android.telephony.ServiceState; +import android.test.AndroidTestCase; +import android.test.PerformanceTestCase; + +import com.android.internal.telephony.Call; +import com.android.internal.telephony.CallStateException; +import com.android.internal.telephony.Connection; +import com.android.internal.telephony.MmiCode; +import com.android.internal.telephony.Phone; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.telephony.gsm.CallFailCause; +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.gsm.GSMTestHandler; +import com.android.internal.telephony.gsm.GsmMmiCode; +import com.android.internal.telephony.gsm.SuppServiceNotification; +import com.android.internal.telephony.test.SimulatedRadioControl; + +import java.util.List; + + +public class GSMPhoneTest extends AndroidTestCase implements PerformanceTestCase { + private SimulatedRadioControl mRadioControl; + private GSMPhone mGSMPhone; + private GSMTestHandler mGSMTestHandler; + private Handler mHandler; + + private static final int EVENT_PHONE_STATE_CHANGED = 1; + private static final int EVENT_DISCONNECT = 2; + private static final int EVENT_RINGING = 3; + private static final int EVENT_CHANNEL_OPENED = 4; + private static final int EVENT_POST_DIAL = 5; + private static final int EVENT_DONE = 6; + private static final int EVENT_SSN = 7; + private static final int EVENT_MMI_INITIATE = 8; + private static final int EVENT_MMI_COMPLETE = 9; + private static final int EVENT_IN_SERVICE = 10; + private static final int SUPP_SERVICE_FAILED = 11; + private static final int SERVICE_STATE_CHANGED = 12; + private static final int EVENT_OEM_RIL_MESSAGE = 13; + public static final int ANY_MESSAGE = -1; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mGSMTestHandler = new GSMTestHandler(mContext); + + mGSMTestHandler.start(); + synchronized (mGSMTestHandler) { + do { + mGSMTestHandler.wait(); + } while (mGSMTestHandler.getGSMPhone() == null); + } + + mGSMPhone = mGSMTestHandler.getGSMPhone(); + mRadioControl = mGSMTestHandler.getSimulatedCommands(); + + mHandler = mGSMTestHandler.getHandler(); + mGSMPhone.registerForPreciseCallStateChanged(mHandler, EVENT_PHONE_STATE_CHANGED, null); + mGSMPhone.registerForNewRingingConnection(mHandler, EVENT_RINGING, null); + mGSMPhone.registerForDisconnect(mHandler, EVENT_DISCONNECT, null); + + mGSMPhone.setOnPostDialCharacter(mHandler, EVENT_POST_DIAL, null); + + mGSMPhone.registerForSuppServiceNotification(mHandler, EVENT_SSN, null); + mGSMPhone.registerForMmiInitiate(mHandler, EVENT_MMI_INITIATE, null); + mGSMPhone.registerForMmiComplete(mHandler, EVENT_MMI_COMPLETE, null); + mGSMPhone.registerForSuppServiceFailed(mHandler, SUPP_SERVICE_FAILED, null); + + mGSMPhone.registerForServiceStateChanged(mHandler, SERVICE_STATE_CHANGED, null); + + // wait until we get phone in both voice and data service + Message msg; + ServiceState state; + + do { + msg = mGSMTestHandler.waitForMessage(SERVICE_STATE_CHANGED); + assertNotNull("Message Time Out", msg); + state = (ServiceState) ((AsyncResult) msg.obj).result; + } while (state.getState() != ServiceState.STATE_IN_SERVICE); + } + + @Override + protected void tearDown() throws Exception { + mRadioControl.shutdown(); + + mGSMPhone.unregisterForPreciseCallStateChanged(mHandler); + mGSMPhone.unregisterForNewRingingConnection(mHandler); + mGSMPhone.unregisterForDisconnect(mHandler); + mGSMPhone.setOnPostDialCharacter(mHandler, 0, null); + mGSMPhone.unregisterForSuppServiceNotification(mHandler); + mGSMPhone.unregisterForMmiInitiate(mHandler); + mGSMPhone.unregisterForMmiComplete(mHandler); + + mGSMPhone = null; + mRadioControl = null; + mHandler = null; + mGSMTestHandler.cleanup(); + + super.tearDown(); + } + + // These test can only be run once. + public int startPerformance(Intermediates intermediates) { + return 1; + } + + public boolean isPerformanceOnly() { + return false; + } + + + //This test is causing the emulator screen to turn off. I don't understand + //why, but I'm removing it until we can figure it out. + public void brokenTestGeneral() throws Exception { + Connection cn; + Message msg; + AsyncResult ar; + + // IDLE state + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + assertFalse(mGSMPhone.canConference()); + + // One DIALING connection + + mRadioControl.setAutoProgressConnectingCall(false); + + mGSMPhone.dial("+13125551212"); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + + msg = mGSMTestHandler.waitForMessage(EVENT_PHONE_STATE_CHANGED); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertEquals(Call.State.DIALING, mGSMPhone.getForegroundCall().getState()); + assertTrue(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + /*do { + mGSMTestHandler.waitForMessage(ANY_MESSAGE); + } while (mGSMPhone.getForegroundCall().getConnections().size() == 0);*/ + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DIALING, + mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + cn = mGSMPhone.getForegroundCall().getConnections().get(0); + assertTrue(!cn.isIncoming()); + assertEquals(Connection.PostDialState.NOT_STARTED, cn.getPostDialState()); + + assertEquals(Connection.DisconnectCause.NOT_DISCONNECTED, cn.getDisconnectCause()); + + assertFalse(mGSMPhone.canConference()); + + // One ALERTING connection + + mRadioControl.progressConnectingCallState(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } + while (mGSMPhone.getForegroundCall().getState() != Call.State.ALERTING); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertTrue(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ALERTING, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + cn = mGSMPhone.getForegroundCall().getConnections().get(0); + assertTrue(!cn.isIncoming()); + assertEquals(Connection.PostDialState.NOT_STARTED, cn.getPostDialState()); + assertFalse(mGSMPhone.canConference()); + + // One ACTIVE connection + + mRadioControl.progressConnectingCallState(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertTrue(mGSMPhone.getForegroundCall().getEarliestConnectTime() > 0); + + cn = mGSMPhone.getForegroundCall().getConnections().get(0); + assertTrue(!cn.isIncoming()); + assertEquals(Connection.PostDialState.COMPLETE, cn.getPostDialState()); + assertFalse(mGSMPhone.canConference()); + + // One disconnected connection + mGSMPhone.getForegroundCall().hangup(); + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertTrue(mGSMPhone.getForegroundCall().getEarliestConnectTime() > 0); + + assertFalse(mGSMPhone.canConference()); + + cn = mGSMPhone.getForegroundCall().getEarliestConnection(); + + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + // Back to idle state + + mGSMPhone.clearDisconnected(); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + assertFalse(mGSMPhone.canConference()); + + // cn left over from before phone.clearDisconnected(); + + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + // One ringing (INCOMING) call + + mRadioControl.triggerRing("18005551212"); + + msg = mGSMTestHandler.waitForMessage(EVENT_RINGING); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + ar = (AsyncResult) msg.obj; + cn = (Connection) ar.result; + assertTrue(cn.isRinging()); + assertEquals(mGSMPhone.getRingingCall(), cn.getCall()); + + assertEquals(1, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.INCOMING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getRingingCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getRingingCall().getEarliestConnectTime()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + cn = mGSMPhone.getRingingCall().getConnections().get(0); + assertTrue(cn.isIncoming()); + assertEquals(Connection.PostDialState.NOT_STARTED, cn.getPostDialState()); + + assertFalse(mGSMPhone.canConference()); + + // One mobile terminated active call + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getConnections().size() == 1); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, + mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertTrue(mGSMPhone.getForegroundCall().getEarliestConnectTime() > 0); + + cn = mGSMPhone.getForegroundCall().getConnections().get(0); + assertTrue(cn.isIncoming()); + assertEquals(Connection.PostDialState.NOT_STARTED, cn.getPostDialState()); + + assertFalse(mGSMPhone.canConference()); + + // One disconnected (local hangup) call + + try { + Connection conn; + conn = mGSMPhone.getForegroundCall().getConnections().get(0); + conn.hangup(); + } catch (CallStateException ex) { + ex.printStackTrace(); + fail("unexpected ex"); + } + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, + mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertTrue(mGSMPhone.getForegroundCall().getEarliestConnectTime() > 0); + + cn = mGSMPhone.getForegroundCall().getEarliestConnection(); + + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + assertEquals(Connection.DisconnectCause.LOCAL, cn.getDisconnectCause()); + + assertFalse(mGSMPhone.canConference()); + + // Back to idle state + + mGSMPhone.clearDisconnected(); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(Connection.DisconnectCause.LOCAL, cn.getDisconnectCause()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + assertFalse(mGSMPhone.canConference()); + + // cn left over from before phone.clearDisconnected(); + + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + // One ringing call + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getConnections().isEmpty()); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(1, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.INCOMING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getRingingCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getRingingCall().getEarliestConnectTime()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + assertFalse(mGSMPhone.canConference()); + + // One rejected call + mGSMPhone.rejectCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.IDLE); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(1, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getRingingCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getRingingCall().getEarliestConnectTime()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + cn = mGSMPhone.getRingingCall().getEarliestConnection(); + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + assertEquals(Connection.DisconnectCause.INCOMING_MISSED, cn.getDisconnectCause()); + + assertFalse(mGSMPhone.canConference()); + + // Back to idle state + + mGSMPhone.clearDisconnected(); + + assertEquals(Connection.DisconnectCause.INCOMING_MISSED, cn.getDisconnectCause()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(0, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestCreateTime()); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + assertFalse(mGSMPhone.canConference()); + assertEquals(Call.State.DISCONNECTED, cn.getState()); + + // One ringing call + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getConnections().isEmpty()); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + cn = mGSMPhone.getRingingCall().getEarliestConnection(); + + // Ringing call disconnects + + mRadioControl.triggerHangupForeground(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.IDLE); + + assertEquals(Connection.DisconnectCause.INCOMING_MISSED, cn.getDisconnectCause()); + + // One Ringing Call + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.RINGING); + + + cn = mGSMPhone.getRingingCall().getEarliestConnection(); + + // One answered call + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // one holding call + mGSMPhone.switchHoldingAndActive(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() == Call.State.IDLE); + + + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // one active call + mGSMPhone.switchHoldingAndActive(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } + while (mGSMPhone.getBackgroundCall().getState() == Call.State.HOLDING); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // One disconnected call in the foreground slot + + mRadioControl.triggerHangupAll(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.IDLE); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Connection.DisconnectCause.NORMAL, cn.getDisconnectCause()); + + // Test missed calls + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.RINGING); + + mGSMPhone.rejectCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (msg.what != EVENT_DISCONNECT); + + ar = (AsyncResult) msg.obj; + cn = (Connection) ar.result; + + assertEquals(Connection.DisconnectCause.INCOMING_MISSED, cn.getDisconnectCause()); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getRingingCall().getState()); + + // Test incoming not missed calls + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.RINGING); + + cn = mGSMPhone.getRingingCall().getEarliestConnection(); + + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + assertEquals(Connection.DisconnectCause.NOT_DISCONNECTED, cn.getDisconnectCause()); + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + + try { + mGSMPhone.getForegroundCall().hangup(); + } catch (CallStateException ex) { + ex.printStackTrace(); + fail("unexpected ex"); + } + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() + != Call.State.DISCONNECTED); + + assertEquals(Connection.DisconnectCause.LOCAL, cn.getDisconnectCause()); + + // + // Test held and hangup held calls + // + + // One ALERTING call + mGSMPhone.dial("+13125551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + assertTrue(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + mRadioControl.progressConnectingCallState(); + mRadioControl.progressConnectingCallState(); + + // One ACTIVE call + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + // One ACTIVE call, one ringing call + + mRadioControl.triggerRing("18005551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.RINGING); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + // One HOLDING call, one ACTIVE call + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertTrue(mGSMPhone.canConference()); + + // Conference the two + mGSMPhone.conference(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertTrue(mGSMPhone.getForegroundCall().isMultiparty()); + assertFalse(mGSMPhone.canConference()); + + // Hold the multiparty call + mGSMPhone.switchHoldingAndActive(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } + while (mGSMPhone.getBackgroundCall().getState() != Call.State.HOLDING); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertTrue(mGSMPhone.getBackgroundCall().isMultiparty()); + assertFalse(mGSMPhone.canConference()); + + // Multiparty call on hold, call waiting added + + mRadioControl.triggerRing("18005558355"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.RINGING); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertTrue(mGSMPhone.getBackgroundCall().isMultiparty()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertFalse(mGSMPhone.canConference()); + + // Hangup conference call, ringing call still around + mGSMPhone.getBackgroundCall().hangup(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.DISCONNECTED); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getBackgroundCall().getState()); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + + // Reject waiting call + mGSMPhone.rejectCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.IDLE); + + assertFalse(mGSMPhone.getForegroundCall().isDialingOrAlerting()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + } + + public void testOutgoingCallFailImmediately() throws Exception { + Message msg; + + // Test outgoing call fail-immediately edge case + // This happens when a call terminated before ever appearing in a + // call list + // This should land the immediately-failing call in the + // ForegroundCall list as an IDLE call + mRadioControl.setNextDialFailImmediately(true); + + Connection cn = mGSMPhone.dial("+13125551212"); + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Connection.DisconnectCause.NORMAL, cn.getDisconnectCause()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + } + + public void testHangupOnOutgoing() throws Exception { + Connection cn; + Message msg; + + mRadioControl.setAutoProgressConnectingCall(false); + + // Test 1: local hangup in "DIALING" state + mGSMPhone.dial("+13125551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } + while (mGSMPhone.getForegroundCall().getState() != Call.State.DIALING); + + cn = mGSMPhone.getForegroundCall().getEarliestConnection(); + + mGSMPhone.getForegroundCall().hangup(); + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Connection.DisconnectCause.LOCAL, cn.getDisconnectCause()); + + // Test 2: local hangup in "ALERTING" state + mGSMPhone.dial("+13125551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + mRadioControl.progressConnectingCallState(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } + while (mGSMPhone.getForegroundCall().getState() != Call.State.ALERTING); + + cn = mGSMPhone.getForegroundCall().getEarliestConnection(); + + mGSMPhone.getForegroundCall().hangup(); + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Connection.DisconnectCause.LOCAL, cn.getDisconnectCause()); + + // Test 3: local immediate hangup before GSM index is + // assigned (CallTracker.hangupPendingMO case) + + mRadioControl.pauseResponses(); + + cn = mGSMPhone.dial("+13125551212"); + + cn.hangup(); + + mRadioControl.resumeResponses(); + + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + + assertEquals(Connection.DisconnectCause.LOCAL, + mGSMPhone.getForegroundCall().getEarliestConnection().getDisconnectCause()); + } + + public void testHangupOnChannelClose() throws Exception { + mGSMPhone.dial("+13125551212"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getConnections().isEmpty()); + + mRadioControl.shutdown(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + mGSMPhone.clearDisconnected(); + } while (!mGSMPhone.getForegroundCall().getConnections().isEmpty()); + } + + public void testIncallMmiCallDeflection() throws Exception { + Message msg; + + // establish an active call + mGSMPhone.dial("+13125551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // establish a ringing (WAITING) call + + mRadioControl.triggerRing("18005551212"); + + msg = mGSMTestHandler.waitForMessage(EVENT_RINGING); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering 0 followed by SEND: release all held calls + // or sets UDUB for a waiting call. + mGSMPhone.handleInCallMmiCommands("0"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getState() == Call.State.WAITING); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // change the active call to holding call + mGSMPhone.switchHoldingAndActive(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() == Call.State.IDLE); + + + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering 0 followed by SEND: release all held calls + // or sets UDUB for a waiting call. + mGSMPhone.handleInCallMmiCommands("0"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() == Call.State.HOLDING); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getBackgroundCall().getState()); + } + + public void testIncallMmiCallWaiting() throws Exception { + Message msg; + + // establish an active call + mGSMPhone.dial("+13125551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // establish a ringing (WAITING) call + + mRadioControl.triggerRing("18005551212"); + + do { + msg = mGSMTestHandler.waitForMessage(ANY_MESSAGE); + assertNotNull("Message Time Out", msg); + } while (msg.what != EVENT_RINGING); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering 1 followed by SEND: release all active calls + // (if any exist) and accepts the other (held or waiting) call. + + mGSMPhone.handleInCallMmiCommands("1"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getState() == Call.State.WAITING); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + + // change the active call to holding call + mGSMPhone.switchHoldingAndActive(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() == Call.State.IDLE); + + assertEquals(Call.State.IDLE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering 1 followed by SEND: release all active calls + // (if any exist) and accepts the other (held or waiting) call. + mGSMPhone.handleInCallMmiCommands("1"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + + // at this point, the active call with number==18005551212 should + // have the gsm index of 2 + + mRadioControl.triggerRing("16505550100"); + + msg = mGSMTestHandler.waitForMessage(EVENT_RINGING); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering "12" followed by SEND: release the call with + // gsm index equals to 2. + mGSMPhone.handleInCallMmiCommands("12"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() == Call.State.ACTIVE); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getState() != PhoneConstants.State.OFFHOOK); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertFalse(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // at this point, the call with number==16505550100 should + // have the gsm index of 1 + mGSMPhone.dial("+13125551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE || + mGSMPhone.getBackgroundCall().getState() != Call.State.HOLDING); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // at this point, the active call with number==13125551212 should + // have the gsm index of 2 + + // Simulate entering "11" followed by SEND: release the call with + // gsm index equals to 1. This should not be allowed, and a + // Supplementary Service notification must be received. + mGSMPhone.handleInCallMmiCommands("11"); + + msg = mGSMTestHandler.waitForMessage(SUPP_SERVICE_FAILED); + assertNotNull("Message Time Out", msg); + assertFalse("IncallMmiCallWaiting: command should not work on holding call", msg == null); + + // Simulate entering "12" followed by SEND: release the call with + // gsm index equals to 2. + mGSMPhone.handleInCallMmiCommands("12"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() == Call.State.ACTIVE); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // Simulate entering 1 followed by SEND: release all active calls + // (if any exist) and accepts the other (held or waiting) call. + mGSMPhone.handleInCallMmiCommands("1"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + assertEquals("16505550100", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + + // Simulate entering "11" followed by SEND: release the call with + // gsm index equals to 1. + mGSMPhone.handleInCallMmiCommands("11"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() == Call.State.ACTIVE); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + } + + public void testIncallMmiCallHold() throws Exception { + Message msg; + + // establish an active call + mGSMPhone.dial("13125551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // establish a ringing (WAITING) call + + mRadioControl.triggerRing("18005551212"); + + msg = mGSMTestHandler.waitForMessage(EVENT_RINGING); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // simulate entering 2 followed by SEND: place all active calls + // (if any exist) on hold and accepts the other (held or waiting) + // call + + mGSMPhone.handleInCallMmiCommands("2"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getState() == Call.State.WAITING); + + + assertFalse(mGSMPhone.getRingingCall().isRinging()); + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, + mGSMPhone.getForegroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertEquals("13125551212", + mGSMPhone.getBackgroundCall().getConnections().get(0).getAddress()); + + // swap the active and holding calls + mGSMPhone.handleInCallMmiCommands("2"); + + msg = mGSMTestHandler.waitForMessage(EVENT_PHONE_STATE_CHANGED); + assertNotNull("Message Time Out", msg); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("13125551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getBackgroundCall().getConnections().get(0).getAddress()); + + // merge the calls + mGSMPhone.conference(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + assertEquals(2, mGSMPhone.getForegroundCall().getConnections().size()); + + // at this point, we have an active conference call, with + // call(1) = 13125551212 and call(2) = 18005551212 + + // Simulate entering "23" followed by SEND: places all active call + // on hold except call 3. This should fail and a supplementary service + // failed notification should be received. + + mGSMPhone.handleInCallMmiCommands("23"); + + msg = mGSMTestHandler.waitForMessage(SUPP_SERVICE_FAILED); + assertNotNull("Message Time Out", msg); + assertFalse("IncallMmiCallHold: separate should have failed!", msg == null); + + // Simulate entering "21" followed by SEND: places all active call + // on hold except call 1. + mGSMPhone.handleInCallMmiCommands("21"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() == Call.State.IDLE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("13125551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getBackgroundCall().getConnections().get(0).getAddress()); + } + + public void testIncallMmiMultipartyServices() throws Exception { + // establish an active call + mGSMPhone.dial("13125551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + // dial another call + mGSMPhone.dial("18005551212"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + mGSMPhone.handleInCallMmiCommands("3"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertEquals(PhoneConstants.State.OFFHOOK, mGSMPhone.getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals("13125551212", + mGSMPhone.getForegroundCall().getConnections().get(1).getAddress()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + } + + public void testCallIndex() throws Exception { + Message msg; + + // establish the first call + mGSMPhone.dial("16505550100"); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + String baseNumber = "1650555010"; + + for (int i = 1; i < 6; i++) { + String number = baseNumber + i; + + mGSMPhone.dial(number); + + do { + mRadioControl.progressConnectingCallState(); + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + if (mGSMPhone.getBackgroundCall().getConnections().size() >= 5) { + break; + } + + mGSMPhone.conference(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getBackgroundCall().getState() != Call.State.IDLE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + } + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("16505550105", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // create an incoming call, this call should have the call index + // of 7 + mRadioControl.triggerRing("18005551212"); + + msg = mGSMTestHandler.waitForMessage(EVENT_RINGING); + assertNotNull("Message Time Out", msg); + + assertEquals(PhoneConstants.State.RINGING, mGSMPhone.getState()); + assertTrue(mGSMPhone.getRingingCall().isRinging()); + assertEquals(Call.State.WAITING, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + + // hangup the background call and accept the ringing call + mGSMPhone.getBackgroundCall().hangup(); + mGSMPhone.acceptCall(); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getRingingCall().getState() != Call.State.IDLE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals("18005551212", + mGSMPhone.getForegroundCall().getConnections().get(0).getAddress()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertEquals("16505550105", + mGSMPhone.getBackgroundCall().getConnections().get(0).getAddress()); + + mGSMPhone.handleInCallMmiCommands("17"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() == Call.State.ACTIVE); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.HOLDING, mGSMPhone.getBackgroundCall().getState()); + assertEquals("16505550105", + mGSMPhone.getBackgroundCall().getConnections().get(0). + getAddress()); + + mGSMPhone.handleInCallMmiCommands("1"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() != Call.State.ACTIVE); + + assertEquals(Call.State.ACTIVE, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + mGSMPhone.handleInCallMmiCommands("16"); + + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (mGSMPhone.getForegroundCall().getState() == Call.State.ACTIVE); + + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + } + + public void testPostDialSequences() throws Exception { + Message msg; + AsyncResult ar; + Connection cn; + + mGSMPhone.dial("+13125551212,1234;5N8xx"); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(',', msg.arg1); + assertEquals("1234;5N8", cn.getRemainingPostDialString()); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('1', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('2', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('3', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('4', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals(';', msg.arg1); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(Connection.PostDialState.WAIT, cn.getPostDialState()); + assertEquals(Connection.PostDialState.WAIT, ar.userObj); + cn.proceedAfterWaitChar(); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('5', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertEquals('N', msg.arg1); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(Connection.PostDialState.WILD, cn.getPostDialState()); + assertEquals(Connection.PostDialState.WILD, ar.userObj); + cn.proceedAfterWildChar(",6;7"); + + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(',', msg.arg1); + assertEquals("6;78", cn.getRemainingPostDialString()); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('6', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals(';', msg.arg1); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(Connection.PostDialState.WAIT, cn.getPostDialState()); + assertEquals(Connection.PostDialState.WAIT, ar.userObj); + cn.proceedAfterWaitChar(); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('7', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals('8', msg.arg1); + ar = (AsyncResult) (msg.obj); + assertEquals(Connection.PostDialState.STARTED, ar.userObj); + + // Bogus chars at end should be ignored + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals(0, msg.arg1); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(Connection.PostDialState.COMPLETE, + cn.getPostDialState()); + assertEquals(Connection.PostDialState.COMPLETE, ar.userObj); + } + + public void testPostDialCancel() throws Exception { + Message msg; + AsyncResult ar; + Connection cn; + + mGSMPhone.dial("+13125551212,N"); + mRadioControl.progressConnectingToActive(); + + mRadioControl.progressConnectingToActive(); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertNotNull("Message Time Out", msg); + assertEquals(',', msg.arg1); + + msg = mGSMTestHandler.waitForMessage(EVENT_POST_DIAL); + assertEquals('N', msg.arg1); + ar = (AsyncResult) (msg.obj); + cn = (Connection) (ar.result); + assertEquals(Connection.PostDialState.WILD, cn.getPostDialState()); + cn.cancelPostDial(); + + assertEquals(Connection.PostDialState.CANCELLED, cn.getPostDialState()); + } + + public void testOutgoingCallFail() throws Exception { + Message msg; + /* + * normal clearing + */ + + mRadioControl.setNextCallFailCause(CallFailCause.NORMAL_CLEARING); + mRadioControl.setAutoProgressConnectingCall(false); + + Connection cn = mGSMPhone.dial("+13125551212"); + + mRadioControl.progressConnectingCallState(); + + // I'm just progressing the call state to + // ensure getCurrentCalls() gets processed... + // Normally these failure conditions would happen in DIALING + // not ALERTING + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (cn.getState() == Call.State.DIALING); + + + mRadioControl.triggerHangupAll(); + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Connection.DisconnectCause.NORMAL, cn.getDisconnectCause()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + /* + * busy + */ + + mRadioControl.setNextCallFailCause(CallFailCause.USER_BUSY); + mRadioControl.setAutoProgressConnectingCall(false); + + cn = mGSMPhone.dial("+13125551212"); + + mRadioControl.progressConnectingCallState(); + + // I'm just progressing the call state to + // ensure getCurrentCalls() gets processed... + // Normally these failure conditions would happen in DIALING + // not ALERTING + do { + assertNotNull("Message Time Out", mGSMTestHandler.waitForMessage(ANY_MESSAGE)); + } while (cn.getState() == Call.State.DIALING); + + + mRadioControl.triggerHangupAll(); + msg = mGSMTestHandler.waitForMessage(EVENT_DISCONNECT); + assertNotNull("Message Time Out", msg); + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Connection.DisconnectCause.BUSY, cn.getDisconnectCause()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, + mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + + /* + * congestion + */ + + mRadioControl.setNextCallFailCause(CallFailCause.NO_CIRCUIT_AVAIL); + mRadioControl.setAutoProgressConnectingCall(false); + + cn = mGSMPhone.dial("+13125551212"); + + mRadioControl.progressConnectingCallState(); + + // I'm just progressing the call state to + // ensure getCurrentCalls() gets processed... + // Normally these failure conditions would happen in DIALING + // not ALERTING + do { + msg = mGSMTestHandler.waitForMessage(ANY_MESSAGE); + assertNotNull("Message Time Out", msg); + } while (cn.getState() == Call.State.DIALING); + + + mRadioControl.triggerHangupAll(); + + // Unlike the while loops above, this one waits + // for a "phone state changed" message back to "idle" + do { + msg = mGSMTestHandler.waitForMessage(ANY_MESSAGE); + assertNotNull("Message Time Out", msg); + } while (!(msg.what == EVENT_PHONE_STATE_CHANGED + && mGSMPhone.getState() == PhoneConstants.State.IDLE)); + + assertEquals(PhoneConstants.State.IDLE, mGSMPhone.getState()); + + assertEquals(Connection.DisconnectCause.CONGESTION, cn.getDisconnectCause()); + + assertEquals(0, mGSMPhone.getRingingCall().getConnections().size()); + assertEquals(1, mGSMPhone.getForegroundCall().getConnections().size()); + assertEquals(0, mGSMPhone.getBackgroundCall().getConnections().size()); + + assertEquals(Call.State.IDLE, mGSMPhone.getRingingCall().getState()); + assertEquals(Call.State.DISCONNECTED, mGSMPhone.getForegroundCall().getState()); + assertEquals(Call.State.IDLE, mGSMPhone.getBackgroundCall().getState()); + + assertTrue(mGSMPhone.getForegroundCall().getEarliestCreateTime() > 0); + assertEquals(0, mGSMPhone.getForegroundCall().getEarliestConnectTime()); + } + + public void testSSNotification() throws Exception { + // MO + runTest(0, SuppServiceNotification.MO_CODE_UNCONDITIONAL_CF_ACTIVE); + runTest(0, SuppServiceNotification.MO_CODE_CALL_IS_WAITING); + runTest(0, SuppServiceNotification.MO_CODE_CALL_DEFLECTED); + + // MT + runTest(1, SuppServiceNotification.MT_CODE_FORWARDED_CALL); + runTest(1, SuppServiceNotification.MT_CODE_CALL_CONNECTED_ECT); + runTest(1, SuppServiceNotification.MT_CODE_ADDITIONAL_CALL_FORWARDED); + } + + private void runTest(int type, int code) { + Message msg; + + mRadioControl.triggerSsn(type, code); + + msg = mGSMTestHandler.waitForMessage(EVENT_SSN); + assertNotNull("Message Time Out", msg); + AsyncResult ar = (AsyncResult) msg.obj; + + assertNull(ar.exception); + + SuppServiceNotification notification = + (SuppServiceNotification) ar.result; + + assertEquals(type, notification.notificationType); + assertEquals(code, notification.code); + } + + public void testUssd() throws Exception { + // Quick hack to work around a race condition in this test: + // We may initiate a USSD MMI before GSMPhone receives its initial + // GSMTestHandler.EVENT_RADIO_OFF_OR_NOT_AVAILABLE event. When the phone sees this + // event, it will cancel the just issued USSD MMI, which we don't + // want. So sleep a little first. + try { + Thread.sleep(1000); + } catch (InterruptedException ex) { + // do nothing + } + + verifyNormal(); + verifyCancel(); + varifyNetworkInitiated(); + } + + private void varifyNetworkInitiated() { + Message msg; + AsyncResult ar; + MmiCode mmi; + + // Receive an incoming NOTIFY + mRadioControl.triggerIncomingUssd("0", "NOTIFY message"); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertFalse(mmi.isUssdRequest()); + + // Receive a REQUEST and send response + mRadioControl.triggerIncomingUssd("1", "REQUEST Message"); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertTrue(mmi.isUssdRequest()); + + mGSMPhone.sendUssdResponse("## TEST: TEST_GSMPhone responding..."); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_INITIATE); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + GsmMmiCode gsmMmi = (GsmMmiCode) mmi; + assertTrue(gsmMmi.isPendingUSSD()); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertNull(ar.exception); + assertFalse(mmi.isUssdRequest()); + + // Receive a REQUEST and cancel + mRadioControl.triggerIncomingUssd("1", "REQUEST Message"); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertTrue(mmi.isUssdRequest()); + + mmi.cancel(); + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertNull(ar.exception); + assertEquals(MmiCode.State.CANCELLED, mmi.getState()); + + List mmiList = mGSMPhone.getPendingMmiCodes(); + assertEquals(0, mmiList.size()); + } + + private void verifyNormal() throws CallStateException { + Message msg; + AsyncResult ar; + MmiCode mmi; + + mGSMPhone.dial("#646#"); + + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_INITIATE); + assertNotNull("Message Time Out", msg); + + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + + ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + assertEquals(MmiCode.State.COMPLETE, mmi.getState()); + } + + + private void verifyCancel() throws CallStateException { + /** + * This case makes an assumption that dial() will add the USSD + * to the "pending MMI codes" list before it returns. This seems + * like reasonable semantics. It also assumes that the USSD + * request in question won't complete until we get back to the + * event loop, thus cancel() is safe. + */ + Message msg; + + mGSMPhone.dial("#646#"); + + List<? extends MmiCode> pendingMmis = mGSMPhone.getPendingMmiCodes(); + + assertEquals(1, pendingMmis.size()); + + MmiCode mmi = pendingMmis.get(0); + assertTrue(mmi.isCancelable()); + mmi.cancel(); + + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_INITIATE); + assertNotNull("Message Time Out", msg); + + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + + AsyncResult ar = (AsyncResult) msg.obj; + mmi = (MmiCode) ar.result; + + assertEquals(MmiCode.State.CANCELLED, mmi.getState()); + } + + public void testRilHooks() throws Exception { + // + // These test cases all assume the RIL OEM hooks + // just echo back their input + // + + Message msg; + AsyncResult ar; + + // null byte array + + mGSMPhone.invokeOemRilRequestRaw(null, mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertNull(ar.result); + assertNull(ar.exception); + + // empty byte array + + mGSMPhone.invokeOemRilRequestRaw(new byte[0], mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertEquals(0, ((byte[]) (ar.result)).length); + assertNull(ar.exception); + + // byte array with data + + mGSMPhone.invokeOemRilRequestRaw("Hello".getBytes("utf-8"), + mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertEquals("Hello", new String(((byte[]) (ar.result)), "utf-8")); + assertNull(ar.exception); + + // null strings + + mGSMPhone.invokeOemRilRequestStrings(null, mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertNull(ar.result); + assertNull(ar.exception); + + // empty byte array + + mGSMPhone.invokeOemRilRequestStrings(new String[0], + mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertEquals(0, ((String[]) (ar.result)).length); + assertNull(ar.exception); + + // Strings with data + + String s[] = new String[1]; + + s[0] = "Hello"; + + mGSMPhone.invokeOemRilRequestStrings(s, mHandler.obtainMessage(EVENT_OEM_RIL_MESSAGE)); + + msg = mGSMTestHandler.waitForMessage(EVENT_OEM_RIL_MESSAGE); + assertNotNull("Message Time Out", msg); + + ar = ((AsyncResult) msg.obj); + + assertEquals("Hello", ((String[]) (ar.result))[0]); + assertEquals(1, ((String[]) (ar.result)).length); + assertNull(ar.exception); + } + + public void testMmi() throws Exception { + mRadioControl.setAutoProgressConnectingCall(false); + + // "valid" MMI sequences + runValidMmi("*#67#", false); + runValidMmi("##43*11#", false); + runValidMmi("#33*1234*11#", false); + runValidMmi("*21*6505551234**5#", false); + runValidMmi("**03**1234*4321*4321#", false); + // pound string + runValidMmi("5308234092307540923#", true); + // short code + runValidMmi("22", true); + // as part of call setup + runValidMmiWithConnect("*31#6505551234"); + + // invalid MMI sequences + runNotMmi("6505551234"); + runNotMmi("1234#*12#34566654"); + runNotMmi("*#*#12#*"); + } + + private void runValidMmi(String dialString, boolean cancelable) throws CallStateException { + Connection c = mGSMPhone.dial(dialString); + assertNull(c); + Message msg = mGSMTestHandler.waitForMessage(EVENT_MMI_INITIATE); + assertNotNull("Message Time Out", msg); + // Should not be cancelable. + AsyncResult ar = (AsyncResult) msg.obj; + MmiCode mmi = (MmiCode) ar.result; + assertEquals(cancelable, mmi.isCancelable()); + + msg = mGSMTestHandler.waitForMessage(EVENT_MMI_COMPLETE); + assertNotNull("Message Time Out", msg); + } + + private void runValidMmiWithConnect(String dialString) throws CallStateException { + mRadioControl.pauseResponses(); + + Connection c = mGSMPhone.dial(dialString); + assertNotNull(c); + + hangup(c); + } + + private void hangup(Connection cn) throws CallStateException { + cn.hangup(); + + mRadioControl.resumeResponses(); + assertNotNull(mGSMTestHandler.waitForMessage(EVENT_DISCONNECT)); + + } + + private void runNotMmi(String dialString) throws CallStateException { + mRadioControl.pauseResponses(); + + Connection c = mGSMPhone.dial(dialString); + assertNotNull(c); + + hangup(c); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMTestHandler.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMTestHandler.java new file mode 100644 index 0000000..fb8a5d9 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/GSMTestHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2009 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.internal.telephony.gsm; + +import android.content.Context; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import com.android.internal.telephony.gsm.GSMPhone; +import com.android.internal.telephony.test.SimulatedCommands; +import com.android.internal.telephony.TestPhoneNotifier; + +/** + * This class creates a HandlerThread which waits for the various messages. + */ +public class GSMTestHandler extends HandlerThread implements Handler.Callback { + + private Handler mHandler; + private Message mCurrentMessage; + + private Boolean mMsgConsumed; + private SimulatedCommands sc; + private GSMPhone mGSMPhone; + private Context mContext; + + private static final int FAIL_TIMEOUT_MILLIS = 5 * 1000; + + public GSMTestHandler(Context context) { + super("GSMPhoneTest"); + mMsgConsumed = false; + mContext = context; + } + + @Override + protected void onLooperPrepared() { + sc = new SimulatedCommands(); + mGSMPhone = new GSMPhone(mContext, sc, new TestPhoneNotifier(), true); + mHandler = new Handler(getLooper(), this); + synchronized (this) { + notifyAll(); + } + } + + public boolean handleMessage(Message msg) { + synchronized (this) { + mCurrentMessage = msg; + this.notifyAll(); + while(!mMsgConsumed) { + try { + this.wait(); + } catch (InterruptedException e) {} + } + mMsgConsumed = false; + } + return true; + } + + + public void cleanup() { + Looper looper = getLooper(); + if (looper != null) looper.quit(); + mHandler = null; + } + + public Handler getHandler() { + return mHandler; + } + + public SimulatedCommands getSimulatedCommands() { + return sc; + } + + public GSMPhone getGSMPhone() { + return mGSMPhone; + } + + public Message waitForMessage(int code) { + Message msg; + while(true) { + msg = null; + synchronized (this) { + try { + this.wait(FAIL_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + } + + // Check if timeout has occurred. + if (mCurrentMessage != null) { + // Consume the message + msg = Message.obtain(); + msg.copyFrom(mCurrentMessage); + mCurrentMessage = null; + mMsgConsumed = true; + this.notifyAll(); + } + } + if (msg == null || code == GSMPhoneTest.ANY_MESSAGE || msg.what == code) return msg; + } + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java new file mode 100644 index 0000000..82c6944 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java @@ -0,0 +1,758 @@ +/* + * Copyright (C) 2010 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.internal.telephony.gsm; + +import android.telephony.SmsCbEtwsInfo; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.test.AndroidTestCase; +import android.util.Log; + +import com.android.internal.telephony.IccUtils; + +import java.util.Random; + +/** + * Test cases for basic SmsCbMessage operations + */ +public class GsmSmsCbTest extends AndroidTestCase { + + private static final String TAG = "GsmSmsCbTest"; + + private static final SmsCbLocation sTestLocation = new SmsCbLocation("94040", 1234, 5678); + + private static SmsCbMessage createFromPdu(byte[] pdu) { + try { + SmsCbHeader header = new SmsCbHeader(pdu); + byte[][] pdus = new byte[1][]; + pdus[0] = pdu; + return GsmSmsCbMessage.createSmsCbMessage(header, sTestLocation, pdus); + } catch (IllegalArgumentException e) { + return null; + } + } + + private static void doTestGeographicalScopeValue(byte[] pdu, byte b, int expectedGs) { + pdu[0] = b; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected geographical scope decoded", expectedGs, msg + .getGeographicalScope()); + } + + public void testCreateNullPdu() { + SmsCbMessage msg = createFromPdu(null); + assertNull("createFromPdu(byte[] with null pdu should return null", msg); + } + + public void testCreateTooShortPdu() { + byte[] pdu = new byte[4]; + SmsCbMessage msg = createFromPdu(pdu); + + assertNull("createFromPdu(byte[] with too short pdu should return null", msg); + } + + public void testGetGeographicalScope() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + + doTestGeographicalScopeValue(pdu, (byte)0x00, + SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE); + doTestGeographicalScopeValue(pdu, (byte)0x40, SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE); + doTestGeographicalScopeValue(pdu, (byte)0x80, SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE); + doTestGeographicalScopeValue(pdu, (byte)0xC0, SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE); + } + + public void testGetGeographicalScopeUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, (byte)0x3A, + (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, + (byte)0x75, (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, + (byte)0xC9, (byte)0x69, (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x34 + }; + + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected geographical scope decoded", + SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE, msg.getGeographicalScope()); + } + + public void testGetMessageBody7Bit() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A GSM default alphabet message with carriage return padding", + msg.getMessageBody()); + } + + public void testGetMessageBody7BitUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, (byte)0x3A, + (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, + (byte)0x75, (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, + (byte)0xC9, (byte)0x69, (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x34 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A GSM default alphabet message with carriage return padding", + msg.getMessageBody()); + } + + public void testGetMessageBody7BitMultipageUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x01, (byte)0xC0, (byte)0x00, (byte)0x40, + + (byte)0x02, + + (byte)0xC6, (byte)0xB4, (byte)0x7C, (byte)0x4E, (byte)0x07, (byte)0xC1, + (byte)0xC3, (byte)0xE7, (byte)0xF2, (byte)0xAA, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, + (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, + (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x0A, + + (byte)0xD3, (byte)0xF2, (byte)0xF8, (byte)0xED, (byte)0x26, (byte)0x83, + (byte)0xE0, (byte)0xE1, (byte)0x73, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, + (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, + (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x0A + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected multipage 7-bit string decoded", + "First page+Second page", + msg.getMessageBody()); + } + + public void testGetMessageBody7BitFull() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xC4, (byte)0xE5, + (byte)0xB4, (byte)0xFB, (byte)0x0C, (byte)0x2A, (byte)0xE3, (byte)0xC3, (byte)0x63, + (byte)0x3A, (byte)0x3B, (byte)0x0F, (byte)0xCA, (byte)0xCD, (byte)0x40, (byte)0x63, + (byte)0x74, (byte)0x58, (byte)0x1E, (byte)0x1E, (byte)0xD3, (byte)0xCB, (byte)0xF2, + (byte)0x39, (byte)0x88, (byte)0xFD, (byte)0x76, (byte)0x9F, (byte)0x59, (byte)0xA0, + (byte)0x76, (byte)0x39, (byte)0xEC, (byte)0x4E, (byte)0xBB, (byte)0xCF, (byte)0x20, + (byte)0x3A, (byte)0xBA, (byte)0x2C, (byte)0x2F, (byte)0x83, (byte)0xD2, (byte)0x73, + (byte)0x90, (byte)0xFB, (byte)0x0D, (byte)0x82, (byte)0x87, (byte)0xC9, (byte)0xE4, + (byte)0xB4, (byte)0xFB, (byte)0x1C, (byte)0x02 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals( + "Unexpected 7-bit string decoded", + "A GSM default alphabet message being exactly 93 characters long, " + + "meaning there is no padding!", + msg.getMessageBody()); + } + + public void testGetMessageBody7BitFullUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xC4, (byte)0xE5, (byte)0xB4, + (byte)0xFB, (byte)0x0C, (byte)0x2A, (byte)0xE3, (byte)0xC3, (byte)0x63, + (byte)0x3A, (byte)0x3B, (byte)0x0F, (byte)0xCA, (byte)0xCD, (byte)0x40, + (byte)0x63, (byte)0x74, (byte)0x58, (byte)0x1E, (byte)0x1E, (byte)0xD3, + (byte)0xCB, (byte)0xF2, (byte)0x39, (byte)0x88, (byte)0xFD, (byte)0x76, + (byte)0x9F, (byte)0x59, (byte)0xA0, (byte)0x76, (byte)0x39, (byte)0xEC, + (byte)0x4E, (byte)0xBB, (byte)0xCF, (byte)0x20, (byte)0x3A, (byte)0xBA, + (byte)0x2C, (byte)0x2F, (byte)0x83, (byte)0xD2, (byte)0x73, (byte)0x90, + (byte)0xFB, (byte)0x0D, (byte)0x82, (byte)0x87, (byte)0xC9, (byte)0xE4, + (byte)0xB4, (byte)0xFB, (byte)0x1C, (byte)0x02, + + (byte)0x52 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals( + "Unexpected 7-bit string decoded", + "A GSM default alphabet message being exactly 93 characters long, " + + "meaning there is no padding!", + msg.getMessageBody()); + } + + public void testGetMessageBody7BitWithLanguage() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x04, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A GSM default alphabet message with carriage return padding", + msg.getMessageBody()); + + assertEquals("Unexpected language indicator decoded", "es", msg.getLanguageCode()); + } + + public void testGetMessageBody7BitWithLanguageInBody() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x10, (byte)0x11, (byte)0x73, + (byte)0x7B, (byte)0x23, (byte)0x08, (byte)0x3A, (byte)0x4E, (byte)0x9B, (byte)0x20, + (byte)0x72, (byte)0xD9, (byte)0x1C, (byte)0xAE, (byte)0xB3, (byte)0xE9, (byte)0xA0, + (byte)0x30, (byte)0x1B, (byte)0x8E, (byte)0x0E, (byte)0x8B, (byte)0xCB, (byte)0x74, + (byte)0x50, (byte)0xBB, (byte)0x3C, (byte)0x9F, (byte)0x87, (byte)0xCF, (byte)0x65, + (byte)0xD0, (byte)0x3D, (byte)0x4D, (byte)0x47, (byte)0x83, (byte)0xC6, (byte)0x61, + (byte)0xB9, (byte)0x3C, (byte)0x1D, (byte)0x3E, (byte)0x97, (byte)0x41, (byte)0xF2, + (byte)0x32, (byte)0xBD, (byte)0x2E, (byte)0x77, (byte)0x83, (byte)0xE0, (byte)0x61, + (byte)0x32, (byte)0x39, (byte)0xED, (byte)0x3E, (byte)0x37, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A GSM default alphabet message with carriage return padding", + msg.getMessageBody()); + + assertEquals("Unexpected language indicator decoded", "sv", msg.getLanguageCode()); + } + + public void testGetMessageBody7BitWithLanguageInBodyUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x10, + + (byte)0x01, + + (byte)0x73, (byte)0x7B, (byte)0x23, (byte)0x08, (byte)0x3A, (byte)0x4E, + (byte)0x9B, (byte)0x20, (byte)0x72, (byte)0xD9, (byte)0x1C, (byte)0xAE, + (byte)0xB3, (byte)0xE9, (byte)0xA0, (byte)0x30, (byte)0x1B, (byte)0x8E, + (byte)0x0E, (byte)0x8B, (byte)0xCB, (byte)0x74, (byte)0x50, (byte)0xBB, + (byte)0x3C, (byte)0x9F, (byte)0x87, (byte)0xCF, (byte)0x65, (byte)0xD0, + (byte)0x3D, (byte)0x4D, (byte)0x47, (byte)0x83, (byte)0xC6, (byte)0x61, + (byte)0xB9, (byte)0x3C, (byte)0x1D, (byte)0x3E, (byte)0x97, (byte)0x41, + (byte)0xF2, (byte)0x32, (byte)0xBD, (byte)0x2E, (byte)0x77, (byte)0x83, + (byte)0xE0, (byte)0x61, (byte)0x32, (byte)0x39, (byte)0xED, (byte)0x3E, + (byte)0x37, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x37 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A GSM default alphabet message with carriage return padding", + msg.getMessageBody()); + + assertEquals("Unexpected language indicator decoded", "sv", msg.getLanguageCode()); + } + + public void testGetMessageBody8Bit() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x44, (byte)0x11, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41, + (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("8-bit message body should be empty", "", msg.getMessageBody()); + } + + public void testGetMessageBodyUcs2() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x48, (byte)0x11, (byte)0x00, + (byte)0x41, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x55, (byte)0x00, (byte)0x43, + (byte)0x00, (byte)0x53, (byte)0x00, (byte)0x32, (byte)0x00, (byte)0x20, (byte)0x00, + (byte)0x6D, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x73, (byte)0x00, (byte)0x73, + (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x65, (byte)0x00, + (byte)0x20, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x6F, (byte)0x00, (byte)0x6E, + (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x69, (byte)0x00, + (byte)0x6E, (byte)0x00, (byte)0x69, (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x67, + (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x20, (byte)0x04, + (byte)0x34, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x68, + (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x61, (byte)0x00, + (byte)0x63, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x72, + (byte)0x00, (byte)0x0D, (byte)0x00, (byte)0x0D + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A UCS2 message containing a \u0434 character", msg.getMessageBody()); + } + + public void testGetMessageBodyUcs2Umts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x48, + + (byte)0x01, + + (byte)0x00, (byte)0x41, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x55, + (byte)0x00, (byte)0x43, (byte)0x00, (byte)0x53, (byte)0x00, (byte)0x32, + (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x6D, (byte)0x00, (byte)0x65, + (byte)0x00, (byte)0x73, (byte)0x00, (byte)0x73, (byte)0x00, (byte)0x61, + (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x20, + (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x6F, (byte)0x00, (byte)0x6E, + (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x69, + (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x69, (byte)0x00, (byte)0x6E, + (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x61, + (byte)0x00, (byte)0x20, (byte)0x04, (byte)0x34, (byte)0x00, (byte)0x20, + (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x68, (byte)0x00, (byte)0x61, + (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x63, + (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x72, + (byte)0x00, (byte)0x0D, (byte)0x00, (byte)0x0D, + + (byte)0x4E + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A UCS2 message containing a \u0434 character", msg.getMessageBody()); + } + + public void testGetMessageBodyUcs2MultipageUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x48, + + (byte)0x02, + + (byte)0x00, (byte)0x41, (byte)0x00, (byte)0x41, (byte)0x00, (byte)0x41, + (byte)0x00, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + + (byte)0x06, + + (byte)0x00, (byte)0x42, (byte)0x00, (byte)0x42, (byte)0x00, (byte)0x42, + (byte)0x00, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + (byte)0x0D, (byte)0x0D, (byte)0x0D, (byte)0x0D, + + (byte)0x06 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected multipage UCS2 string decoded", + "AAABBB", msg.getMessageBody()); + } + + public void testGetMessageBodyUcs2WithLanguageInBody() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x00, (byte)0x32, (byte)0x11, (byte)0x11, (byte)0x78, + (byte)0x3C, (byte)0x00, (byte)0x41, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x55, + (byte)0x00, (byte)0x43, (byte)0x00, (byte)0x53, (byte)0x00, (byte)0x32, (byte)0x00, + (byte)0x20, (byte)0x00, (byte)0x6D, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x73, + (byte)0x00, (byte)0x73, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x67, (byte)0x00, + (byte)0x65, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x6F, + (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x61, (byte)0x00, + (byte)0x69, (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x69, (byte)0x00, (byte)0x6E, + (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x61, (byte)0x00, + (byte)0x20, (byte)0x04, (byte)0x34, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x63, + (byte)0x00, (byte)0x68, (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x72, (byte)0x00, + (byte)0x61, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65, + (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x0D + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A UCS2 message containing a \u0434 character", msg.getMessageBody()); + + assertEquals("Unexpected language indicator decoded", "xx", msg.getLanguageCode()); + } + + public void testGetMessageBodyUcs2WithLanguageInBodyUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x00, (byte)0x32, (byte)0xC0, (byte)0x00, (byte)0x11, + + (byte)0x01, + + (byte)0x78, (byte)0x3C, (byte)0x00, (byte)0x41, (byte)0x00, (byte)0x20, + (byte)0x00, (byte)0x55, (byte)0x00, (byte)0x43, (byte)0x00, (byte)0x53, + (byte)0x00, (byte)0x32, (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x6D, + (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x73, (byte)0x00, (byte)0x73, + (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x65, + (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x6F, + (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x61, + (byte)0x00, (byte)0x69, (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x69, + (byte)0x00, (byte)0x6E, (byte)0x00, (byte)0x67, (byte)0x00, (byte)0x20, + (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x20, (byte)0x04, (byte)0x34, + (byte)0x00, (byte)0x20, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x68, + (byte)0x00, (byte)0x61, (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x61, + (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65, + (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x0D, + + (byte)0x50 + }; + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected 7-bit string decoded", + "A UCS2 message containing a \u0434 character", msg.getMessageBody()); + + assertEquals("Unexpected language indicator decoded", "xx", msg.getLanguageCode()); + } + + public void testGetMessageIdentifier() { + byte[] pdu = { + (byte)0xC0, (byte)0x00, (byte)0x30, (byte)0x39, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory()); + } + + public void testGetMessageIdentifierUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x30, (byte)0x39, (byte)0x2A, (byte)0xA5, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, (byte)0x3A, + (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, + (byte)0x75, (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, + (byte)0xC9, (byte)0x69, (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x34 + }; + + SmsCbMessage msg = createFromPdu(pdu); + + assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory()); + } + + public void testGetMessageCode() { + byte[] pdu = { + (byte)0x2A, (byte)0xA5, (byte)0x30, (byte)0x39, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + + SmsCbMessage msg = createFromPdu(pdu); + int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4; + + assertEquals("Unexpected message code decoded", 682, messageCode); + } + + public void testGetMessageCodeUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x30, (byte)0x39, (byte)0x2A, (byte)0xA5, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, (byte)0x3A, + (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, + (byte)0x75, (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, + (byte)0xC9, (byte)0x69, (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x34 + }; + + SmsCbMessage msg = createFromPdu(pdu); + int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4; + + assertEquals("Unexpected message code decoded", 682, messageCode); + } + + public void testGetUpdateNumber() { + byte[] pdu = { + (byte)0x2A, (byte)0xA5, (byte)0x30, (byte)0x39, (byte)0x40, (byte)0x11, (byte)0x41, + (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, (byte)0xCB, (byte)0xE6, + (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, (byte)0x85, (byte)0xD9, (byte)0x70, + (byte)0x74, (byte)0x58, (byte)0x5C, (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, + (byte)0xF9, (byte)0x3C, (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, + (byte)0x3A, (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, (byte)0x75, + (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, (byte)0xC9, (byte)0x69, + (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00 + }; + + SmsCbMessage msg = createFromPdu(pdu); + int updateNumber = msg.getSerialNumber() & 0x000f; + + assertEquals("Unexpected update number decoded", 5, updateNumber); + } + + public void testGetUpdateNumberUmts() { + byte[] pdu = { + (byte)0x01, (byte)0x30, (byte)0x39, (byte)0x2A, (byte)0xA5, (byte)0x40, + + (byte)0x01, + + (byte)0x41, (byte)0xD0, (byte)0x71, (byte)0xDA, (byte)0x04, (byte)0x91, + (byte)0xCB, (byte)0xE6, (byte)0x70, (byte)0x9D, (byte)0x4D, (byte)0x07, + (byte)0x85, (byte)0xD9, (byte)0x70, (byte)0x74, (byte)0x58, (byte)0x5C, + (byte)0xA6, (byte)0x83, (byte)0xDA, (byte)0xE5, (byte)0xF9, (byte)0x3C, + (byte)0x7C, (byte)0x2E, (byte)0x83, (byte)0xEE, (byte)0x69, (byte)0x3A, + (byte)0x1A, (byte)0x34, (byte)0x0E, (byte)0xCB, (byte)0xE5, (byte)0xE9, + (byte)0xF0, (byte)0xB9, (byte)0x0C, (byte)0x92, (byte)0x97, (byte)0xE9, + (byte)0x75, (byte)0xB9, (byte)0x1B, (byte)0x04, (byte)0x0F, (byte)0x93, + (byte)0xC9, (byte)0x69, (byte)0xF7, (byte)0xB9, (byte)0xD1, (byte)0x68, + (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, (byte)0xD1, + (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, (byte)0xA3, + (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, (byte)0x46, + (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D, + (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00, + + (byte)0x34 + }; + + SmsCbMessage msg = createFromPdu(pdu); + int updateNumber = msg.getSerialNumber() & 0x000f; + + assertEquals("Unexpected update number decoded", 5, updateNumber); + } + + /* ETWS Test message including header */ + private static final byte[] etwsMessageNormal = IccUtils.hexStringToBytes("000011001101" + + "0D0A5BAE57CE770C531790E85C716CBF3044573065B930675730" + + "9707767A751F30025F37304463FA308C306B5099304830664E0B30553044FF086C178C615E81FF09" + + "0000000000000000000000000000"); + + private static final byte[] etwsMessageCancel = IccUtils.hexStringToBytes("000011001101" + + "0D0A5148307B3069002800310030003A0035" + + "00320029306E7DCA602557309707901F5831309253D66D883057307E3059FF086C178C615E81FF09" + + "00000000000000000000000000000000000000000000"); + + private static final byte[] etwsMessageTest = IccUtils.hexStringToBytes("000011031101" + + "0D0A5BAE57CE770C531790E85C716CBF3044" + + "573065B9306757309707300263FA308C306B5099304830664E0B30553044FF086C178C615E81FF09" + + "00000000000000000000000000000000000000000000"); + + // FIXME: add example of ETWS primary notification PDU + + public void testEtwsMessageNormal() { + SmsCbMessage msg = createFromPdu(etwsMessageNormal); + Log.d(TAG, msg.toString()); + assertEquals("GS mismatch", 0, msg.getGeographicalScope()); + assertEquals("serial number mismatch", 0, msg.getSerialNumber()); + assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory()); + assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE, + msg.getEtwsWarningInfo().getWarningType()); + } + + public void testEtwsMessageCancel() { + SmsCbMessage msg = createFromPdu(etwsMessageCancel); + Log.d(TAG, msg.toString()); + assertEquals("GS mismatch", 0, msg.getGeographicalScope()); + assertEquals("serial number mismatch", 0, msg.getSerialNumber()); + assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory()); + assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE, + msg.getEtwsWarningInfo().getWarningType()); + } + + public void testEtwsMessageTest() { + SmsCbMessage msg = createFromPdu(etwsMessageTest); + Log.d(TAG, msg.toString()); + assertEquals("GS mismatch", 0, msg.getGeographicalScope()); + assertEquals("serial number mismatch", 0, msg.getSerialNumber()); + assertEquals("message ID mismatch", 0x1103, msg.getServiceCategory()); + assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE, + msg.getEtwsWarningInfo().getWarningType()); + } + + // Make sure we don't throw an exception if we feed random data to the PDU parser. + public void testRandomPdus() { + Random r = new Random(94040); + for (int run = 0; run < 10000; run++) { + int len = r.nextInt(140); + byte[] data = new byte[len]; + for (int i = 0; i < len; i++) { + data[i] = (byte) r.nextInt(256); + } + try { + // this should return a SmsCbMessage object or null for invalid data + SmsCbMessage msg = createFromPdu(data); + } catch (Exception e) { + Log.d(TAG, "exception thrown", e); + fail("Exception in decoder at run " + run + " length " + len + ": " + e); + } + } + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadCommands.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadCommands.java new file mode 100644 index 0000000..ea6836d --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadCommands.java @@ -0,0 +1,624 @@ +/* + * Copyright (C) 2011 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.internal.telephony.gsm; + +import android.content.Context; +import android.os.AsyncResult; +import android.os.Message; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.telephony.BaseCommands; +import com.android.internal.telephony.IccIoResult; +import com.android.internal.telephony.UUSInfo; + +import junit.framework.Assert; + +/** + * Dummy BaseCommands for UsimDataDownloadTest. Only implements UICC envelope and + * SMS acknowledgement commands. + */ +class UsimDataDownloadCommands extends BaseCommands { + private static final String TAG = "UsimDataDownloadCommands"; + + private boolean mExpectingAcknowledgeGsmSms; // true if expecting ack GSM SMS + private boolean mExpectingAcknowledgeGsmSmsSuccess; // true if expecting ack SMS success + private int mExpectingAcknowledgeGsmSmsFailureCause; // expecting ack SMS failure cause + private String mExpectingAcknowledgeGsmSmsPdu; // expecting ack SMS PDU + + private boolean mExpectingSendEnvelope; // true to expect a send envelope command + private String mExpectingSendEnvelopeContents; // expected string for send envelope + private int mExpectingSendEnvelopeResponseSw1; // SW1/SW2 response status + private int mExpectingSendEnvelopeResponseSw2; // SW1/SW2 response status + private String mExpectingSendEnvelopeResponse; // Response string for Send Envelope + + UsimDataDownloadCommands(Context context) { + super(context); + } + + /** + * Expect a call to acknowledgeLastIncomingGsmSms with success flag and failure cause. + * @param success true if expecting success; false if expecting failure + * @param cause the failure cause, if success is false + */ + synchronized void expectAcknowledgeGsmSms(boolean success, int cause) { + Assert.assertFalse("expectAcknowledgeGsmSms called twice", mExpectingAcknowledgeGsmSms); + mExpectingAcknowledgeGsmSms = true; + mExpectingAcknowledgeGsmSmsSuccess = success; + mExpectingAcknowledgeGsmSmsFailureCause = cause; + } + + /** + * Expect a call to acknowledgeLastIncomingGsmSmsWithPdu with success flag and PDU. + * @param success true if expecting success; false if expecting failure + * @param ackPdu the acknowledgement PDU to expect + */ + synchronized void expectAcknowledgeGsmSmsWithPdu(boolean success, String ackPdu) { + Assert.assertFalse("expectAcknowledgeGsmSms called twice", mExpectingAcknowledgeGsmSms); + mExpectingAcknowledgeGsmSms = true; + mExpectingAcknowledgeGsmSmsSuccess = success; + mExpectingAcknowledgeGsmSmsPdu = ackPdu; + } + + /** + * Expect a call to sendEnvelopeWithStatus(). + * @param contents expected envelope contents to send + * @param sw1 simulated SW1 status to return + * @param sw2 simulated SW2 status to return + * @param response simulated envelope response to return + */ + synchronized void expectSendEnvelope(String contents, int sw1, int sw2, String response) { + Assert.assertFalse("expectSendEnvelope called twice", mExpectingSendEnvelope); + mExpectingSendEnvelope = true; + mExpectingSendEnvelopeContents = contents; + mExpectingSendEnvelopeResponseSw1 = sw1; + mExpectingSendEnvelopeResponseSw2 = sw2; + mExpectingSendEnvelopeResponse = response; + } + + synchronized void assertExpectedMethodsCalled() { + long stopTime = SystemClock.elapsedRealtime() + 5000; + while ((mExpectingAcknowledgeGsmSms || mExpectingSendEnvelope) + && SystemClock.elapsedRealtime() < stopTime) { + try { + wait(); + } catch (InterruptedException ignored) {} + } + Assert.assertFalse("expecting SMS acknowledge call", mExpectingAcknowledgeGsmSms); + Assert.assertFalse("expecting send envelope call", mExpectingSendEnvelope); + } + + @Override + public synchronized void acknowledgeLastIncomingGsmSms(boolean success, int cause, + Message response) { + Log.d(TAG, "acknowledgeLastIncomingGsmSms: success=" + success + ", cause=" + cause); + Assert.assertTrue("unexpected call to acknowledge SMS", mExpectingAcknowledgeGsmSms); + Assert.assertEquals(mExpectingAcknowledgeGsmSmsSuccess, success); + Assert.assertEquals(mExpectingAcknowledgeGsmSmsFailureCause, cause); + mExpectingAcknowledgeGsmSms = false; + if (response != null) { + AsyncResult.forMessage(response); + response.sendToTarget(); + } + notifyAll(); // wake up assertExpectedMethodsCalled() + } + + @Override + public synchronized void acknowledgeIncomingGsmSmsWithPdu(boolean success, String ackPdu, + Message response) { + Log.d(TAG, "acknowledgeLastIncomingGsmSmsWithPdu: success=" + success + + ", ackPDU= " + ackPdu); + Assert.assertTrue("unexpected call to acknowledge SMS", mExpectingAcknowledgeGsmSms); + Assert.assertEquals(mExpectingAcknowledgeGsmSmsSuccess, success); + Assert.assertEquals(mExpectingAcknowledgeGsmSmsPdu, ackPdu); + mExpectingAcknowledgeGsmSms = false; + if (response != null) { + AsyncResult.forMessage(response); + response.sendToTarget(); + } + notifyAll(); // wake up assertExpectedMethodsCalled() + } + + @Override + public synchronized void sendEnvelopeWithStatus(String contents, Message response) { + // Add spaces between hex bytes for readability + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < contents.length(); i += 2) { + builder.append(contents.charAt(i)).append(contents.charAt(i+1)).append(' '); + } + Log.d(TAG, "sendEnvelopeWithStatus: " + builder.toString()); + + Assert.assertTrue("unexpected call to send envelope", mExpectingSendEnvelope); + Assert.assertEquals(mExpectingSendEnvelopeContents, contents); + mExpectingSendEnvelope = false; + + IccIoResult result = new IccIoResult(mExpectingSendEnvelopeResponseSw1, + mExpectingSendEnvelopeResponseSw2, mExpectingSendEnvelopeResponse); + + if (response != null) { + AsyncResult.forMessage(response, result, null); + response.sendToTarget(); + } + notifyAll(); // wake up assertExpectedMethodsCalled() + } + + @Override + public void setSuppServiceNotifications(boolean enable, Message result) { + } + + @Override + public void supplyIccPin(String pin, Message result) { + } + + @Override + public void supplyIccPinForApp(String pin, String aid, Message result) { + } + + @Override + public void supplyIccPuk(String puk, String newPin, Message result) { + } + + @Override + public void supplyIccPukForApp(String puk, String newPin, String aid, Message result) { + } + + @Override + public void supplyIccPin2(String pin2, Message result) { + } + + @Override + public void supplyIccPin2ForApp(String pin2, String aid, Message result) { + } + + @Override + public void supplyIccPuk2(String puk2, String newPin2, Message result) { + } + + @Override + public void supplyIccPuk2ForApp(String puk2, String newPin2, String aid, Message result) { + } + + @Override + public void changeIccPin(String oldPin, String newPin, Message result) { + } + + @Override + public void changeIccPinForApp(String oldPin, String newPin, String aidPtr, Message result) { + } + + @Override + public void changeIccPin2(String oldPin2, String newPin2, Message result) { + } + + @Override + public void changeIccPin2ForApp(String oldPin2, String newPin2, String aidPtr, Message result) { + } + + @Override + public void changeBarringPassword(String facility, String oldPwd, String newPwd, + Message result) { + } + + @Override + public void supplyNetworkDepersonalization(String netpin, Message result) { + } + + @Override + public void getCurrentCalls(Message result) { + } + + @Override + public void getPDPContextList(Message result) { + } + + @Override + public void getDataCallList(Message result) { + } + + @Override + public void dial(String address, int clirMode, Message result) { + } + + @Override + public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) { + } + + @Override + public void getIMSI(Message result) { + } + + @Override + public void getIMEI(Message result) { + } + + @Override + public void getIMEISV(Message result) { + } + + @Override + public void hangupConnection(int gsmIndex, Message result) { + } + + @Override + public void hangupWaitingOrBackground(Message result) { + } + + @Override + public void hangupForegroundResumeBackground(Message result) { + } + + @Override + public void switchWaitingOrHoldingAndActive(Message result) { + } + + @Override + public void conference(Message result) { + } + + @Override + public void setPreferredVoicePrivacy(boolean enable, Message result) { + } + + @Override + public void getPreferredVoicePrivacy(Message result) { + } + + @Override + public void separateConnection(int gsmIndex, Message result) { + } + + @Override + public void acceptCall(Message result) { + } + + @Override + public void rejectCall(Message result) { + } + + @Override + public void explicitCallTransfer(Message result) { + } + + @Override + public void getLastCallFailCause(Message result) { + } + + @Override + public void getLastPdpFailCause(Message result) { + } + + @Override + public void getLastDataCallFailCause(Message result) { + } + + @Override + public void setMute(boolean enableMute, Message response) { + } + + @Override + public void getMute(Message response) { + } + + @Override + public void getSignalStrength(Message response) { + } + + @Override + public void getVoiceRegistrationState(Message response) { + } + + @Override + public void getDataRegistrationState(Message response) { + } + + @Override + public void getOperator(Message response) { + } + + @Override + public void sendDtmf(char c, Message result) { + } + + @Override + public void startDtmf(char c, Message result) { + } + + @Override + public void stopDtmf(Message result) { + } + + @Override + public void sendBurstDtmf(String dtmfString, int on, int off, Message result) { + } + + @Override + public void sendSMS(String smscPDU, String pdu, Message response) { + } + + @Override + public void sendCdmaSms(byte[] pdu, Message response) { + } + + @Override + public void deleteSmsOnSim(int index, Message response) { + } + + @Override + public void deleteSmsOnRuim(int index, Message response) { + } + + @Override + public void writeSmsToSim(int status, String smsc, String pdu, Message response) { + } + + @Override + public void writeSmsToRuim(int status, String pdu, Message response) { + } + + @Override + public void setRadioPower(boolean on, Message response) { + } + + @Override + public void acknowledgeLastIncomingCdmaSms(boolean success, int cause, Message response) { + } + + @Override + public void iccIO(int command, int fileid, String path, int p1, int p2, int p3, String data, + String pin2, Message response) { + } + + @Override + public void queryCLIP(Message response) { + } + + @Override + public void getCLIR(Message response) { + } + + @Override + public void setCLIR(int clirMode, Message response) { + } + + @Override + public void queryCallWaiting(int serviceClass, Message response) { + } + + @Override + public void setCallWaiting(boolean enable, int serviceClass, Message response) { + } + + @Override + public void setCallForward(int action, int cfReason, int serviceClass, String number, + int timeSeconds, Message response) { + } + + @Override + public void queryCallForwardStatus(int cfReason, int serviceClass, String number, + Message response) { + } + + @Override + public void setNetworkSelectionModeAutomatic(Message response) { + } + + @Override + public void setNetworkSelectionModeManual(String operatorNumeric, Message response) { + } + + @Override + public void getNetworkSelectionMode(Message response) { + } + + @Override + public void getAvailableNetworks(Message response) { + } + + @Override + public void getBasebandVersion(Message response) { + } + + @Override + public void queryFacilityLock(String facility, String password, int serviceClass, + Message response) { + } + + @Override + public void queryFacilityLockForApp(String facility, String password, int serviceClass, + String appId, Message response) { + } + + @Override + public void setFacilityLock(String facility, boolean lockState, String password, + int serviceClass, Message response) { + } + + @Override + public void setFacilityLockForApp(String facility, boolean lockState, String password, + int serviceClass, String appId, Message response) { + } + + @Override + public void sendUSSD(String ussdString, Message response) { + } + + @Override + public void cancelPendingUssd(Message response) { + } + + @Override + public void resetRadio(Message result) { + } + + @Override + public void setBandMode(int bandMode, Message response) { + } + + @Override + public void queryAvailableBandMode(Message response) { + } + + @Override + public void setPreferredNetworkType(int networkType, Message response) { + } + + @Override + public void getPreferredNetworkType(Message response) { + } + + @Override + public void getNeighboringCids(Message response) { + } + + @Override + public void setLocationUpdates(boolean enable, Message response) { + } + + @Override + public void getSmscAddress(Message result) { + } + + @Override + public void setSmscAddress(String address, Message result) { + } + + @Override + public void reportSmsMemoryStatus(boolean available, Message result) { + } + + @Override + public void reportStkServiceIsRunning(Message result) { + } + + @Override + public void invokeOemRilRequestRaw(byte[] data, Message response) { + } + + @Override + public void invokeOemRilRequestStrings(String[] strings, Message response) { + } + + @Override + public void sendTerminalResponse(String contents, Message response) { + } + + @Override + public void sendEnvelope(String contents, Message response) { + } + + @Override + public void handleCallSetupRequestFromSim(boolean accept, Message response) { + } + + @Override + public void setGsmBroadcastActivation(boolean activate, Message result) { + } + + @Override + public void setGsmBroadcastConfig(SmsBroadcastConfigInfo[] config, Message response) { + } + + @Override + public void getGsmBroadcastConfig(Message response) { + } + + @Override + public void getDeviceIdentity(Message response) { + } + + @Override + public void getCDMASubscription(Message response) { + } + + @Override + public void sendCDMAFeatureCode(String FeatureCode, Message response) { + } + + @Override + public void setPhoneType(int phoneType) { + } + + @Override + public void queryCdmaRoamingPreference(Message response) { + } + + @Override + public void setCdmaRoamingPreference(int cdmaRoamingType, Message response) { + } + + @Override + public void setCdmaSubscriptionSource(int cdmaSubscriptionType, Message response) { + } + + @Override + public void getCdmaSubscriptionSource(Message response) { + } + + @Override + public void setTTYMode(int ttyMode, Message response) { + } + + @Override + public void queryTTYMode(Message response) { + } + + @Override + public void setupDataCall(String radioTechnology, String profile, String apn, String user, + String password, String authType, String protocol, Message result) { + } + + @Override + public void deactivateDataCall(int cid, int reason, Message result) { + } + + @Override + public void setCdmaBroadcastActivation(boolean activate, Message result) { + } + + @Override + public void setCdmaBroadcastConfig(int[] configValuesArray, Message result) { + } + + @Override + public void getCdmaBroadcastConfig(Message result) { + } + + @Override + public void exitEmergencyCallbackMode(Message response) { + } + + @Override + public void getIccCardStatus(Message result) { + } + + @Override + public void requestIsimAuthentication(String nonce, Message response) { + } + + @Override + public void getVoiceRadioTechnology(Message response) { + } + + @Override + public void getIMSIForApp(String aid, Message result) { + } + + @Override + public void iccIOForApp(int command, int fileid, String path, int p1, int p2, int p3, + String data, String pin2, String aid, Message response) { + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadTest.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadTest.java new file mode 100644 index 0000000..6c8ba5e --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimDataDownloadTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2011 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.internal.telephony.gsm; + +import android.os.HandlerThread; +import android.test.AndroidTestCase; +import android.util.Log; + +import java.nio.charset.Charset; + +/** + * Test SMS-PP data download to UICC. + * Uses test messages from 3GPP TS 31.124 section 27.22.5. + */ +public class UsimDataDownloadTest extends AndroidTestCase { + private static final String TAG = "UsimDataDownloadTest"; + + class TestHandlerThread extends HandlerThread { + private UsimDataDownloadHandler mHandler; + + TestHandlerThread() { + super("TestHandlerThread"); + } + + @Override + protected void onLooperPrepared() { + synchronized (this) { + mHandler = new UsimDataDownloadHandler(mCm); + notifyAll(); + } + } + + UsimDataDownloadHandler getHandler() { + synchronized (this) { + while (mHandler == null) { + try { + wait(); + } catch (InterruptedException ignored) {} + } + return mHandler; + } + } + } + + private UsimDataDownloadCommands mCm; + private TestHandlerThread mHandlerThread; + UsimDataDownloadHandler mHandler; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mCm = new UsimDataDownloadCommands(mContext); + mHandlerThread = new TestHandlerThread(); + mHandlerThread.start(); + mHandler = mHandlerThread.getHandler(); + Log.d(TAG, "mHandler is constructed"); + } + + @Override + protected void tearDown() throws Exception { + mHandlerThread.quit(); + super.tearDown(); + } + + // SMS-PP Message 3.1.1 + private static final byte[] SMS_PP_MESSAGE_3_1_1 = { + // Service center address + 0x09, (byte) 0x91, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, (byte) 0xf8, + + 0x04, 0x04, (byte) 0x91, 0x21, 0x43, 0x7f, 0x16, (byte) 0x89, 0x10, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x0d, 0x54, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x20, 0x31 + }; + + // SMS-PP Download Envelope 3.1.1 + private static final String SMS_PP_ENVELOPE_3_1_1 = "d12d8202838106099111223344556677f88b1c04" + + "049121437f16891010000000000d546573744d6573736167652031"; + + // SMS-PP Message 3.1.5 + private static final byte[] SMS_PP_MESSAGE_3_1_5 = { + // Service center address + 0x09, (byte) 0x91, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, (byte) 0xf8, + + 0x44, 0x04, (byte) 0x91, 0x21, 0x43, 0x7f, (byte) 0xf6, (byte) 0x89, 0x10, 0x10, 0x00, + 0x00, 0x00, 0x00, 0x1e, 0x02, 0x70, 0x00, 0x00, 0x19, 0x00, 0x0d, 0x00, 0x00, + 0x00, 0x00, (byte) 0xbf, (byte) 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + (byte) 0xdc, (byte) 0xdc, (byte) 0xdc, (byte) 0xdc, (byte) 0xdc, (byte) 0xdc, + (byte) 0xdc, (byte) 0xdc, (byte) 0xdc, (byte) 0xdc + }; + + // SMS-PP Download Envelope 3.1.5 + private static final String SMS_PP_ENVELOPE_3_1_5 = "d13e8202838106099111223344556677f88b2d44" + + "049121437ff6891010000000001e0270000019000d00000000bfff00000000000100" + + "dcdcdcdcdcdcdcdcdcdc"; + + public void testDataDownloadMessage1() { + SmsMessage message = SmsMessage.createFromPdu(SMS_PP_MESSAGE_3_1_1); + assertTrue("message is SMS-PP data download", message.isUsimDataDownload()); + + mCm.expectSendEnvelope(SMS_PP_ENVELOPE_3_1_1, 0x90, 0x00, ""); + mCm.expectAcknowledgeGsmSms(true, 0); + mHandler.startDataDownload(message); + mCm.assertExpectedMethodsCalled(); + + mCm.expectSendEnvelope(SMS_PP_ENVELOPE_3_1_1, 0x90, 0x00, "0123456789"); + mCm.expectAcknowledgeGsmSmsWithPdu(true, "00077f16050123456789"); + mHandler.startDataDownload(message); + mCm.assertExpectedMethodsCalled(); + + mCm.expectSendEnvelope(SMS_PP_ENVELOPE_3_1_1, 0x62, 0xff, "0123456789abcdef"); + mCm.expectAcknowledgeGsmSmsWithPdu(false, "00d5077f16080123456789abcdef"); + mHandler.startDataDownload(message); + mCm.assertExpectedMethodsCalled(); + } + + public void testDataDownloadMessage5() { + SmsMessage message = SmsMessage.createFromPdu(SMS_PP_MESSAGE_3_1_5); + assertTrue("message is SMS-PP data download", message.isUsimDataDownload()); + + mCm.expectSendEnvelope(SMS_PP_ENVELOPE_3_1_5, 0x90, 0x00, "9876543210"); + mCm.expectAcknowledgeGsmSmsWithPdu(true, "00077ff6059876543210"); + mHandler.startDataDownload(message); + mCm.assertExpectedMethodsCalled(); + + mCm.expectSendEnvelope(SMS_PP_ENVELOPE_3_1_5, 0x93, 0x00, ""); + mCm.expectAcknowledgeGsmSms(false, 0xd4); // SIM toolkit busy + mHandler.startDataDownload(message); + mCm.assertExpectedMethodsCalled(); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimServiceTableTest.java b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimServiceTableTest.java new file mode 100644 index 0000000..56854ed --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/gsm/UsimServiceTableTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 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.internal.telephony.gsm; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +/** + * Test UsimServiceTable class. + */ +public class UsimServiceTableTest extends AndroidTestCase { + + @SmallTest + public void testUsimServiceTable() { + byte[] noServices = {0x00}; + byte[] service1 = {0x01, 0x00}; + byte[] service8 = {(byte) 0x80, 0x00, 0x00}; + byte[] service8And9 = {(byte) 0x80, 0x01}; + byte[] service28 = {0x00, 0x00, 0x00, 0x08}; + byte[] service89To96 = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, (byte) 0xff}; + + UsimServiceTable testTable1 = new UsimServiceTable(noServices); + assertFalse(testTable1.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertFalse(testTable1.isAvailable(UsimServiceTable.UsimService.FDN)); + assertFalse(testTable1.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + + UsimServiceTable testTable2 = new UsimServiceTable(service1); + assertTrue(testTable2.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertFalse(testTable2.isAvailable(UsimServiceTable.UsimService.FDN)); + assertFalse(testTable2.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + + UsimServiceTable testTable3 = new UsimServiceTable(service8); + assertFalse(testTable3.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertFalse(testTable3.isAvailable(UsimServiceTable.UsimService.BDN_EXTENSION)); + assertTrue(testTable3.isAvailable(UsimServiceTable.UsimService.OUTGOING_CALL_INFO)); + assertFalse(testTable3.isAvailable(UsimServiceTable.UsimService.INCOMING_CALL_INFO)); + assertFalse(testTable3.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + + UsimServiceTable testTable4 = new UsimServiceTable(service8And9); + assertFalse(testTable4.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertFalse(testTable4.isAvailable(UsimServiceTable.UsimService.BDN_EXTENSION)); + assertTrue(testTable4.isAvailable(UsimServiceTable.UsimService.OUTGOING_CALL_INFO)); + assertTrue(testTable4.isAvailable(UsimServiceTable.UsimService.INCOMING_CALL_INFO)); + assertFalse(testTable4.isAvailable(UsimServiceTable.UsimService.SM_STORAGE)); + assertFalse(testTable4.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + + UsimServiceTable testTable5 = new UsimServiceTable(service28); + assertFalse(testTable5.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertTrue(testTable5.isAvailable(UsimServiceTable.UsimService.DATA_DL_VIA_SMS_PP)); + assertFalse(testTable5.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + + UsimServiceTable testTable6 = new UsimServiceTable(service89To96); + assertFalse(testTable6.isAvailable(UsimServiceTable.UsimService.PHONEBOOK)); + assertFalse(testTable6.isAvailable(UsimServiceTable.UsimService.HPLMN_DIRECT_ACCESS)); + assertTrue(testTable6.isAvailable(UsimServiceTable.UsimService.ECALL_DATA)); + assertTrue(testTable6.isAvailable(UsimServiceTable.UsimService.SM_OVER_IP)); + assertTrue(testTable6.isAvailable(UsimServiceTable.UsimService.UICC_ACCESS_TO_IMS)); + assertTrue(testTable6.isAvailable(UsimServiceTable.UsimService.NAS_CONFIG_BY_USIM)); + } +} diff --git a/tests/telephonytests/src/com/android/internal/telephony/mockril/MockRilTest.java b/tests/telephonytests/src/com/android/internal/telephony/mockril/MockRilTest.java new file mode 100644 index 0000000..3149ee1 --- /dev/null +++ b/tests/telephonytests/src/com/android/internal/telephony/mockril/MockRilTest.java @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2010 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.internal.telephony.mockril; + +import android.util.Log; +import android.test.InstrumentationTestCase; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.android.internal.communication.MsgHeader; +import com.android.internal.communication.Msg; +import com.android.internal.telephony.RilChannel; +import com.android.internal.telephony.ril_proto.RilCtrlCmds; +import com.android.internal.telephony.ril_proto.RilCmds; + +import com.android.frameworks.telephonytests.TelephonyMockRilTestRunner; +import com.google.protobuf.micro.InvalidProtocolBufferMicroException; + +// Test suite for test ril +public class MockRilTest extends InstrumentationTestCase { + private static final String TAG = "MockRilTest"; + + RilChannel mMockRilChannel; + TelephonyMockRilTestRunner mRunner; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mRunner = (TelephonyMockRilTestRunner)getInstrumentation(); + mMockRilChannel = mRunner.mMockRilChannel; + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + static void log(String s) { + Log.v(TAG, s); + } + + /** + * Test Case 1: Test protobuf serialization and deserialization + * @throws InvalidProtocolBufferMicroException + */ + public void testProtobufSerDes() throws InvalidProtocolBufferMicroException { + log("testProtobufSerdes E"); + + RilCtrlCmds.CtrlRspRadioState rs = new RilCtrlCmds.CtrlRspRadioState(); + assertTrue(String.format("expected rs.state == 0 was %d", rs.getState()), + rs.getState() == 0); + rs.setState(1); + assertTrue(String.format("expected rs.state == 1 was %d", rs.getState()), + rs.getState() == 1); + + byte[] rs_ser = rs.toByteArray(); + RilCtrlCmds.CtrlRspRadioState rsNew = RilCtrlCmds.CtrlRspRadioState.parseFrom(rs_ser); + assertTrue(String.format("expected rsNew.state == 1 was %d", rs.getState()), + rs.getState() == 1); + + log("testProtobufSerdes X"); + } + + /** + * Test case 2: Test echo command works using writeMsg & readMsg + */ + public void testEchoMsg() throws IOException { + log("testEchoMsg E"); + + MsgHeader mh = new MsgHeader(); + mh.setCmd(0); + mh.setToken(1); + mh.setStatus(2); + ByteBuffer data = ByteBuffer.allocate(3); + data.put((byte)3); + data.put((byte)4); + data.put((byte)5); + Msg.send(mMockRilChannel, mh, data); + + Msg respMsg = Msg.recv(mMockRilChannel); + assertTrue(String.format("expected mhd.header.cmd == 0 was %d",respMsg.getCmd()), + respMsg.getCmd() == 0); + assertTrue(String.format("expected mhd.header.token == 1 was %d",respMsg.getToken()), + respMsg.getToken() == 1); + assertTrue(String.format("expected mhd.header.status == 2 was %d", respMsg.getStatus()), + respMsg.getStatus() == 2); + assertTrue(String.format("expected mhd.data[0] == 3 was %d", respMsg.getData(0)), + respMsg.getData(0) == 3); + assertTrue(String.format("expected mhd.data[1] == 4 was %d", respMsg.getData(1)), + respMsg.getData(1) == 4); + assertTrue(String.format("expected mhd.data[2] == 5 was %d", respMsg.getData(2)), + respMsg.getData(2) == 5); + + log("testEchoMsg X"); + } + + /** + * Test case 3: Test get as + */ + public void testGetAs() { + log("testGetAs E"); + + // Use a message header as the protobuf data content + MsgHeader mh = new MsgHeader(); + mh.setCmd(12345); + mh.setToken(9876); + mh.setStatus(7654); + mh.setLengthData(4321); + byte[] data = mh.toByteArray(); + MsgHeader mhResult = Msg.getAs(MsgHeader.class, data); + + assertTrue(String.format("expected cmd == 12345 was %d", mhResult.getCmd()), + mhResult.getCmd() == 12345); + assertTrue(String.format("expected token == 9876 was %d", mhResult.getToken()), + mhResult.getToken() == 9876); + assertTrue(String.format("expected status == 7654 was %d", mhResult.getStatus()), + mhResult.getStatus() == 7654); + assertTrue(String.format("expected lengthData == 4321 was %d", mhResult.getLengthData()), + mhResult.getLengthData() == 4321); + + Msg msg = Msg.obtain(); + msg.setData(ByteBuffer.wrap(data)); + + mhResult = msg.getDataAs(MsgHeader.class); + + assertTrue(String.format("expected cmd == 12345 was %d", mhResult.getCmd()), + mhResult.getCmd() == 12345); + assertTrue(String.format("expected token == 9876 was %d", mhResult.getToken()), + mhResult.getToken() == 9876); + assertTrue(String.format("expected status == 7654 was %d", mhResult.getStatus()), + mhResult.getStatus() == 7654); + assertTrue(String.format("expected lengthData == 4321 was %d", mhResult.getLengthData()), + mhResult.getLengthData() == 4321); + + log("testGetAs X"); + } + + /** + * Test case 3: test get radio state + */ + public void testGetRadioState() throws IOException { + log("testGetRadioState E"); + + Msg.send(mMockRilChannel, 1, 9876, 0, null); + + Msg resp = Msg.recv(mMockRilChannel); + //resp.printHeader("testGetRadioState"); + + assertTrue(String.format("expected cmd == 1 was %d", resp.getCmd()), + resp.getCmd() == 1); + assertTrue(String.format("expected token == 9876 was %d", resp.getToken()), + resp.getToken() == 9876); + assertTrue(String.format("expected status == 0 was %d", resp.getStatus()), + resp.getStatus() == 0); + + RilCtrlCmds.CtrlRspRadioState rsp = resp.getDataAs(RilCtrlCmds.CtrlRspRadioState.class); + + int state = rsp.getState(); + log("testGetRadioState state=" + state); + assertTrue(String.format("expected RadioState >= 0 && RadioState <= 9 was %d", state), + ((state >= 0) && (state <= 9))); + + log("testGetRadioState X"); + } + + /** + * Test case 5: test set radio state + */ + public void testSetRadioState() throws IOException { + log("testSetRadioState E"); + + RilCtrlCmds.CtrlReqRadioState cmdrs = new RilCtrlCmds.CtrlReqRadioState(); + assertEquals(0, cmdrs.getState()); + + cmdrs.setState(RilCmds.RADIOSTATE_SIM_NOT_READY); + assertEquals(2, cmdrs.getState()); + + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_RADIO_STATE, 0, 0, cmdrs); + + Msg resp = Msg.recv(mMockRilChannel); + log("get response status :" + resp.getStatus()); + log("get response for command: " + resp.getCmd()); + log("get command token: " + resp.getToken()); + + RilCtrlCmds.CtrlRspRadioState rsp = resp.getDataAs(RilCtrlCmds.CtrlRspRadioState.class); + + int state = rsp.getState(); + log("get response for testSetRadioState: " + state); + assertTrue(RilCmds.RADIOSTATE_SIM_NOT_READY == state); + } + + /** + * Test case 6: test start incoming call and hangup it. + */ + public void testStartIncomingCallAndHangup() throws IOException { + log("testStartIncomingCallAndHangup"); + RilCtrlCmds.CtrlReqSetMTCall cmd = new RilCtrlCmds.CtrlReqSetMTCall(); + String incomingCall = "6502889108"; + // set the MT call + cmd.setPhoneNumber(incomingCall); + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_MT_CALL, 0, 0, cmd); + // get response + Msg resp = Msg.recv(mMockRilChannel); + log("Get response status: " + resp.getStatus()); + assertTrue("The ril is not in a proper state to set MT calls.", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + + // allow the incoming call alerting for some time + try { + Thread.sleep(5000); + } catch (InterruptedException e) {} + + // we are playing a trick to assume the current is 1 + RilCtrlCmds.CtrlHangupConnRemote hangupCmd = new RilCtrlCmds.CtrlHangupConnRemote(); + hangupCmd.setConnectionId(1); + hangupCmd.setCallFailCause(16); // normal hangup + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_HANGUP_CONN_REMOTE, 0, 0, hangupCmd); + + // get response + resp = Msg.recv(mMockRilChannel); + log("Get response for hangup connection: " + resp.getStatus()); + assertTrue("CTRL_CMD_HANGUP_CONN_REMOTE failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + } + + /** + * Test case 7: test set call transition flag + */ + public void testSetCallTransitionFlag() throws IOException { + log("testSetCallTransitionFlag"); + // Set flag to true: + RilCtrlCmds.CtrlSetCallTransitionFlag cmd = new RilCtrlCmds.CtrlSetCallTransitionFlag(); + cmd.setFlag(true); + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_CALL_TRANSITION_FLAG, 0, 0, cmd); + + Msg resp = Msg.recv(mMockRilChannel); + log("Get response status: " + resp.getStatus()); + assertTrue("Set call transition flag failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + + // add a dialing call + RilCtrlCmds.CtrlReqAddDialingCall cmdDialCall = new RilCtrlCmds.CtrlReqAddDialingCall(); + String phoneNumber = "5102345678"; + cmdDialCall.setPhoneNumber(phoneNumber); + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_ADD_DIALING_CALL, 0, 0, cmdDialCall); + resp = Msg.recv(mMockRilChannel); + log("Get response status for adding a dialing call: " + resp.getStatus()); + assertTrue("add dialing call failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + try { + Thread.sleep(5000); + } catch (InterruptedException e) {} + + // send command to force call state change + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_CALL_ALERT, 0, 0, null); + resp = Msg.recv(mMockRilChannel); + log("Get response status: " + resp.getStatus()); + assertTrue("Set call alert failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + + try { + Thread.sleep(2000); + } catch (InterruptedException e) {} + + // send command to force call state change + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_CALL_ACTIVE, 0, 0, null); + resp = Msg.recv(mMockRilChannel); + log("Get response status: " + resp.getStatus()); + assertTrue("Set call active failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + + // hangup the active all remotely + RilCtrlCmds.CtrlHangupConnRemote hangupCmd = new RilCtrlCmds.CtrlHangupConnRemote(); + hangupCmd.setConnectionId(1); + hangupCmd.setCallFailCause(16); // normal hangup + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_HANGUP_CONN_REMOTE, 0, 0, hangupCmd); + resp = Msg.recv(mMockRilChannel); + log("Get response for hangup connection: " + resp.getStatus()); + assertTrue("CTRL_CMD_HANGUP_CONN_REMOTE failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + + // set the flag to false + cmd.setFlag(false); + Msg.send(mMockRilChannel, RilCtrlCmds.CTRL_CMD_SET_CALL_TRANSITION_FLAG, 0, 0, cmd); + resp = Msg.recv(mMockRilChannel); + assertTrue("Set call transition flag failed", + resp.getStatus() == RilCtrlCmds.CTRL_STATUS_OK); + } +} |