Merge "Notify the playback status to DRM agents before the playback starts"
diff --git a/api/current.xml b/api/current.xml
index 7dc498d..69d21a5 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -55702,17 +55702,6 @@
 <parameter name="message" type="java.lang.String">
 </parameter>
 </constructor>
-<field name="TYPE_DRM_INFO_ACQUISITION_FAILED"
- type="int"
- transient="false"
- volatile="false"
- value="2008"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="TYPE_NOT_SUPPORTED"
  type="int"
  transient="false"
@@ -55846,17 +55835,6 @@
  visibility="public"
 >
 </method>
-<field name="DRM_INFO_OBJECT"
- type="java.lang.String"
- transient="false"
- volatile="false"
- value="&quot;drm_info_object&quot;"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="DRM_INFO_STATUS_OBJECT"
  type="java.lang.String"
  transient="false"
@@ -55879,17 +55857,6 @@
  visibility="public"
 >
 </field>
-<field name="TYPE_DRM_INFO_ACQUIRED"
- type="int"
- transient="false"
- volatile="false"
- value="1003"
- static="true"
- final="true"
- deprecated="not deprecated"
- visibility="public"
->
-</field>
 <field name="TYPE_DRM_INFO_PROCESSED"
  type="int"
  transient="false"
@@ -56365,6 +56332,19 @@
 </parameter>
 </constructor>
 <method name="acquireDrmInfo"
+ return="android.drm.DrmInfo"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="drmInfoRequest" type="android.drm.DrmInfoRequest">
+</parameter>
+</method>
+<method name="acquireRights"
  return="int"
  abstract="false"
  native="false"
@@ -56562,6 +56542,32 @@
 <parameter name="mimeType" type="java.lang.String">
 </parameter>
 </method>
+<method name="getMetadata"
+ return="android.content.ContentValues"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="path" type="java.lang.String">
+</parameter>
+</method>
+<method name="getMetadata"
+ return="android.content.ContentValues"
+ abstract="false"
+ native="false"
+ synchronized="false"
+ static="false"
+ final="false"
+ deprecated="not deprecated"
+ visibility="public"
+>
+<parameter name="uri" type="android.net.Uri">
+</parameter>
+</method>
 <method name="getOriginalMimeType"
  return="java.lang.String"
  abstract="false"
diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java
index 2fb746c..9a3d621 100644
--- a/core/java/android/app/SearchDialog.java
+++ b/core/java/android/app/SearchDialog.java
@@ -1107,6 +1107,9 @@
      * @return true if a successful launch, false if could not (e.g. bad position).
      */
     protected boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+        if (mSuggestionsAdapter == null) {
+            return false;
+        }
         Cursor c = mSuggestionsAdapter.getCursor();
         if ((c != null) && c.moveToPosition(position)) {
 
diff --git a/drm/common/Android.mk b/drm/common/Android.mk
index 808b2c2..c79a91a 100644
--- a/drm/common/Android.mk
+++ b/drm/common/Android.mk
@@ -18,6 +18,7 @@
 
 LOCAL_SRC_FILES:= \
     DrmConstraints.cpp \
+    DrmMetadata.cpp \
     DrmConvertedStatus.cpp \
     DrmEngineBase.cpp \
     DrmInfo.cpp \
diff --git a/drm/common/DrmEngineBase.cpp b/drm/common/DrmEngineBase.cpp
index 10c64ee..ac360eb 100644
--- a/drm/common/DrmEngineBase.cpp
+++ b/drm/common/DrmEngineBase.cpp
@@ -31,6 +31,10 @@
     return onGetConstraints(uniqueId, path, action);
 }
 
+DrmMetadata* DrmEngineBase::getMetadata(int uniqueId, const String8* path) {
+    return onGetMetadata(uniqueId, path);
+}
+
 status_t DrmEngineBase::initialize(int uniqueId) {
     return onInitialize(uniqueId);
 }
diff --git a/drm/common/DrmMetadata.cpp b/drm/common/DrmMetadata.cpp
new file mode 100644
index 0000000..6cc5ec1
--- /dev/null
+++ b/drm/common/DrmMetadata.cpp
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+#include <drm/DrmMetadata.h>
+
+using namespace android;
+
+int DrmMetadata::getCount(void) const {
+	return mMetadataMap.size();
+}
+
+status_t DrmMetadata::put(const String8* key,
+                          const char* value) {
+    if((value != NULL) && (key != NULL)) {
+        int length = strlen(value);
+        char* charValue = new char[length + 1];
+
+        memcpy(charValue, value, length);
+        charValue[length] = '\0';
+        mMetadataMap.add(*key, charValue);
+    }
+    return NO_ERROR;
+}
+
+String8 DrmMetadata::get(const String8& key) const {
+    if (NULL != getValue(&key)) {
+        return String8(getValue(&key));
+    }
+    else {
+        return String8("");
+    }
+}
+
+const char* DrmMetadata::getValue(const String8* key) const {
+    if(key != NULL) {
+        if (NAME_NOT_FOUND != mMetadataMap.indexOfKey(*key)) {
+            return mMetadataMap.valueFor(*key);
+        }
+        else {
+            return NULL;
+        }
+    } else {
+        return NULL;
+    }
+}
+
+const char* DrmMetadata::getAsByteArray(const String8* key) const {
+    return getValue(key);
+}
+
+bool DrmMetadata::KeyIterator::hasNext() {
+    return mIndex < mDrmMetadata->mMetadataMap.size();
+}
+
+const String8& DrmMetadata::KeyIterator::next() {
+    const String8& key = mDrmMetadata->mMetadataMap.keyAt(mIndex);
+    mIndex++;
+    return key;
+}
+
+DrmMetadata::KeyIterator DrmMetadata::keyIterator() {
+    return KeyIterator(this);
+}
+
+DrmMetadata::KeyIterator::KeyIterator(const DrmMetadata::KeyIterator& keyIterator) :
+    mDrmMetadata(keyIterator.mDrmMetadata),
+    mIndex(keyIterator.mIndex) {
+    LOGV("DrmMetadata::KeyIterator::KeyIterator");
+}
+
+DrmMetadata::KeyIterator& DrmMetadata::KeyIterator::operator=(const DrmMetadata::KeyIterator& keyIterator) {
+    LOGV("DrmMetadata::KeyIterator::operator=");
+    mDrmMetadata = keyIterator.mDrmMetadata;
+    mIndex = keyIterator.mIndex;
+    return *this;
+}
+
+
+DrmMetadata::Iterator DrmMetadata::iterator() {
+    return Iterator(this);
+}
+
+DrmMetadata::Iterator::Iterator(const DrmMetadata::Iterator& iterator) :
+    mDrmMetadata(iterator.mDrmMetadata),
+    mIndex(iterator.mIndex) {
+    LOGV("DrmMetadata::Iterator::Iterator");
+}
+
+DrmMetadata::Iterator& DrmMetadata::Iterator::operator=(const DrmMetadata::Iterator& iterator) {
+    LOGV("DrmMetadata::Iterator::operator=");
+    mDrmMetadata = iterator.mDrmMetadata;
+    mIndex = iterator.mIndex;
+    return *this;
+}
+
+bool DrmMetadata::Iterator::hasNext() {
+    return mIndex < mDrmMetadata->mMetadataMap.size();
+}
+
+String8 DrmMetadata::Iterator::next() {
+    String8 value = String8(mDrmMetadata->mMetadataMap.editValueAt(mIndex));
+    mIndex++;
+    return value;
+}
diff --git a/drm/common/IDrmManagerService.cpp b/drm/common/IDrmManagerService.cpp
index b8ae852..723b50e 100644
--- a/drm/common/IDrmManagerService.cpp
+++ b/drm/common/IDrmManagerService.cpp
@@ -24,6 +24,7 @@
 
 #include <drm/DrmInfo.h>
 #include <drm/DrmConstraints.h>
+#include <drm/DrmMetadata.h>
 #include <drm/DrmRights.h>
 #include <drm/DrmInfoStatus.h>
 #include <drm/DrmConvertedStatus.h>
@@ -123,6 +124,35 @@
     return drmConstraints;
 }
 
+DrmMetadata* BpDrmManagerService::getMetadata(int uniqueId, const String8* path) {
+    LOGV("Get Metadata");
+    Parcel data, reply;
+    data.writeInterfaceToken(IDrmManagerService::getInterfaceDescriptor());
+    data.writeInt32(uniqueId);
+
+    DrmMetadata* drmMetadata = NULL;
+    data.writeString8(*path);
+    remote()->transact(GET_METADATA_FROM_CONTENT, data, &reply);
+
+    if (0 != reply.dataAvail()) {
+        //Filling Drm Metadata
+        drmMetadata = new DrmMetadata();
+
+        const int size = reply.readInt32();
+        for (int index = 0; index < size; ++index) {
+            const String8 key(reply.readString8());
+            const int bufferSize = reply.readInt32();
+            char* data = NULL;
+            if (0 < bufferSize) {
+                data = new char[bufferSize];
+                reply.read(data, bufferSize);
+            }
+            drmMetadata->put(&key, data);
+        }
+    }
+    return drmMetadata;
+}
+
 bool BpDrmManagerService::canHandle(int uniqueId, const String8& path, const String8& mimeType) {
     LOGV("Can Handle");
     Parcel data, reply;
@@ -827,6 +857,38 @@
         return DRM_NO_ERROR;
     }
 
+    case GET_METADATA_FROM_CONTENT:
+    {
+        LOGV("BnDrmManagerService::onTransact :GET_METADATA_FROM_CONTENT");
+        CHECK_INTERFACE(IDrmManagerService, data, reply);
+
+        const int uniqueId = data.readInt32();
+        const String8 path = data.readString8();
+
+        DrmMetadata* drmMetadata = getMetadata(uniqueId, &path);
+        if (NULL != drmMetadata) {
+            //Filling DRM Metadata contents
+            reply->writeInt32(drmMetadata->getCount());
+
+            DrmMetadata::KeyIterator keyIt = drmMetadata->keyIterator();
+            while (keyIt.hasNext()) {
+                const String8 key = keyIt.next();
+                reply->writeString8(key);
+                const char* value = drmMetadata->getAsByteArray(&key);
+                int bufferSize = 0;
+                if (NULL != value) {
+                    bufferSize = strlen(value);
+                    reply->writeInt32(bufferSize + 1);
+                    reply->write(value, bufferSize + 1);
+                } else {
+                    reply->writeInt32(0);
+                }
+            }
+        }
+        delete drmMetadata; drmMetadata = NULL;
+        return NO_ERROR;
+    }
+
     case CAN_HANDLE:
     {
         LOGV("BnDrmManagerService::onTransact :CAN_HANDLE");
diff --git a/drm/drmserver/DrmManager.cpp b/drm/drmserver/DrmManager.cpp
index b7a035f..537791c 100644
--- a/drm/drmserver/DrmManager.cpp
+++ b/drm/drmserver/DrmManager.cpp
@@ -23,6 +23,7 @@
 #include <drm/DrmInfoEvent.h>
 #include <drm/DrmRights.h>
 #include <drm/DrmConstraints.h>
+#include <drm/DrmMetadata.h>
 #include <drm/DrmInfoStatus.h>
 #include <drm/DrmInfoRequest.h>
 #include <drm/DrmSupportInfo.h>
@@ -148,6 +149,15 @@
     return NULL;
 }
 
+DrmMetadata* DrmManager::getMetadata(int uniqueId, const String8* path) {
+    const String8 plugInId = getSupportedPlugInIdFromPath(uniqueId, *path);
+    if (EMPTY_STRING != plugInId) {
+        IDrmEngine& rDrmEngine = mPlugInManager.getPlugIn(plugInId);
+        return rDrmEngine.getMetadata(uniqueId, path);
+    }
+    return NULL;
+}
+
 status_t DrmManager::installDrmEngine(int uniqueId, const String8& absolutePath) {
     mPlugInManager.loadPlugIn(absolutePath);
 
diff --git a/drm/drmserver/DrmManagerService.cpp b/drm/drmserver/DrmManagerService.cpp
index 8cf510d..4dcfa72 100644
--- a/drm/drmserver/DrmManagerService.cpp
+++ b/drm/drmserver/DrmManagerService.cpp
@@ -18,18 +18,50 @@
 #define LOG_TAG "DrmManagerService(Native)"
 #include <utils/Log.h>
 
+#include <private/android_filesystem_config.h>
+
 #include <errno.h>
 #include <utils/threads.h>
 #include <binder/IServiceManager.h>
+#include <binder/IPCThreadState.h>
 #include <sys/stat.h>
 #include "DrmManagerService.h"
 #include "DrmManager.h"
 
 using namespace android;
 
+static Vector<uid_t> trustedUids;
+
+static bool isProtectedCallAllowed() {
+    // TODO
+    // Following implementation is just for reference.
+    // Each OEM manufacturer should implement/replace with their own solutions.
+    bool result = false;
+
+    IPCThreadState* ipcState = IPCThreadState::self();
+    uid_t uid = ipcState->getCallingUid();
+
+    for (unsigned int i = 0; i < trustedUids.size(); ++i) {
+        if (trustedUids[i] == uid) {
+            result = true;
+            break;
+        }
+    }
+    return result;
+}
+
 void DrmManagerService::instantiate() {
     LOGV("instantiate");
     defaultServiceManager()->addService(String16("drm.drmManager"), new DrmManagerService());
+
+    if (0 >= trustedUids.size()) {
+        // TODO
+        // Following implementation is just for reference.
+        // Each OEM manufacturer should implement/replace with their own solutions.
+
+        // Add trusted uids here
+        trustedUids.push(AID_MEDIA);
+    }
 }
 
 DrmManagerService::DrmManagerService() :
@@ -79,6 +111,11 @@
     return mDrmManager->getConstraints(uniqueId, path, action);
 }
 
+DrmMetadata* DrmManagerService::getMetadata(int uniqueId, const String8* path) {
+    LOGV("Entering getMetadata from content");
+    return mDrmManager->getMetadata(uniqueId, path);
+}
+
 bool DrmManagerService::canHandle(int uniqueId, const String8& path, const String8& mimeType) {
     LOGV("Entering canHandle");
     return mDrmManager->canHandle(uniqueId, path, mimeType);
@@ -172,13 +209,21 @@
 DecryptHandle* DrmManagerService::openDecryptSession(
             int uniqueId, int fd, int offset, int length) {
     LOGV("Entering DrmManagerService::openDecryptSession");
-    return mDrmManager->openDecryptSession(uniqueId, fd, offset, length);
+    if (isProtectedCallAllowed()) {
+        return mDrmManager->openDecryptSession(uniqueId, fd, offset, length);
+    }
+
+    return NULL;
 }
 
 DecryptHandle* DrmManagerService::openDecryptSession(
             int uniqueId, const char* uri) {
     LOGV("Entering DrmManagerService::openDecryptSession with uri");
-    return mDrmManager->openDecryptSession(uniqueId, uri);
+    if (isProtectedCallAllowed()) {
+        return mDrmManager->openDecryptSession(uniqueId, uri);
+    }
+
+    return NULL;
 }
 
 status_t DrmManagerService::closeDecryptSession(int uniqueId, DecryptHandle* decryptHandle) {
diff --git a/drm/java/android/drm/DrmErrorEvent.java b/drm/java/android/drm/DrmErrorEvent.java
index 9294884..20fd8aa 100644
--- a/drm/java/android/drm/DrmErrorEvent.java
+++ b/drm/java/android/drm/DrmErrorEvent.java
@@ -53,11 +53,6 @@
      * associated with all DRM schemes.
      */
     public static final int TYPE_REMOVE_ALL_RIGHTS_FAILED = 2007;
-    /**
-     * TYPE_DRM_INFO_ACQUISITION_FAILED, when failed to get the required information to
-     * communicate with the service.
-     */
-    public static final int TYPE_DRM_INFO_ACQUISITION_FAILED = 2008;
 
     /**
      * constructor to create DrmErrorEvent object with given parameters
diff --git a/drm/java/android/drm/DrmEvent.java b/drm/java/android/drm/DrmEvent.java
index 583337f..f7bc5cd1 100644
--- a/drm/java/android/drm/DrmEvent.java
+++ b/drm/java/android/drm/DrmEvent.java
@@ -31,14 +31,8 @@
      * Constant field signifies that given information is processed successfully
      */
     public static final int TYPE_DRM_INFO_PROCESSED = 1002;
-    /**
-     * Constant field signifies that the required information to communicate with
-     * the service is acquired sucessfully
-     */
-    public static final int TYPE_DRM_INFO_ACQUIRED = 1003;
 
     public static final String DRM_INFO_STATUS_OBJECT = "drm_info_status_object";
-    public static final String DRM_INFO_OBJECT = "drm_info_object";
 
     private final int mUniqueId;
     private final int mType;
diff --git a/drm/java/android/drm/DrmManagerClient.java b/drm/java/android/drm/DrmManagerClient.java
index 5044d36..2f54b33 100644
--- a/drm/java/android/drm/DrmManagerClient.java
+++ b/drm/java/android/drm/DrmManagerClient.java
@@ -102,8 +102,7 @@
     }
 
     private static final int ACTION_REMOVE_ALL_RIGHTS = 1001;
-    private static final int ACTION_ACQUIRE_DRM_INFO = 1002;
-    private static final int ACTION_PROCESS_DRM_INFO = 1003;
+    private static final int ACTION_PROCESS_DRM_INFO = 1002;
 
     private int mUniqueId;
     private int mNativeContext;
@@ -126,18 +125,6 @@
             HashMap<String, Object> attributes = new HashMap<String, Object>();
 
             switch(msg.what) {
-            case ACTION_ACQUIRE_DRM_INFO: {
-                final DrmInfoRequest request = (DrmInfoRequest) msg.obj;
-                DrmInfo drmInfo = _acquireDrmInfo(mUniqueId, request);
-                if (null != drmInfo) {
-                    attributes.put(DrmEvent.DRM_INFO_OBJECT, drmInfo);
-                    event = new DrmEvent(mUniqueId, DrmEvent.TYPE_DRM_INFO_ACQUIRED, null);
-                } else {
-                    error = new DrmErrorEvent(mUniqueId,
-                            DrmErrorEvent.TYPE_DRM_INFO_ACQUISITION_FAILED, null);
-                }
-                break;
-            }
             case ACTION_PROCESS_DRM_INFO: {
                 final DrmInfo drmInfo = (DrmInfo) msg.obj;
                 DrmInfoStatus status = _processDrmInfo(mUniqueId, drmInfo);
@@ -243,19 +230,14 @@
      */
     public DrmManagerClient(Context context) {
         mContext = context;
-        Looper looper;
 
-        if (null != (looper = Looper.myLooper())) {
-            mInfoHandler = new InfoHandler(looper);
-        } else if (null != (looper = Looper.getMainLooper())) {
-            mInfoHandler = new InfoHandler(looper);
-        } else {
-            mInfoHandler = null;
-        }
+        HandlerThread infoThread = new HandlerThread("DrmManagerClient.InfoHandler");
+        infoThread.start();
+        mInfoHandler = new InfoHandler(infoThread.getLooper());
 
-        HandlerThread thread = new HandlerThread("DrmManagerClient.EventHandler");
-        thread.start();
-        mEventHandler = new EventHandler(thread.getLooper());
+        HandlerThread eventThread = new HandlerThread("DrmManagerClient.EventHandler");
+        eventThread.start();
+        mEventHandler = new EventHandler(eventThread.getLooper());
 
         // save the unique id
         mUniqueId = hashCode();
@@ -335,10 +317,24 @@
         return _getConstraints(mUniqueId, path, action);
     }
 
+   /**
+    * Get metadata information from DRM content
+    *
+    * @param path Content path from where DRM metadata would be retrieved.
+    * @return ContentValues instance in which metadata key-value pairs are embedded
+    *         or null in case of failure
+    */
+    public ContentValues getMetadata(String path) {
+        if (null == path || path.equals("")) {
+            throw new IllegalArgumentException("Given path is invalid/null");
+        }
+        return _getMetadata(mUniqueId, path);
+    }
+
     /**
      * Get constraints information evaluated from DRM content
      *
-     * @param uri The Content URI of the data
+     * @param uri Content URI from where DRM constraints would be retrieved.
      * @param action Actions defined in {@link DrmStore.Action}
      * @return ContentValues instance in which constraints key-value pairs are embedded
      *         or null in case of failure
@@ -350,6 +346,20 @@
         return getConstraints(convertUriToPath(uri), action);
     }
 
+   /**
+    * Get metadata information from DRM content
+    *
+    * @param uri Content URI from where DRM metadata would be retrieved.
+    * @return ContentValues instance in which metadata key-value pairs are embedded
+    *         or null in case of failure
+    */
+    public ContentValues getMetadata(Uri uri) {
+        if (null == uri || Uri.EMPTY == uri) {
+            throw new IllegalArgumentException("Uri should be non null");
+        }
+        return getMetadata(convertUriToPath(uri));
+    }
+
     /**
      * Save DRM rights to specified rights path
      * and make association with content path.
@@ -408,7 +418,7 @@
     /**
      * Check whether the given mimetype or uri can be handled.
      *
-     * @param uri The content URI of the data
+     * @param uri Content URI of the data to be handled.
      * @param mimeType Mimetype of the object to be handled
      * @return
      *        true - if the given mimeType or path can be handled
@@ -445,20 +455,31 @@
      * Retrieves necessary information for register, unregister or rights acquisition.
      *
      * @param drmInfoRequest Request information to retrieve drmInfo
+     * @return DrmInfo Instance as a result of processing given input
+     */
+    public DrmInfo acquireDrmInfo(DrmInfoRequest drmInfoRequest) {
+        if (null == drmInfoRequest || !drmInfoRequest.isValid()) {
+            throw new IllegalArgumentException("Given drmInfoRequest is invalid/null");
+        }
+        return _acquireDrmInfo(mUniqueId, drmInfoRequest);
+    }
+
+    /**
+     * Executes given DrmInfoRequest and returns the rights information asynchronously.
+     * This is a utility API which consists of {@link #acquireDrmInfo(DrmInfoRequest)}
+     * and {@link #processDrmInfo(DrmInfo)}.
+     * It can be used if selected DRM agent can work with this combined sequences.
+     * In case of some DRM schemes, such as OMA DRM, application needs to invoke
+     * {@link #acquireDrmInfo(DrmInfoRequest)} and {@link #processDrmInfo(DrmInfo)}, separately.
+     *
+     * @param drmInfoRequest Request information to retrieve drmInfo
      * @return
      *     ERROR_NONE for success
      *     ERROR_UNKNOWN for failure
      */
-    public int acquireDrmInfo(DrmInfoRequest drmInfoRequest) {
-        if (null == drmInfoRequest || !drmInfoRequest.isValid()) {
-            throw new IllegalArgumentException("Given drmInfoRequest is invalid/null");
-        }
-        int result = ERROR_UNKNOWN;
-        if (null != mEventHandler) {
-            Message msg = mEventHandler.obtainMessage(ACTION_ACQUIRE_DRM_INFO, drmInfoRequest);
-            result = (mEventHandler.sendMessage(msg)) ? ERROR_NONE : result;
-        }
-        return result;
+    public int acquireRights(DrmInfoRequest drmInfoRequest) {
+        DrmInfo drmInfo = acquireDrmInfo(drmInfoRequest);
+        return processDrmInfo(drmInfo);
     }
 
     /**
@@ -750,6 +771,8 @@
 
     private native ContentValues _getConstraints(int uniqueId, String path, int usage);
 
+    private native ContentValues _getMetadata(int uniqueId, String path);
+
     private native boolean _canHandle(int uniqueId, String path, String mimeType);
 
     private native DrmInfoStatus _processDrmInfo(int uniqueId, DrmInfo drmInfo);
diff --git a/drm/jni/android_drm_DrmManagerClient.cpp b/drm/jni/android_drm_DrmManagerClient.cpp
index e5e4547..e131839 100644
--- a/drm/jni/android_drm_DrmManagerClient.cpp
+++ b/drm/jni/android_drm_DrmManagerClient.cpp
@@ -29,6 +29,7 @@
 #include <drm/DrmInfoRequest.h>
 #include <drm/DrmSupportInfo.h>
 #include <drm/DrmConstraints.h>
+#include <drm/DrmMetadata.h>
 #include <drm/DrmConvertedStatus.h>
 #include <drm/drm_framework_common.h>
 
@@ -298,6 +299,43 @@
     return constraints;
 }
 
+static jobject android_drm_DrmManagerClient_getMetadataFromContent(
+            JNIEnv* env, jobject thiz, jint uniqueId, jstring jpath) {
+    LOGV("GetMetadata - Enter");
+    const String8 pathString = Utility::getStringValue(env, jpath);
+    DrmMetadata* pMetadata =
+            getDrmManagerClientImpl(env, thiz)->getMetadata(uniqueId, &pathString);
+
+    jobject metadata = NULL;
+
+    jclass localRef = NULL;
+    localRef = env->FindClass("android/content/ContentValues");
+    if (NULL != localRef && NULL != pMetadata) {
+        // Get the constructor id
+        jmethodID constructorId = NULL;
+        constructorId = env->GetMethodID(localRef, "<init>", "()V");
+        if (NULL != constructorId) {
+            // create the java DrmMetadata object
+            metadata = env->NewObject(localRef, constructorId);
+            if (NULL != metadata) {
+                DrmMetadata::KeyIterator keyIt = pMetadata->keyIterator();
+                while (keyIt.hasNext()) {
+                    String8 key = keyIt.next();
+                    // insert the entry<constraintKey, constraintValue>
+                    // to newly created java object
+                    String8 value = pMetadata->get(key);
+                    env->CallVoidMethod(metadata, env->GetMethodID(localRef, "put",
+                            "(Ljava/lang/String;Ljava/lang/String;)V"),
+                    env->NewStringUTF(key.string()), env->NewStringUTF(value.string()));
+                }
+            }
+        }
+    }
+    delete pMetadata; pMetadata = NULL;
+    LOGV("GetMetadata - Exit");
+    return metadata;
+}
+
 static jobjectArray android_drm_DrmManagerClient_getAllSupportInfo(
             JNIEnv* env, jobject thiz, jint uniqueId) {
     LOGV("GetAllSupportInfo - Enter");
@@ -682,6 +720,9 @@
     {"_getConstraints", "(ILjava/lang/String;I)Landroid/content/ContentValues;",
                                     (void*)android_drm_DrmManagerClient_getConstraintsFromContent},
 
+    {"_getMetadata", "(ILjava/lang/String;)Landroid/content/ContentValues;",
+                                    (void*)android_drm_DrmManagerClient_getMetadataFromContent},
+
     {"_getAllSupportInfo", "(I)[Landroid/drm/DrmSupportInfo;",
                                     (void*)android_drm_DrmManagerClient_getAllSupportInfo},
 
diff --git a/drm/libdrmframework/DrmManagerClient.cpp b/drm/libdrmframework/DrmManagerClient.cpp
index f0439eb..fa3d52a 100644
--- a/drm/libdrmframework/DrmManagerClient.cpp
+++ b/drm/libdrmframework/DrmManagerClient.cpp
@@ -43,6 +43,10 @@
     return mDrmManagerClientImpl->getConstraints(mUniqueId, path, action);
 }
 
+DrmMetadata* DrmManagerClient::getMetadata(const String8* path) {
+    return mDrmManagerClientImpl->getMetadata(mUniqueId, path);
+}
+
 bool DrmManagerClient::canHandle(const String8& path, const String8& mimeType) {
     return mDrmManagerClientImpl->canHandle(mUniqueId, path, mimeType);
 }
diff --git a/drm/libdrmframework/DrmManagerClientImpl.cpp b/drm/libdrmframework/DrmManagerClientImpl.cpp
index b3ae9a7..32fa491 100644
--- a/drm/libdrmframework/DrmManagerClientImpl.cpp
+++ b/drm/libdrmframework/DrmManagerClientImpl.cpp
@@ -101,6 +101,14 @@
     return drmConstraints;
 }
 
+DrmMetadata* DrmManagerClientImpl::getMetadata(int uniqueId, const String8* path) {
+    DrmMetadata *drmMetadata = NULL;
+    if ((NULL != path) && (EMPTY_STRING != *path)) {
+        drmMetadata = getDrmManagerService()->getMetadata(uniqueId, path);
+    }
+    return drmMetadata;
+}
+
 bool DrmManagerClientImpl::canHandle(int uniqueId, const String8& path, const String8& mimeType) {
     bool retCode = false;
     if ((EMPTY_STRING != path) || (EMPTY_STRING != mimeType)) {
diff --git a/drm/libdrmframework/include/DrmManager.h b/drm/libdrmframework/include/DrmManager.h
index d782f5b..bc462c2 100644
--- a/drm/libdrmframework/include/DrmManager.h
+++ b/drm/libdrmframework/include/DrmManager.h
@@ -32,6 +32,7 @@
 class DrmRightsAcquisitionInfo;
 class DrmContentIds;
 class DrmConstraints;
+class DrmMetadata;
 class DrmRights;
 class DrmInfo;
 class DrmInfoStatus;
@@ -74,6 +75,8 @@
 
     DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action);
 
+    DrmMetadata* getMetadata(int uniqueId, const String8* path);
+
     bool canHandle(int uniqueId, const String8& path, const String8& mimeType);
 
     DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo);
diff --git a/drm/libdrmframework/include/DrmManagerClientImpl.h b/drm/libdrmframework/include/DrmManagerClientImpl.h
index 1c6be46..ff84fc7 100644
--- a/drm/libdrmframework/include/DrmManagerClientImpl.h
+++ b/drm/libdrmframework/include/DrmManagerClientImpl.h
@@ -86,6 +86,18 @@
     DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action);
 
     /**
+     * Get metadata information associated with input content.
+     *
+     * @param[in] uniqueId Unique identifier for a session
+     * @param[in] path Path of the protected content
+     * @return DrmMetadata
+     *         key-value pairs of metadata are embedded in it
+     * @note
+     *    In case of error, return NULL
+     */
+    DrmMetadata* getMetadata(int uniqueId, const String8* path);
+
+    /**
      * Check whether the given mimetype or path can be handled
      *
      * @param[in] uniqueId Unique identifier for a session
diff --git a/drm/libdrmframework/include/DrmManagerService.h b/drm/libdrmframework/include/DrmManagerService.h
index 4a3aeae..f346356 100644
--- a/drm/libdrmframework/include/DrmManagerService.h
+++ b/drm/libdrmframework/include/DrmManagerService.h
@@ -61,6 +61,8 @@
 
     DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action);
 
+    DrmMetadata* getMetadata(int uniqueId, const String8* path);
+
     bool canHandle(int uniqueId, const String8& path, const String8& mimeType);
 
     DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo);
diff --git a/drm/libdrmframework/include/IDrmManagerService.h b/drm/libdrmframework/include/IDrmManagerService.h
index 1275488..f1dabd3 100644
--- a/drm/libdrmframework/include/IDrmManagerService.h
+++ b/drm/libdrmframework/include/IDrmManagerService.h
@@ -27,6 +27,7 @@
 
 class DrmContentIds;
 class DrmConstraints;
+class DrmMetadata;
 class DrmRights;
 class DrmInfo;
 class DrmInfoStatus;
@@ -51,6 +52,7 @@
         SET_DRM_SERVICE_LISTENER,
         INSTALL_DRM_ENGINE,
         GET_CONSTRAINTS_FROM_CONTENT,
+        GET_METADATA_FROM_CONTENT,
         CAN_HANDLE,
         PROCESS_DRM_INFO,
         ACQUIRE_DRM_INFO,
@@ -96,6 +98,8 @@
     virtual DrmConstraints* getConstraints(
             int uniqueId, const String8* path, const int action) = 0;
 
+    virtual DrmMetadata* getMetadata(int uniqueId, const String8* path) = 0;
+
     virtual bool canHandle(int uniqueId, const String8& path, const String8& mimeType) = 0;
 
     virtual DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo) = 0;
@@ -179,6 +183,8 @@
 
     virtual DrmConstraints* getConstraints(int uniqueId, const String8* path, const int action);
 
+    virtual DrmMetadata* getMetadata(int uniqueId, const String8* path);
+
     virtual bool canHandle(int uniqueId, const String8& path, const String8& mimeType);
 
     virtual DrmInfoStatus* processDrmInfo(int uniqueId, const DrmInfo* drmInfo);
diff --git a/drm/libdrmframework/plugins/common/include/DrmEngineBase.h b/drm/libdrmframework/plugins/common/include/DrmEngineBase.h
index 5851af5..67b63552 100644
--- a/drm/libdrmframework/plugins/common/include/DrmEngineBase.h
+++ b/drm/libdrmframework/plugins/common/include/DrmEngineBase.h
@@ -36,6 +36,8 @@
 public:
     DrmConstraints* getConstraints(int uniqueId, const String8* path, int action);
 
+    DrmMetadata* getMetadata(int uniqueId, const String8* path);
+
     status_t initialize(int uniqueId);
 
     status_t setOnInfoListener(int uniqueId, const IDrmEngine::OnInfoListener* infoListener);
@@ -117,6 +119,18 @@
             int uniqueId, const String8* path, int action) = 0;
 
     /**
+     * Get metadata information associated with input content
+     *
+     * @param[in] uniqueId Unique identifier for a session
+     * @param[in] path Path of the protected content
+     * @return DrmMetadata
+     *         key-value pairs of metadata
+     * @note
+     *     In case of error, return NULL
+     */
+    virtual DrmMetadata* onGetMetadata(int uniqueId, const String8* path) = 0;
+
+    /**
      * Initialize plug-in
      *
      * @param[in] uniqueId Unique identifier for a session
diff --git a/drm/libdrmframework/plugins/common/include/IDrmEngine.h b/drm/libdrmframework/plugins/common/include/IDrmEngine.h
index cc03ef2..f839070 100644
--- a/drm/libdrmframework/plugins/common/include/IDrmEngine.h
+++ b/drm/libdrmframework/plugins/common/include/IDrmEngine.h
@@ -23,6 +23,7 @@
 
 class DrmContentIds;
 class DrmConstraints;
+class DrmMetadata;
 class DrmRights;
 class DrmInfo;
 class DrmInfoStatus;
@@ -105,6 +106,18 @@
             int uniqueId, const String8* path, int action) = 0;
 
     /**
+     * Get metadata information associated with input content
+     *
+     * @param[in] uniqueId Unique identifier for a session
+     * @param[in] path Path of the protected content
+     * @return DrmMetadata
+     *         key-value pairs of metadata
+     * @note
+     *      In case of error, return NULL
+     */
+    virtual DrmMetadata* getMetadata(int uniqueId, const String8* path) = 0;
+
+    /**
      * Get whether the given content can be handled by this plugin or not
      *
      * @param[in] uniqueId Unique identifier for a session
diff --git a/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h b/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h
index ddb7fd3..bbcd9ed 100644
--- a/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h
+++ b/drm/libdrmframework/plugins/passthru/include/DrmPassthruPlugIn.h
@@ -30,6 +30,8 @@
 protected:
     DrmConstraints* onGetConstraints(int uniqueId, const String8* path, int action);
 
+    DrmMetadata* onGetMetadata(int uniqueId, const String8* path);
+
     status_t onInitialize(int uniqueId);
 
     status_t onSetOnInfoListener(int uniqueId, const IDrmEngine::OnInfoListener* infoListener);
diff --git a/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp b/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp
index 41f8e91..dee1fdb 100644
--- a/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp
+++ b/drm/libdrmframework/plugins/passthru/src/DrmPassthruPlugIn.cpp
@@ -20,6 +20,7 @@
 
 #include <drm/DrmRights.h>
 #include <drm/DrmConstraints.h>
+#include <drm/DrmMetadata.h>
 #include <drm/DrmInfo.h>
 #include <drm/DrmInfoEvent.h>
 #include <drm/DrmInfoStatus.h>
@@ -51,6 +52,10 @@
 
 }
 
+DrmMetadata* DrmPassthruPlugIn::onGetMetadata(int uniqueId, const String8* path) {
+    return NULL;
+}
+
 DrmConstraints* DrmPassthruPlugIn::onGetConstraints(
         int uniqueId, const String8* path, int action) {
     LOGD("DrmPassthruPlugIn::onGetConstraints From Path: %d", uniqueId);
diff --git a/include/drm/DrmManagerClient.h b/include/drm/DrmManagerClient.h
index 5963c42..3dbfbe2 100644
--- a/include/drm/DrmManagerClient.h
+++ b/include/drm/DrmManagerClient.h
@@ -25,6 +25,7 @@
 
 class DrmInfo;
 class DrmRights;
+class DrmMetadata;
 class DrmInfoEvent;
 class DrmInfoStatus;
 class DrmInfoRequest;
@@ -204,6 +205,17 @@
     DrmConstraints* getConstraints(const String8* path, const int action);
 
     /**
+     * Get metadata information associated with input content
+     *
+     * @param[in] path Path of the protected content
+     * @return DrmMetadata
+     *         key-value pairs of metadata
+     * @note
+     *     In case of error, return NULL
+     */
+    DrmMetadata* getMetadata(const String8* path);
+
+    /**
      * Check whether the given mimetype or path can be handled
      *
      * @param[in] path Path of the content needs to be handled
diff --git a/include/drm/DrmMetadata.h b/include/drm/DrmMetadata.h
new file mode 100644
index 0000000..2c7538a
--- /dev/null
+++ b/include/drm/DrmMetadata.h
@@ -0,0 +1,111 @@
+/*
+ * 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.
+ */
+
+#ifndef __DRM_METADATA_H__
+#define __DRM_METADATA_H__
+
+#include "drm_framework_common.h"
+
+namespace android {
+
+/**
+ * This is an utility class which contains the constraints information.
+ *
+ * As a result of DrmManagerClient::getMetadata(const String8*)
+ * an instance of DrmMetadata would be returned.
+ */
+class DrmMetadata {
+public:
+    /**
+     * Iterator for key
+     */
+    class KeyIterator {
+        friend class DrmMetadata;
+    private:
+        KeyIterator(DrmMetadata* drmMetadata) : mDrmMetadata(drmMetadata), mIndex(0) {}
+
+    public:
+        KeyIterator(const KeyIterator& keyIterator);
+        KeyIterator& operator=(const KeyIterator& keyIterator);
+        virtual ~KeyIterator() {}
+
+    public:
+        bool hasNext();
+        const String8& next();
+
+    private:
+        DrmMetadata* mDrmMetadata;
+        unsigned int mIndex;
+    };
+
+    /**
+     * Iterator for constraints
+     */
+    class Iterator {
+        friend class DrmMetadata;
+    private:
+        Iterator(DrmMetadata* drmMetadata) : mDrmMetadata(drmMetadata), mIndex(0) {}
+
+    public:
+        Iterator(const Iterator& iterator);
+        Iterator& operator=(const Iterator& iterator);
+        virtual ~Iterator() {}
+
+    public:
+        bool hasNext();
+        String8 next();
+
+    private:
+        DrmMetadata* mDrmMetadata;
+        unsigned int mIndex;
+    };
+
+public:
+    DrmMetadata() {}
+    virtual ~DrmMetadata() {
+        DrmMetadata::KeyIterator keyIt = this->keyIterator();
+
+        while (keyIt.hasNext()) {
+            String8 key = keyIt.next();
+            const char* value = this->getAsByteArray(&key);
+            if (NULL != value) {
+                delete[] value;
+                value = NULL;
+            }
+        }
+        mMetadataMap.clear();
+    }
+
+public:
+    int getCount(void) const;
+    status_t put(const String8* key, const char* value);
+    String8 get(const String8& key) const;
+    const char* getAsByteArray(const String8* key) const;
+    KeyIterator keyIterator();
+    Iterator iterator();
+
+private:
+    const char* getValue(const String8* key) const;
+
+private:
+    typedef KeyedVector<String8, const char*> DrmMetadataMap;
+    DrmMetadataMap mMetadataMap;
+};
+
+};
+
+#endif /* __DRM_METADATA_H__ */
+
diff --git a/telephony/java/android/telephony/SmsCbMessage.java b/telephony/java/android/telephony/SmsCbMessage.java
new file mode 100644
index 0000000..3543275
--- /dev/null
+++ b/telephony/java/android/telephony/SmsCbMessage.java
@@ -0,0 +1,267 @@
+/*
+ * 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 com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.gsm.SmsCbHeader;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Describes an SMS-CB message.
+ *
+ * {@hide}
+ */
+public class SmsCbMessage {
+
+    /**
+     * Cell wide immediate geographical scope
+     */
+    public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0;
+
+    /**
+     * PLMN wide geographical scope
+     */
+    public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1;
+
+    /**
+     * Location / service area wide geographical scope
+     */
+    public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2;
+
+    /**
+     * Cell wide geographical scope
+     */
+    public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3;
+
+    /**
+     * Create an instance of this class from a received PDU
+     *
+     * @param pdu PDU bytes
+     * @return An instance of this class, or null if invalid pdu
+     */
+    public static SmsCbMessage createFromPdu(byte[] pdu) {
+        try {
+            return new SmsCbMessage(pdu);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 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 SmsCbHeader mHeader;
+
+    private String mLanguage;
+
+    private String mBody;
+
+    private SmsCbMessage(byte[] pdu) throws IllegalArgumentException {
+        mHeader = new SmsCbHeader(pdu);
+        parseBody(pdu);
+    }
+
+    /**
+     * Return the geographical scope of this message, one of
+     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE},
+     * {@link #GEOGRAPHICAL_SCOPE_PLMN_WIDE},
+     * {@link #GEOGRAPHICAL_SCOPE_LA_WIDE},
+     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE}
+     *
+     * @return Geographical scope
+     */
+    public int getGeographicalScope() {
+        return mHeader.geographicalScope;
+    }
+
+    /**
+     * 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 identifier of this message (0-65535)
+     *
+     * @return Message identifier
+     */
+    public int getMessageIdentifier() {
+        return mHeader.messageIdentifier;
+    }
+
+    /**
+     * Get the message code of this message (0-1023)
+     *
+     * @return Message code
+     */
+    public int getMessageCode() {
+        return mHeader.messageCode;
+    }
+
+    /**
+     * Get the update number of this message (0-15)
+     *
+     * @return Update number
+     */
+    public int getUpdateNumber() {
+        return mHeader.updateNumber;
+    }
+
+    private void parseBody(byte[] pdu) {
+        int encoding;
+        boolean hasLanguageIndicator = false;
+
+        // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
+        // section 5.
+        switch ((mHeader.dataCodingScheme & 0xf0) >> 4) {
+            case 0x00:
+                encoding = SmsMessage.ENCODING_7BIT;
+                mLanguage = LANGUAGE_CODES_GROUP_0[mHeader.dataCodingScheme & 0x0f];
+                break;
+
+            case 0x01:
+                hasLanguageIndicator = true;
+                if ((mHeader.dataCodingScheme & 0x0f) == 0x01) {
+                    encoding = SmsMessage.ENCODING_16BIT;
+                } else {
+                    encoding = SmsMessage.ENCODING_7BIT;
+                }
+                break;
+
+            case 0x02:
+                encoding = SmsMessage.ENCODING_7BIT;
+                mLanguage = LANGUAGE_CODES_GROUP_2[mHeader.dataCodingScheme & 0x0f];
+                break;
+
+            case 0x03:
+                encoding = SmsMessage.ENCODING_7BIT;
+                break;
+
+            case 0x04:
+            case 0x05:
+                switch ((mHeader.dataCodingScheme & 0x0c) >> 2) {
+                    case 0x01:
+                        encoding = SmsMessage.ENCODING_8BIT;
+                        break;
+
+                    case 0x02:
+                        encoding = SmsMessage.ENCODING_16BIT;
+                        break;
+
+                    case 0x00:
+                    default:
+                        encoding = SmsMessage.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
+                encoding = SmsMessage.ENCODING_UNKNOWN;
+                break;
+
+            case 0x0f:
+                if (((mHeader.dataCodingScheme & 0x04) >> 2) == 0x01) {
+                    encoding = SmsMessage.ENCODING_8BIT;
+                } else {
+                    encoding = SmsMessage.ENCODING_7BIT;
+                }
+                break;
+
+            default:
+                // Reserved values are to be treated as 7-bit
+                encoding = SmsMessage.ENCODING_7BIT;
+                break;
+        }
+
+        switch (encoding) {
+            case SmsMessage.ENCODING_7BIT:
+                mBody = GsmAlphabet.gsm7BitPackedToString(pdu, SmsCbHeader.PDU_HEADER_LENGTH,
+                        (pdu.length - SmsCbHeader.PDU_HEADER_LENGTH) * 8 / 7);
+
+                if (hasLanguageIndicator && mBody != null && mBody.length() > 2) {
+                    mLanguage = mBody.substring(0, 2);
+                    mBody = mBody.substring(3);
+                }
+                break;
+
+            case SmsMessage.ENCODING_16BIT:
+                int offset = SmsCbHeader.PDU_HEADER_LENGTH;
+
+                if (hasLanguageIndicator && pdu.length >= SmsCbHeader.PDU_HEADER_LENGTH + 2) {
+                    mLanguage = GsmAlphabet.gsm7BitPackedToString(pdu,
+                            SmsCbHeader.PDU_HEADER_LENGTH, 2);
+                    offset += 2;
+                }
+
+                try {
+                    mBody = new String(pdu, offset, (pdu.length & 0xfffe) - offset, "utf-16");
+                } catch (UnsupportedEncodingException e) {
+                    // Eeeek
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        if (mBody != null) {
+            // Remove trailing carriage return
+            for (int i = mBody.length() - 1; i >= 0; i--) {
+                if (mBody.charAt(i) != CARRIAGE_RETURN) {
+                    mBody = mBody.substring(0, i + 1);
+                    break;
+                }
+            }
+        } else {
+            mBody = "";
+        }
+    }
+}
diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java
index f5e9751..0ecd854 100644
--- a/telephony/java/android/telephony/SmsManager.java
+++ b/telephony/java/android/telephony/SmsManager.java
@@ -341,7 +341,67 @@
         }
 
         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;
+    }
 
     /**
      * Create a list of <code>SmsMessage</code>s from a list of RawSmsData
diff --git a/telephony/java/com/android/internal/telephony/ISms.aidl b/telephony/java/com/android/internal/telephony/ISms.aidl
index 65bad96..90de5e1 100644
--- a/telephony/java/com/android/internal/telephony/ISms.aidl
+++ b/telephony/java/com/android/internal/telephony/ISms.aidl
@@ -144,4 +144,30 @@
             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);
+
 }
diff --git a/telephony/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java b/telephony/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java
index 1910a9c..5049249 100644
--- a/telephony/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java
+++ b/telephony/java/com/android/internal/telephony/IccSmsInterfaceManagerProxy.java
@@ -68,4 +68,12 @@
                 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);
+    }
+
 }
diff --git a/telephony/java/com/android/internal/telephony/SMSDispatcher.java b/telephony/java/com/android/internal/telephony/SMSDispatcher.java
index ca526a5..55e8450 100644
--- a/telephony/java/com/android/internal/telephony/SMSDispatcher.java
+++ b/telephony/java/com/android/internal/telephony/SMSDispatcher.java
@@ -116,6 +116,9 @@
     /** Radio is ON */
     static final protected int EVENT_RADIO_ON = 12;
 
+    /** New broadcast SMS */
+    static final protected int EVENT_NEW_BROADCAST_SMS = 13;
+
     protected Phone mPhone;
     protected Context mContext;
     protected ContentResolver mResolver;
@@ -399,6 +402,9 @@
                 mCm.reportSmsMemoryStatus(mStorageAvailable,
                         obtainMessage(EVENT_REPORT_MEMORY_STATUS_DONE));
             }
+
+        case EVENT_NEW_BROADCAST_SMS:
+            handleBroadcastSms((AsyncResult)msg.obj);
             break;
         }
     }
@@ -995,4 +1001,17 @@
             }
 
         };
+
+    protected abstract void handleBroadcastSms(AsyncResult ar);
+
+    protected void dispatchBroadcastPdus(byte[][] pdus) {
+        Intent intent = new Intent("android.provider.telephony.SMS_CB_RECEIVED");
+        intent.putExtra("pdus", pdus);
+
+        if (Config.LOGD)
+            Log.d(TAG, "Dispatching " + pdus.length + " SMS CB pdus");
+
+        dispatch(intent, "android.permission.RECEIVE_SMS");
+    }
+
 }
diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
index ed93aea..8b2ea9b 100644
--- a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
+++ b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
@@ -488,6 +488,11 @@
         mCm.setCdmaBroadcastConfig(configValuesArray, response);
     }
 
+    protected void handleBroadcastSms(AsyncResult ar) {
+        // Not supported
+        Log.e(TAG, "Error! Not implemented for CDMA.");
+    }
+
     private int resultToCause(int rc) {
         switch (rc) {
         case Activity.RESULT_OK:
diff --git a/telephony/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java b/telephony/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java
index cfcfd98..422c1e2 100644
--- a/telephony/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java
+++ b/telephony/java/com/android/internal/telephony/cdma/RuimSmsInterfaceManager.java
@@ -191,6 +191,18 @@
         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;
+    }
+
     protected void log(String msg) {
         Log.d(LOG_TAG, "[RuimSmsInterfaceManager] " + msg);
     }
diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
index d720516..438c811 100644
--- a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
+++ b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
@@ -22,21 +22,27 @@
 import android.content.Intent;
 import android.os.AsyncResult;
 import android.os.Message;
+import android.os.SystemProperties;
 import android.provider.Telephony.Sms.Intents;
 import android.telephony.ServiceState;
+import android.telephony.SmsCbMessage;
+import android.telephony.gsm.GsmCellLocation;
 import android.util.Config;
 import android.util.Log;
 
 import com.android.internal.telephony.IccUtils;
 import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails;
 import com.android.internal.telephony.gsm.SmsMessage;
+import com.android.internal.telephony.BaseCommands;
 import com.android.internal.telephony.CommandsInterface;
 import com.android.internal.telephony.SMSDispatcher;
 import com.android.internal.telephony.SmsHeader;
 import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.TelephonyProperties;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 
 import static android.telephony.SmsMessage.MessageClass;
 
@@ -48,6 +54,8 @@
     GsmSMSDispatcher(GSMPhone phone) {
         super(phone);
         mGsmPhone = phone;
+
+        ((BaseCommands)mCm).setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null);
     }
 
     /**
@@ -384,4 +392,162 @@
                 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 String mPlmn;
+
+        private final int mLac;
+
+        private final int mCid;
+
+        public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) {
+            mHeader = header;
+            mPlmn = plmn;
+            mLac = lac;
+            mCid = cid;
+        }
+
+        @Override
+        public int hashCode() {
+            return mHeader.messageIdentifier * 31 + mHeader.updateNumber;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof SmsCbConcatInfo) {
+                SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
+
+                // Two pages match if all header attributes (except the page
+                // index) are identical, and both pages belong to the same
+                // location (which is also determined by the scope parameter)
+                if (mHeader.geographicalScope == other.mHeader.geographicalScope
+                        && mHeader.messageCode == other.mHeader.messageCode
+                        && mHeader.updateNumber == other.mHeader.updateNumber
+                        && mHeader.messageIdentifier == other.mHeader.messageIdentifier
+                        && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme
+                        && mHeader.nrOfPages == other.mHeader.nrOfPages) {
+                    return matchesLocation(other.mPlmn, other.mLac, other.mCid);
+                }
+            }
+
+            return false;
+        }
+
+        /**
+         * Checks if this concatenation info matches the given location. The
+         * granularity of the match depends on the geographical scope.
+         *
+         * @param plmn PLMN
+         * @param lac Location area code
+         * @param cid Cell ID
+         * @return true if matching, false otherwise
+         */
+        public boolean matchesLocation(String plmn, int lac, int cid) {
+            switch (mHeader.geographicalScope) {
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                    if (mCid != cid) {
+                        return false;
+                    }
+                    // deliberate fall-through
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
+                    if (mLac != lac) {
+                        return false;
+                    }
+                    // deliberate fall-through
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                    return mPlmn != null && mPlmn.equals(plmn);
+            }
+
+            return false;
+        }
+    }
+
+    // This map holds incomplete concatenated messages waiting for assembly
+    private HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
+            new HashMap<SmsCbConcatInfo, byte[][]>();
+
+    protected void handleBroadcastSms(AsyncResult ar) {
+        try {
+            byte[][] pdus = null;
+            byte[] receivedPdu = (byte[])ar.result;
+
+            if (Config.LOGD) {
+                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)mGsmPhone.getCellLocation();
+            int lac = cellLocation.getLac();
+            int cid = cellLocation.getCid();
+
+            if (header.nrOfPages > 1) {
+                // Multi-page message
+                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid);
+
+                // Try to find other pages of the same message
+                pdus = mSmsCbPageMap.get(concatInfo);
+
+                if (pdus == null) {
+                    // This it the first page of this message, make room for all
+                    // pages and keep until complete
+                    pdus = new byte[header.nrOfPages][];
+
+                    mSmsCbPageMap.put(concatInfo, pdus);
+                }
+
+                // Page parameter is one-based
+                pdus[header.pageIndex - 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;
+            }
+
+            dispatchBroadcastPdus(pdus);
+
+            // 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/telephony/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java b/telephony/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java
index 2028ca4..a5e8378 100644
--- a/telephony/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java
+++ b/telephony/java/com/android/internal/telephony/gsm/SimSmsInterfaceManager.java
@@ -17,7 +17,9 @@
 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;
@@ -29,7 +31,10 @@
 
 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;
 
@@ -44,9 +49,15 @@
     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 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
@@ -74,6 +85,14 @@
                         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;
             }
         }
     };
@@ -191,6 +210,126 @@
         return mSms;
     }
 
+    public boolean enableCellBroadcast(int messageIdentifier) {
+        if (DBG) log("enableCellBroadcast");
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Enabling cell broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+        HashSet<String> clients = mCellBroadcastSubscriptions.get(messageIdentifier);
+
+        if (clients == null) {
+            // This is a new message identifier
+            clients = new HashSet<String>();
+            mCellBroadcastSubscriptions.put(messageIdentifier, clients);
+
+            if (!updateCellBroadcastConfig()) {
+                mCellBroadcastSubscriptions.remove(messageIdentifier);
+                return false;
+            }
+        }
+
+        clients.add(client);
+
+        if (DBG)
+            log("Added cell broadcast subscription for MID " + messageIdentifier
+                    + " from client " + client);
+
+        return true;
+    }
+
+    public boolean disableCellBroadcast(int messageIdentifier) {
+        if (DBG) log("disableCellBroadcast");
+
+        Context context = mPhone.getContext();
+
+        context.enforceCallingPermission(
+                "android.permission.RECEIVE_SMS",
+                "Disabling cell broadcast SMS");
+
+        String client = context.getPackageManager().getNameForUid(
+                Binder.getCallingUid());
+        HashSet<String> clients = mCellBroadcastSubscriptions.get(messageIdentifier);
+
+        if (clients != null && clients.remove(client)) {
+            if (DBG)
+                log("Removed cell broadcast subscription for MID " + messageIdentifier
+                        + " from client " + client);
+
+            if (clients.isEmpty()) {
+                mCellBroadcastSubscriptions.remove(messageIdentifier);
+                updateCellBroadcastConfig();
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean updateCellBroadcastConfig() {
+        Set<Integer> messageIdentifiers = mCellBroadcastSubscriptions.keySet();
+
+        if (messageIdentifiers.size() > 0) {
+            SmsBroadcastConfigInfo[] configs =
+                    new SmsBroadcastConfigInfo[messageIdentifiers.size()];
+            int i = 0;
+
+            for (int messageIdentifier : messageIdentifiers) {
+                configs[i++] = new SmsBroadcastConfigInfo(messageIdentifier, messageIdentifier,
+                        SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, true);
+            }
+
+            return setCellBroadcastConfig(configs) && setCellBroadcastActivation(true);
+        } else {
+            return setCellBroadcastActivation(false);
+        }
+    }
+
+    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;
+    }
+
     protected void log(String msg) {
         Log.d(LOG_TAG, "[SimSmsInterfaceManager] " + msg);
     }
diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java
new file mode 100644
index 0000000..5f27cfc
--- /dev/null
+++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+public class SmsCbHeader {
+    public static final int PDU_HEADER_LENGTH = 6;
+
+    public final int geographicalScope;
+
+    public final int messageCode;
+
+    public final int updateNumber;
+
+    public final int messageIdentifier;
+
+    public final int dataCodingScheme;
+
+    public final int pageIndex;
+
+    public final int nrOfPages;
+
+    public SmsCbHeader(byte[] pdu) throws IllegalArgumentException {
+        if (pdu == null || pdu.length < PDU_HEADER_LENGTH) {
+            throw new IllegalArgumentException("Illegal PDU");
+        }
+
+        geographicalScope = (pdu[0] & 0xc0) >> 6;
+        messageCode = ((pdu[0] & 0x3f) << 4) | ((pdu[1] & 0xf0) >> 4);
+        updateNumber = pdu[1] & 0x0f;
+        messageIdentifier = (pdu[2] << 8) | pdu[3];
+        dataCodingScheme = pdu[4];
+
+        // 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;
+    }
+}
diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java
new file mode 100644
index 0000000..7136ea0
--- /dev/null
+++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.SmsCbMessage;
+import android.test.AndroidTestCase;
+
+/**
+ * Test cases for basic SmsCbMessage operations
+ */
+public class GsmSmsCbTest extends AndroidTestCase {
+
+    private void doTestGeographicalScopeValue(byte[] pdu, byte b, int expectedGs) {
+        pdu[0] = b;
+        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected geographical scope decoded", expectedGs, msg
+                .getGeographicalScope());
+    }
+
+    public void testCreateNullPdu() {
+        SmsCbMessage msg = SmsCbMessage.createFromPdu(null);
+
+        assertNull("createFromPdu(byte[] with null pdu should return null", msg);
+    }
+
+    public void testCreateTooShortPdu() {
+        byte[] pdu = new byte[4];
+        SmsCbMessage msg = SmsCbMessage.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 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 = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A GSM default alphabet message with carriage return padding",
+                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 = SmsCbMessage.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 = SmsCbMessage.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 = SmsCbMessage.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 = SmsCbMessage.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 = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected 7-bit string decoded",
+                "A UCS2 message containing a \u0434 character", 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 = SmsCbMessage.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 = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected message identifier decoded", 12345, msg.getMessageIdentifier());
+    }
+
+    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 = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected message code decoded", 682, msg.getMessageCode());
+    }
+
+    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 = SmsCbMessage.createFromPdu(pdu);
+
+        assertEquals("Unexpected update number decoded", 5, msg.getUpdateNumber());
+    }
+}