Implement new method for handling SMS/MMS on the platform
David Braun [Mon, 16 Sep 2013 20:44:51 +0000 (13:44 -0700)]
Multi project change:
The changes in this project implement the actual change in the SMS related
intent behavior (defining and using the new events).

Bug: 10449618
Change-Id: Ia707ed561d89428db78203a2ed54504d867a3e43

src/java/android/provider/Telephony.java
src/java/com/android/internal/telephony/InboundSmsHandler.java
src/java/com/android/internal/telephony/SmsApplication.java [new file with mode: 0644]
src/java/com/android/internal/telephony/WapPushOverSms.java

index 8fdd49c..d1eb4fd 100644 (file)
@@ -18,6 +18,7 @@ package android.provider;
 
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -30,6 +31,8 @@ import android.text.TextUtils;
 import android.telephony.Rlog;
 import android.util.Patterns;
 
+import com.android.internal.telephony.SmsApplication;
+
 
 import java.util.HashSet;
 import java.util.Set;
@@ -519,17 +522,73 @@ public final class Telephony {
             public static final int RESULT_SMS_UNSUPPORTED = 4;
 
             /**
-             * Set by BroadcastReceiver. Indicates the duplicated imcoming message.
+             * Set by BroadcastReceiver. Indicates the duplicated incoming message.
              */
             public static final int RESULT_SMS_DUPLICATED = 5;
 
             /**
+             * Activity action: Ask the user to change the default
+             * SMS application. This will show a dialog that asks the
+             * user whether they want to replace the current default
+             * SMS application with the one specified in
+             * {@link #EXTRA_PACKAGE_NAME}.
+             */
+            @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+            public static final String ACTION_CHANGE_DEFAULT =
+                    "android.provider.Telephony.ACTION_CHANGE_DEFAULT";
+
+            /**
+             * The PackageName string passed in as an
+             * extra for {@link #ACTION_CHANGE_DEFAULT}
+             *
+             * @see #ACTION_CHANGE_DEFAULT
+             */
+            public static final String EXTRA_PACKAGE_NAME = "package";
+
+            /**
+             * Used by applications to determine if they are the current default sms package.
+             * @param context context of the requesting application
+             * @param packageName name of the package to check.
+             * @return true if the specified package is the current default SMS app.
+             */
+            public static boolean isDefaultSmsPackage(Context context, String packageName) {
+                ComponentName component = SmsApplication.getDefaultSmsApplication(context, false);
+                if (component != null && component.getPackageName().equals(packageName)) {
+                    return true;
+                }
+                return false;
+            }
+
+            /**
              * Broadcast Action: A new text based SMS message has been received
-             * by the device. The intent will have the following extra
+             * by the device. This intent will only be delivered to the default
+             * sms app. That app is responsible for writing the message and notifying
+             * the user. 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 SMS_DELIVER_ACTION =
+                    "android.provider.Telephony.SMS_DELIVER";
+
+            /**
+             * Broadcast Action: A new text based SMS message has been received
+             * by the device. This intent will be delivered to all registered
+             * receivers as a notification. These apps are not expected to write the
+             * message or notify the user. The intent will have the following extra
              * values:</p>
              *
              * <ul>
-             *   <li><em>pdus</em> - An Object[] od byte[]s containing the PDUs
+             *   <li><em>pdus</em> - An Object[] of byte[]s containing the PDUs
              *   that make up the message.</li>
              * </ul>
              *
@@ -545,7 +604,8 @@ public final class Telephony {
 
             /**
              * Broadcast Action: A new data based SMS message has been received
-             * by the device. The intent will have the following extra
+             * by the device. This intent will be delivered to all registered
+             * receivers as a notification. The intent will have the following extra
              * values:</p>
              *
              * <ul>
@@ -565,7 +625,39 @@ public final class Telephony {
 
             /**
              * Broadcast Action: A new WAP PUSH message has been received by the
-             * device. The intent will have the following extra
+             * device. This intent will only be delivered to the default
+             * sms app. That app is responsible for writing the message and notifying
+             * the user. 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&lt;String,String&gt;)</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_DELIVER_ACTION =
+                    "android.provider.Telephony.WAP_PUSH_DELIVER";
+
+            /**
+             * Broadcast Action: A new WAP PUSH message has been received by the
+             * device. This intent will be delivered to all registered
+             * receivers as a notification. These apps are not expected to write the
+             * message or notify the user. The intent will have the following extra
              * values:</p>
              *
              * <ul>
index d243493..3eb77cf 100644 (file)
@@ -19,6 +19,7 @@ package com.android.internal.telephony;
 import android.app.Activity;
 import android.app.AppOpsManager;
 import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -627,11 +628,22 @@ public abstract class InboundSmsHandler extends StateMachine {
 
         Intent intent;
         if (destPort == -1) {
-            intent = new Intent(Intents.SMS_RECEIVED_ACTION);
+            intent = new Intent(Intents.SMS_DELIVER_ACTION);
+
+            // Direct the intent to only the default SMS app. If we can't find a default SMS app
+            // then sent it to all broadcast receivers.
+            ComponentName componentName = SmsApplication.getDefaultSmsApplication(mContext, true);
+            if (componentName != null) {
+                // Deliver SMS message only to this receiver
+                intent.setComponent(componentName);
+                log("Delivering SMS to: " + componentName.getPackageName() +
+                        " " + componentName.getClassName());
+            }
         } else {
             Uri uri = Uri.parse("sms://localhost:" + destPort);
             intent = new Intent(Intents.DATA_SMS_RECEIVED_ACTION, uri);
         }
+
         intent.putExtra("pdus", pdus);
         intent.putExtra("format", tracker.getFormat());
         dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
@@ -769,27 +781,43 @@ public abstract class InboundSmsHandler extends StateMachine {
         @Override
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
-            if (!Intents.SMS_RECEIVED_ACTION.equals(action)
-                    && !Intents.DATA_SMS_RECEIVED_ACTION.equals(action)) {
-                loge("unexpected BroadcastReceiver action: " + action);
-            }
+            if (action.equals(Intents.SMS_DELIVER_ACTION)) {
+                // Now dispatch the notification only intent
+                intent.setAction(Intents.SMS_RECEIVED_ACTION);
+                intent.setComponent(null);
+                dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
+                        AppOpsManager.OP_RECEIVE_SMS, this);
+            } else if (action.equals(Intents.WAP_PUSH_DELIVER_ACTION)) {
+                // Now dispatch the notification only intent
+                intent.setAction(Intents.WAP_PUSH_RECEIVED_ACTION);
+                intent.setComponent(null);
+                dispatchIntent(intent, android.Manifest.permission.RECEIVE_SMS,
+                        AppOpsManager.OP_RECEIVE_SMS, this);
+            } else {
+                // Now that the intents have been deleted we can clean up the PDU data.
+                if (!Intents.DATA_SMS_RECEIVED_ACTION.equals(action)
+                        && !Intents.DATA_SMS_RECEIVED_ACTION.equals(action)
+                        && !Intents.WAP_PUSH_RECEIVED_ACTION.equals(action)) {
+                    loge("unexpected BroadcastReceiver action: " + action);
+                }
 
-            int rc = getResultCode();
-            if ((rc != Activity.RESULT_OK) && (rc != Intents.RESULT_SMS_HANDLED)) {
-                loge("a broadcast receiver set the result code to " + rc
-                        + ", deleting from raw table anyway!");
-            } else if (DBG) {
-                log("successful broadcast, deleting from raw table.");
-            }
+                int rc = getResultCode();
+                if ((rc != Activity.RESULT_OK) && (rc != Intents.RESULT_SMS_HANDLED)) {
+                    loge("a broadcast receiver set the result code to " + rc
+                            + ", deleting from raw table anyway!");
+                } else if (DBG) {
+                    log("successful broadcast, deleting from raw table.");
+                }
 
-            deleteFromRawTable(mDeleteWhere, mDeleteWhereArgs);
-            sendMessage(EVENT_BROADCAST_COMPLETE);
+                deleteFromRawTable(mDeleteWhere, mDeleteWhereArgs);
+                sendMessage(EVENT_BROADCAST_COMPLETE);
 
-            int durationMillis = (int) ((System.nanoTime() - mBroadcastTimeNano) / 1000000);
-            if (durationMillis >= 5000) {
-                loge("Slow ordered broadcast completion time: " + durationMillis + " ms");
-            } else if (DBG) {
-                log("ordered broadcast completed in: " + durationMillis + " ms");
+                int durationMillis = (int) ((System.nanoTime() - mBroadcastTimeNano) / 1000000);
+                if (durationMillis >= 5000) {
+                    loge("Slow ordered broadcast completion time: " + durationMillis + " ms");
+                } else if (DBG) {
+                    log("ordered broadcast completed in: " + durationMillis + " ms");
+                }
             }
         }
     }
diff --git a/src/java/com/android/internal/telephony/SmsApplication.java b/src/java/com/android/internal/telephony/SmsApplication.java
new file mode 100644 (file)
index 0000000..9980c18
--- /dev/null
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS 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.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.provider.Settings;
+import android.provider.Telephony.Sms.Intents;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Class for managing the primary application that we will deliver SMS/MMS messages to
+ *
+ * {@hide}
+ */
+public final class SmsApplication {
+    private static final String MESSAGING_PACKAGE_NAME = "com.android.mms";
+
+    public static class SmsApplicationData {
+        /**
+         * Name of this SMS app for display.
+         */
+        public String mApplicationName;
+
+        /**
+         * Package name for this SMS app.
+         */
+        public String mPackageName;
+
+        /**
+         * The class name of the SMS receiver in this app.
+         */
+        public String mSmsReceiverClass;
+
+        /**
+         * The class name of the MMS receiver in this app.
+         */
+        public String mMmsReceiverClass;
+
+        /**
+         * The user-id for this application
+         */
+        public int mUid;
+
+        public SmsApplicationData(String applicationName, String packageName,
+                String smsReceiverName, String mmsReceiverName, int uid) {
+            mApplicationName = applicationName;
+            mPackageName = packageName;
+            mSmsReceiverClass = smsReceiverName;
+            mMmsReceiverClass = mmsReceiverName;
+            mUid = uid;
+        }
+    }
+
+    /**
+     * Returns the list of available SMS apps defined as apps that are registered for both the
+     * SMS_RECEIVED_ACTION (SMS) and WAP_PUSH_RECEIVED_ACTION (MMS) broadcasts (and their broadcast
+     * receivers are enabled)
+     */
+    public static Collection<SmsApplicationData> getApplicationCollection(Context context) {
+        PackageManager packageManager = context.getPackageManager();
+
+        // Get the list of apps registered for SMS
+        Intent intent = new Intent(Intents.SMS_DELIVER_ACTION);
+        int flags = 0;
+        List<ResolveInfo> smsReceivers = packageManager.queryBroadcastReceivers(intent, flags);
+
+        intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
+        intent.setDataAndType(null, "application/vnd.wap.mms-message");
+        List<ResolveInfo> mmsReceivers = packageManager.queryBroadcastReceivers(intent, flags);
+
+        HashMap<String, SmsApplicationData> receivers = new HashMap<String, SmsApplicationData>();
+
+        // Add one entry to the map for every sms receiver (ignoring duplicate sms receivers)
+        for (ResolveInfo r : smsReceivers) {
+            String packageName = r.activityInfo.packageName;
+            if (!receivers.containsKey(packageName)) {
+                String applicationName = r.loadLabel(packageManager).toString();
+                SmsApplicationData smsApplicationData = new SmsApplicationData(applicationName,
+                        packageName, r.activityInfo.name, null, r.activityInfo.applicationInfo.uid);
+                receivers.put(packageName, smsApplicationData);
+            }
+        }
+
+        // Update any existing entries with mms receiver class
+        for (ResolveInfo r : mmsReceivers) {
+            String packageName = r.activityInfo.packageName;
+            SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null) {
+                smsApplicationData.mMmsReceiverClass = r.activityInfo.name;
+            }
+        }
+
+        // Remove any entries (which we added for sms receivers) for which we did not also find
+        // valid mms receivers
+        for (ResolveInfo r : smsReceivers) {
+            String packageName = r.activityInfo.packageName;
+            SmsApplicationData smsApplicationData = receivers.get(packageName);
+            if (smsApplicationData != null && smsApplicationData.mMmsReceiverClass == null) {
+                receivers.remove(packageName);
+            }
+        }
+        return receivers.values();
+    }
+
+    /**
+     * Checks to see if we have a valid installed SMS application for the specified package name
+     * @return Data for the specified package name or null if there isn't one
+     */
+    private static SmsApplicationData getApplicationForPackage(
+            Collection<SmsApplicationData> applications, String packageName) {
+        if (packageName == null) {
+            return null;
+        }
+        // Is there an entry in the application list for the specified package?
+        for (SmsApplicationData application : applications) {
+            if (application.mPackageName.contentEquals(packageName)) {
+                return application;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the application we will use for delivering SMS/MMS messages.
+     *
+     * We return the preferred sms application with the following order of preference:
+     * (1) User selected SMS app (if selected, and if still valid)
+     * (2) Android Messaging (if installed)
+     * (3) The currently configured highest priority broadcast receiver
+     * (4) Null
+     */
+    private static SmsApplicationData getApplication(Context context, boolean updateIfNeeded) {
+        Collection<SmsApplicationData> applications = getApplicationCollection(context);
+
+        // Determine which application receives the broadcast
+        String defaultApplication = Settings.Secure.getString(context.getContentResolver(),
+                Settings.Secure.SMS_DEFAULT_APPLICATION);
+
+        SmsApplicationData applicationData = null;
+        if (defaultApplication != null) {
+            applicationData = getApplicationForPackage(applications, defaultApplication);
+        }
+        // Picking a new SMS app requires AppOps and Settings.Secure permissions, so we only do
+        // this if the caller asked us to.
+        if (updateIfNeeded) {
+            if (applicationData == null) {
+                // Do we have a package for Android Messaging?
+                applicationData = getApplicationForPackage(applications, MESSAGING_PACKAGE_NAME);
+            }
+            if (applicationData == null) {
+                // Are there any applications?
+                if (applications.size() != 0) {
+                    applicationData = (SmsApplicationData)applications.toArray()[0];
+                }
+            }
+
+            // If we found a new default app, update the setting
+            if (applicationData != null) {
+                setDefaultApplication(applicationData.mPackageName, context);
+            }
+        }
+        return applicationData;
+    }
+
+    /**
+     * Sets the specified package as the default SMS/MMS application. The caller of this method
+     * needs to have permission to set AppOps and write to secure settings.
+     */
+    public static void setDefaultApplication(String packageName, Context context) {
+        Collection<SmsApplicationData> applications = getApplicationCollection(context);
+        String oldPackageName = Settings.Secure.getString(context.getContentResolver(),
+                Settings.Secure.SMS_DEFAULT_APPLICATION);
+        SmsApplicationData oldSmsApplicationData = getApplicationForPackage(applications,
+                oldPackageName);
+        SmsApplicationData smsApplicationData = getApplicationForPackage(applications,
+                packageName);
+
+        if (smsApplicationData != null && smsApplicationData != oldSmsApplicationData) {
+            // Ignore OP_WRITE_SMS for the previously configured default SMS app.
+            AppOpsManager appOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+            if (oldSmsApplicationData != null) {
+                appOps.setMode(AppOpsManager.OP_WRITE_SMS, oldSmsApplicationData.mUid,
+                        oldSmsApplicationData.mPackageName, AppOpsManager.MODE_IGNORED);
+            }
+
+            // Update the secure setting.
+            Settings.Secure.putString(context.getContentResolver(),
+                    Settings.Secure.SMS_DEFAULT_APPLICATION, smsApplicationData.mPackageName);
+
+            // Allow OP_WRITE_SMS for the newly configured default SMS app.
+            appOps.setMode(AppOpsManager.OP_WRITE_SMS, smsApplicationData.mUid,
+                    smsApplicationData.mPackageName, AppOpsManager.MODE_ALLOWED);
+        }
+    }
+
+    /**
+     * Returns SmsApplicationData for this package if this package is capable of being set as the
+     * default SMS application.
+     */
+    public static SmsApplicationData getSmsApplicationData(String packageName, Context context) {
+        Collection<SmsApplicationData> applications = getApplicationCollection(context);
+        return getApplicationForPackage(applications, packageName);
+    }
+
+    /**
+     * Gets the default SMS application
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver SMS messages to
+     */
+    public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) {
+        ComponentName component = null;
+        SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded);
+        if (smsApplicationData != null) {
+            component = new ComponentName(smsApplicationData.mPackageName,
+                    smsApplicationData.mSmsReceiverClass);
+        }
+        return component;
+    }
+
+    /**
+     * Gets the default MMS application
+     * @param context context from the calling app
+     * @param updateIfNeeded update the default app if there is no valid default app configured.
+     * @return component name of the app and class to deliver MMS messages to
+     */
+    public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) {
+        ComponentName component = null;
+        SmsApplicationData smsApplicationData = getApplication(context, updateIfNeeded);
+        if (smsApplicationData != null) {
+            component = new ComponentName(smsApplicationData.mPackageName,
+                    smsApplicationData.mMmsReceiverClass);
+        }
+        return component;
+    }
+}
index 04f73dc..b600606 100755 (executable)
@@ -216,7 +216,7 @@ public class WapPushOverSms implements ServiceConnection {
             appOp = AppOpsManager.OP_RECEIVE_WAP_PUSH;
         }
 
-        Intent intent = new Intent(Intents.WAP_PUSH_RECEIVED_ACTION);
+        Intent intent = new Intent(Intents.WAP_PUSH_DELIVER_ACTION);
         intent.setType(mimeType);
         intent.putExtra("transactionId", transactionId);
         intent.putExtra("pduType", pduType);
@@ -224,6 +224,16 @@ public class WapPushOverSms implements ServiceConnection {
         intent.putExtra("data", intentData);
         intent.putExtra("contentTypeParameters", pduDecoder.getContentParameters());
 
+        // Direct the intent to only the default MMS app. If we can't find a default MMS app
+        // then sent it to all broadcast receivers.
+        ComponentName componentName = SmsApplication.getDefaultMmsApplication(mContext, true);
+        if (componentName != null) {
+            // Deliver MMS message only to this receiver
+            intent.setComponent(componentName);
+            if (DBG) Rlog.v(TAG, "Delivering MMS to: " + componentName.getPackageName() +
+                  " " + componentName.getClassName());
+        }
+
         handler.dispatchIntent(intent, permission, appOp, receiver);
         return Activity.RESULT_OK;
     }