読者です 読者をやめる 読者になる 読者になる

遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

AndroidアプリでSMS受信を偽装する方法

android

今日のGoogle Developer Day 2010のセッションで、IMoNiの作者@t_eggさんが、「IMoNiはSMSのContentProviderにデータを突っ込むことで、iモード.netメールの受信通知をSMSとして(実際に受信している訳ではなく、仮想的に)受信させている」ということを説明されていました。

これについて、興味があったので自分でも実装してみました。
また、ContentProviderに突っ込む以外でも、SMSの受信サービスを直接呼び出すことでSMSを仮想的に受信させる方法を見つけたので、それについても記しておきます。

SMSのContentProviderに突っ込む方法

「SMSのContentProviderに突っ込む」と一言でいっても、SMSのContentProviderはSDKandroid.jarには存在しないので、詳細はOSのソースを追っていく必要があります。
URIとかカラム名の詳細は、
android.provider.Telephony
クラスを参照します。

これによると、insertすべきSMS受信BOXのContent URIは "content://sms/inbox" で、"address","date","read","subject","body"あたりのカラムにデータを突っ込めば良さそうということが分かります。

	ContentValues values = new ContentValues();
	values.put("address", "090xxxxxxxx");
	values.put("date", System.currentTimeMillis());
	values.put("read", false);
	values.put("subject", "test subject");
	values.put("body", "test body");
	getContentResolver().insert(Uri.parse("content://sms/inbox"), values);

この書き込みをするためにはAndroidManifest.xmlに以下のパーミッション設定が必要です。

	<uses-permission android:name="android.permission.WRITE_SMS" />
	<uses-permission android:name="android.permission.READ_SMS" />

このソースを実機で実行してみると、以下のような感じでちゃんと受信できていることが分かります。

受信元は数字じゃなくてもちゃんと表示されてますね。

もう一つの方法 - 直接SMS受信用のServiceを呼ぶ

さて上記のContentProviderに突っ込む方法でSMSの一覧に表示させることはできるのですが、一つ問題があって、ステータスバーへの通知が表示されません。

もちろん、自力で別途Notificationを発行すれば出ますが、出来ればこの辺の処理も本来行うべきクラスに行わせたいところです。
そこでもう一つの方法ですが、SMSの受信サービスに直接Intentを発行する、ということが出来ます。

呼び出す対象は「メッセージ」アプリのサービスクラスである、
com.android.mms.transaction.SmsReceiverService
クラスです。

このSmsReceiverServiceに発行するIntentには、SMS受信を表すActionである「android.provider.Telephony.SMS_RECEIVED」を指定します。
さらに、ここで肝となるのは、受信したSMSを表すバイト列を、"pdus"というキー名のextraにセットしてあげる必要がある点です。僕が調べた限り、このデータを一発で組み立ててくれるような便利なメソッドとかは存在しないので、SMSのフォーマットを調べた上で、自力でバイト列を作成してあげる必要があります。

以下のコードは、SMS and the PDU formatなどを参考にして、SMSのPDUバイト列を組み立てて受信サービスに渡してあげるサンプルソースです。
処理中で、GsmAlphabetというクラスを使ってますが、これはAndroid OSのcom.android.internal.telephonyパッケージに存在する内部ユーテリティクラスです。同パッケージ中のEncodeExceptionクラスと一緒に自分のプロジェクトにコピーしてやればそのまま使えるはずです。

	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		sendSms(this, "09000000000", "これはてすとだよ!");
        }

	private static void sendSms(Context context, String sender, String body) {
		byte[] pdu = null;
		byte[] scBytes = PhoneNumberUtils
				.networkPortionToCalledPartyBCD("0000000000");
		byte[] senderBytes = PhoneNumberUtils
				.networkPortionToCalledPartyBCD(sender);
		int lsmcs = scBytes.length;
		byte[] dateBytes = new byte[7];
		Calendar calendar = new GregorianCalendar();
		dateBytes[0] = reverseByte((byte) (calendar.get(Calendar.YEAR)));
		dateBytes[1] = reverseByte((byte) (calendar.get(Calendar.MONTH) + 1));
		dateBytes[2] = reverseByte((byte) (calendar.get(Calendar.DAY_OF_MONTH)));
		dateBytes[3] = reverseByte((byte) (calendar.get(Calendar.HOUR_OF_DAY)));
		dateBytes[4] = reverseByte((byte) (calendar.get(Calendar.MINUTE)));
		dateBytes[5] = reverseByte((byte) (calendar.get(Calendar.SECOND)));
		dateBytes[6] = reverseByte((byte) ((calendar.get(Calendar.ZONE_OFFSET) + calendar
				.get(Calendar.DST_OFFSET)) / (60 * 1000 * 15)));
		try {
			ByteArrayOutputStream bo = new ByteArrayOutputStream();
			bo.write(lsmcs);
			bo.write(scBytes);
			bo.write(0x04);
			bo.write((byte) sender.length());
			bo.write(senderBytes);
			bo.write(0x00);
			try {
				// ascii文字のみの場合こっち
				byte[] bodyBytes = GsmAlphabet.stringToGsm7BitPacked(body);
				bo.write(0x00);
				bo.write(dateBytes);
				bo.write(bodyBytes);
			} catch (EncodeException e) {
				// 2バイト文字を含む場合こっち
				System.out.println("try UCS2");
				try {
					byte[] textPart = body.getBytes("utf-16be");
					bo.write(0x0b);
					bo.write(dateBytes);
					bo.write(textPart.length);
					bo.write(textPart);
				} catch (UnsupportedEncodingException e1) {
				}
			}
			pdu = bo.toByteArray();
		} catch (IOException e) {
		}

		Intent intent = new Intent();
		intent.setClassName("com.android.mms",
				"com.android.mms.transaction.SmsReceiverService");
		intent.setAction("android.provider.Telephony.SMS_RECEIVED");
		intent.putExtra("pdus", new Object[] { pdu });
		context.startService(intent);
	}

	private static byte reverseByte(byte b) {
		return (byte) ((b & 0xF0) >> 4 | (b & 0x0F) << 4);
	}

このソースは単にサービスを呼び出しているだけなので、Permissionの設定は特に必要ありません。
こいつを実機で実行してみると、以下のようにちゃんと受信通知まで表示してくれます。
まるで本当にSMSを受信してるようです。


ただ、当然ながらメッセージアプリがアンインストールされちゃったりしている場合は、Serviceが反応しないのでこの方法は使えません。(レアケースだとは思いますが)

まとめ

  • SMSのContentProviderにデータを直接突っ込んでSMS受信を偽装する方法
  • SMSの受信サービスを直接呼び出してSMS受信を偽装する方法

を紹介しました。
これを使えば、アプリからの様々な通知をSMSで置き換えることができるので、うまく使えばユーザービリティを高められると思います!