Android KITKAT: デフォルトSMSアプリを作る

この記事は、Android Advent Calendar 2013の12月3日のものとなります。

Android 4.4 KITKATから、一般のアプリ開発者がSMS/MMSのデフォルトアプリを開発することが出来るようになりました。

本記事では、新しいSMSの仕組みのメリット、逆にこれまで動作していたアプリで気をつけないといけないこと、デフォルトSMSアプリを作る方法などについて紹介します。

なお、筆者の環境ではMMSを取り扱えないため、SMSに絞って説明いたします。1

本記事は、Android Developer Blogの以下のAndroid 4.4 SMS新機能関連記事とAPI Reference、AOSPのコードなどなどを参考にして実験した結果を基にして書いています。

目次

AndroidのSMS APIのおさらい

まず、AndroidアプリがSMSに対して何を行うことが出来るのかについてざっとおさらいします。

SMSの受信

Android 4.3まででも、適切にpermissionを宣言していれば、SMSの受信、送信を行うアプリを開発することは可能です。

SMS受信をトリガにしてなんらかの処理を行いたいアプリは、android.provider.Telephony.SMS_RECEIVED のアクションを受け取るBroadcastReceiverを作成することで、SMSを受け取ることができます。 以下にAndroidManifest.xmlでの指定方法の例を示します。

...
<uses-permission android:name="android.permission.RECEIVE_SMS" />
...
<receiver android:name=".SMSBroadcastReceiver">
    <intent-filter >
        <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
    </intent-filter>
</receiver>
...

この方法はAndroid 4.4でも有効です。2

たとえば、特定の番号からのSMSを受け取ったことをトリガにして、保存されたSMSからそれを取り除く(指令用途なので保存不要)というようなことを行いたい場合、SMS_RECEIVEDのアクションで対応するのは適さなくなります。理由は後述します。

SMSの送信

android.permission.SEND_SMSのpermissionを持っていれば、アプリが直接SmsManagerクラスを使ってSMSの送信を行うことが可能です。以下に例を示します。3

SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage("XXX-XXXX-XXXX", null, "Hello", null, null);

sendTextMessage()の第一引数は送信先の電話番号です。

また、前述のpermissionを持っていなくても、SMSアプリをACTION_SENDTOのIntentで呼び出すことでSMS送信を行うこともできます。こちらの方法は、送信するSMSの編集画面が開きます。

Intent intent = new Intent(Intent.ACTION_SENDTO);
Uri smsNumberUri = Uri.parse("sms:XXX-XXXX-XXXX"); // 電話番号指定
intent.setData(smsNumberUri);
intent.putExtra("sms_body", "Message Body"); // 本文
startActivity(Intent.createChooser(intent, "Select a SMS app"));

どうしてもバックグランドで送信したいのでなければ、こちらの方法をもちいることがセキュリティ上望ましいです。

どちらの方法もAndroid 4.4で有効です。

SMSの読み出し

android.permission.READ_SMSのpermissionを持ったアプリは、SMSのContentProviderから、保存されたSMSを読み出すことができます。

ContentResolver cr = context.getContentResolver();
Cursor cursor = cr.query(Uri.parse("content://sms/"),
    null, // projection
    null, // selection
    null, // selectionArgs
    null  // sortOrder
    );

while (cursor.moveToNext()) {
    // SMSを1つずつ取り出して処理する(省略)
}

これもAndroid 4.4で有効です。

SMSの書き込み

android.permission.WRITE_SMSのpermissionを持ったアプリは、SMSのContentProviderにデータを書き込む(追加、変更、削除)ことができます。以下の例は、送信SMSとしてデータを追加をしています。

ContentValues values = new ContentValues();
values.put("address", "XXX-XXXX-XXXX"); // 電話番号
values.put("body", "Message Body");     // 本文

ContentResolver cr = context.getContentResolver();
Uri uri = cr.insert(Uri.parse("content://sms/sent", values);

Android 4.4では、これを正常に行うことが出来るのはデフォルトSMSアプリのみです。

デフォルトSMSアプリ以外は、WRITE_SMSのpermissionを持っていても、書き込みが成功しません。特に例外なども発生することなく、SMSのContentProviderのデータが変更されません。すなわち、既存アプリはエラーにはなりませんが、正常な結果を得られません。

SMSをSDカードやDropboxなどにバックアップ・リストアするようなアプリの場合、Android 4.4向けの対策を行っていなければ、バックアップはできてもリストアで失敗することになります。

なんらかの指令によってSMSデータベースを書き換える(特定のSMSを削除する)ようなアプリも、これまで通りのやり方では正常動作しません。

デフォルトSMSアプリを作ろう

Android 4.4からアプリ開発者がデフォルトSMSアプリを作ることが出来るようになりました。以下の場合には、手順に従ってデフォルトSMSアプリになれるように開発をする必要があります。

  • SMS/MMSのバックアップ・リストアアプリ
  • 独自機能を持ったSMS/MMSアプリ

前者がデフォルトSMSにならないといけないのは、先に述べたとおりです。ただ、リストアする時だけデフォルトSMSアプリになっていればよく、それ以外の時には元のデフォルトSMSアプリに戻すように作らなければ、ユーザの不評を買うでしょう。その手順については最後に述べます。

まずは単純にデフォルトSMSアプリの作り方について説明します。

AndroidManifest.xml

アプリがデフォルトSMSアプリになれるかどうかは、AndroidManifest.xml に以下のものがすべて宣言されているかどうかで判断されます。

  • SMS_DELIVERED のアクションを受け取るBroadcastReceiver
  • WAP_PUSH_DELIVERED のアクションを受け取るBroardcastReceiver
  • SMS/MMS関連のSEND、SENDTOのアクションを受け取るActivity
  • SMS/MMS関連のRESPOND_VIA_MESSAGEのアクションを受け取るService

具体的には以下のような記述になります。

<!-- BroadcastReceiver that listens for incoming SMS messages -->
<receiver android:name=".SmsReceiver"
          android:permission="android.permission.BROADCAST_SMS">
    <intent-filter>
        <action android:name="android.provider.Telephony.SMS_DELIVER" />
    </intent-filter>
</receiver>

<!-- BroadcastReceiver that listens for incoming MMS messages -->
<receiver android:name=".MmsReceiver"
    android:permission="android.permission.BROADCAST_WAP_PUSH">
    <intent-filter>
        <action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
        <data android:mimeType="application/vnd.wap.mms-message" />
    </intent-filter>
</receiver>

<!-- Activity that allows the user to send new SMS/MMS messages -->
<activity android:name=".ComposeSmsActivity" >
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <action android:name="android.intent.action.SENDTO" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="sms" />
        <data android:scheme="smsto" />
        <data android:scheme="mms" />
        <data android:scheme="mmsto" />
    </intent-filter>
</activity>

<!-- Service that delivers messages from the phone "quick response" -->
<service android:name=".HeadlessSmsSendService"
         android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
         android:exported="true" >
    <intent-filter>
        <action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="sms" />
        <data android:scheme="smsto" />
        <data android:scheme="mms" />
        <data android:scheme="mmsto" />
    </intent-filter>
</service>

これらはすべてが必要で、一つでも欠けているとデフォルトSMSアプリとして選択できなくなります(クラス名などは自身のアプリにあわせて変更して構いません)。

SMS_DELIVER、WAP_PUSH_DELIVERE、RESPOND_VIA_MESSAGE のアクションを受け取るものは、それぞれ BROADCAST_SMS、BROARDCAST_WAP_PUSH、SEND_RESPOND_VIA_MESSAGE のpermissionも宣言しておかなければなりません。

本記事では、SMSの受信とSEND_RESPOND_VIA_MESSAGEについての実装について説明します。

SMS受信 (デフォルトSMSアプリ)

SMS_RECEIVED のアクションを受けた時にメッセージを取り出す処理とほとんど同じですが、受信したメッセージをSMSのContentProviderに登録しなければなりません。

@Override
public void onReceive(Context context, Intent intent) {
    Bundle extras = intent.getExtras();

    if (extras == null) {
        return;
    }

    Object[] smsExtras = (Object[]) extras.get(SmsConstant.PDUS);

    ContentResolver contentResolver = context.getContentResolver();
    Uri smsUri = Uri.parse(SmsConstant.SMS_URI);

    for (Object smsExtra : smsExtras) {
        // SMSを取り出す
        byte[] smsBytes = (byte[]) smsExtra;
        SmsMessage smsMessage = SmsMessage.createFromPdu(smsBytes);

        String body = smsMessage.getMessageBody();
        String address = smsMessage.getOriginatingAddress();

        // SMSのContentProviderに保存する    
        ContentValues values = new ContentValues();
        values.put("address", address
        values.put("body", body);

        Uri uri = contentResolver.insert(Uri.parse("content://sms/"), values);

        // TODO: 通知処理など、足りないものを実装してください
    }
}

SMSにはなんらかの指令だけ書いてあって、そのSMSは保存が不要であるような場合、そもそも保存しないという実装にすることも可能です。SMS_RECEIVEDの場合との大きな違いはそこになります。

MMSも同様に実装することになりますが、本記事では省略します。

RESPOND_VIA_MESSAGE

Android 4.0から導入されたquick responseという機能をあります。

電話着信時に、ロック画面から電話にでるのではなく、テキストメッセージ(SMS)を送信して応答するという機能です。たとえば会議中などで出られない時に、折り返し連絡する旨を返信します。

RESPOND_VIA_MESSAGE のアクションとは、このロック画面でのメッセージ選択によって呼ばれるものです。

Android 4.3まででは、この機能はシステムのみが使用できる機能でしたが、デフォルトSMSアプリになると、選択結果を受け取ることができます。逆に言えば、受け取った選択結果を元に、電話の発信元に対してテキストメッセージ(SMS)を送信する処理を自分で実装しなければなりません。

よって、以下のようなサービスを実装します。

public class HeadlessSmsSendService extends IntentService {

    public HeadlessSmsSendService() {
        super(HeadlessSmsSendService.class.getName());

        setIntentRedelivery(true);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        String action = intent.getAction();
        if (!TelephonyManager.ACTION_RESPOND_VIA_MESSAGE.equals(action)) {
            return;
        }

        Bundle extras = intent.getExtras();

        if (extras == null) {
            return;
        }

        String message = extras.getString(Intent.EXTRA_TEXT);
        Uri intentUri = intent.getData();
        String recipients = getRecipients(intentUri);

        // 宛先がなければ終了
        if (TextUtils.isEmpty(recipients)) {
            return;
        }

        // メッセージがなければ終了
        if (TextUtils.isEmpty(message)) {
            return;
        }

        String[] destinations = TextUtils.split(recipients, ";");

        sendAndStoreTextMessage(getContentResolver(), destinations, message);
    }

    /**
     * get quick response recipients from URI
     */
    private String getRecipients(Uri uri) {
        String base = uri.getSchemeSpecificPart();
        int pos = base.indexOf('?');
        return (pos == -1) ? base : base.substring(0, pos);
    }

    /**
     * Send text message to recipients and store the message to SMS Content Provider
     *
     * @param contentResolver ContentResolver
     * @param destinations recipients of message
     * @param message message
     */
    private void sendAndStoreTextMessage(ContentResolver contentResolver, String[] destinations, String message) {
        SmsManager smsManager = SmsManager.getDefault();

        Uri smsSentUri = Uri.parse(SmsConstant.SMS_SENT_URI);

        for (String destination : destinations) {
            smsManager.sendTextMessage(destination, null, message, null, null);

            ContentValues values = new ContentValues();
            values.put(SmsConstant.COLUMN_ADDRESS, destination);
            values.put(SmsConstant.COLUMN_BODY, message);

            Uri uri = contentResolver.insert(smsSentUri, values);
        }
    }
}

一時的にデフォルトSMSアプリになろう

SMS/MMSのバックアップ・レストアアプリなどは、Android 4.4以降では一時的にだけデフォルトSMSになる必要があります。すなわち、

  • 現在のデフォルトSMSアプリの情報を保存する
  • 自分をデフォルトSMSアプリに変更する
  • SMSのContentProviderへの書き込み処理を行う
  • デフォルトSMSアプリを元に戻す

という流れで処理を行わないといけません。上記のうち、デフォルトSMSアプリに関する部分について簡単に説明します。

現在のデフォルトSMSアプリの情報を保存する

以下のようにして現在のデフォルトSMSアプリの情報を取得し、プリファレンスに保持します。

String defaultSmsApp = Telephony.Sms.getDefaultSmsPackage(context);
SharedPreferences prefs = getSharedPreference("backup", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.editor();
editor.putString("smsapp", defaultSmsApp);
editor.apply();

自分をデフォルトSMSアプリに変更する

変更要求のIntentを投げる。

Intent intent = new Intent(context, Sms.Intents.ACTION_CHANGE_DEFAULT);
intent.putExtra(Sms.Intents.EXTRA_PACKAGE_NAME, context.getPackageName());
startActivity(intent);

デフォルトSMSアプリを元に戻す

プリファレンスに保存しておいたデフォルトSMSアプリに戻す。

SharedPreferences prefs = getSharedPreference("backup", Context.MODE_PRIVATE);
String defaultSmsApp = prefs.getString("smsapp", null);
if (defaultSmsApp != null) {
    Intent intent = new Intent(context, Sms.Intents.ACTION_CHANGE_DEFAULT);
    intent.putExtra(Sms.Intents.EXTRA_PACKAGE_NAME, defaultSmsApp);
    startActivity(intent);
}

注意点

一時的にデフォルトSMSアプリになるとはいえ、その間はSMS/MMS着信で正しくデータを受け取って保存しないと、ユーザの大事なSMS/MMSが消失するということもあり得ます。

SMSのバックアップ・リストアアプリの中には、その部分をサボってとりあえずデフォルトSMSアプリになれるようにだけしているものもありますが、そういうマナーの悪いアプリは作らないようにしましょう。4

サンプルアプリ

Github上にサンプルを公開していますので、ご参考にどうぞ。

https://github.com/ynakanishi/KitKatSms

以上です。

  1. NTT DOCOMO回線ではMMSを利用できないため。 []
  2. Android 4.4でも、SMSを受け取って電話番号認証を行いたいようなアプリは、この方法を使うべきだと述べています。 []
  3. SmsManagerクラスはandorid.telephony.SmsManagerを使用してください。android.telephony.gsm.SmsManagerはdeprecatedです。 []
  4. SIMが挿入されている場合にはデフォルトSMSアプリにならないという方法もあるかもしれませんが、非常に悪いUXになるでしょう。 []

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です