Android Wear: アプリプロジェクトの新規作成から実機起動まで

追記 (2014/06/27 11:38 PST)

Android 0.8.1 が公開され、そちらで新規作成したプロジェクトにはバグが含まれないことを確認しました。

ただし、Android 0.8.1での新規プロジェクト作成で作られるMyActivityクラスは、Wearアプリ用に用意されたInsetActivityクラスではなく、普通にActivityクラスを親クラスとして参照するようです。

(Android SDK Managerでの、Android Support RepositoryとGoogle Repositoryのrevisionが古い問題は未解決です。)

アプリビルド環境について

Android Wearアプリのビルドには、Android Studio 0.8.0 が必要です。ただし、単純にそれをインストールしてただけではビルドが通りません。

回避策は見つかっているので、前回の記事を参照してください。

Android Studio 0.8.0が生成するプロジェクトの問題

Android Studio 0.8.0 では、Wearアプリのプロジェクトを新規作成できますが、前述の環境設定が終わっていてもビルドに成功しません。

作成されるプロジェクトの初期状態のMyActivityが誤っているようです。新規作成ウィザードが完了すると、以下のようなMyActivityクラスが生成されます。

public class MyActivity  extends WatchActivity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                mTextView = (TextView) stub.findViewById(R.id.text);
                Log.d(TAG, "TextView: " + mTextView.getText() + " view=" + mTextView);
            }
        });
    }
}

上記には以下の問題があります。

  • 親クラスとして指定しているWatchActivityというクラスが存在しない
  • TAGが定義されていない (おそらく WatchActivityに定義されている前提)

Wear用のサポートライブラリに含まれるJavaDocから、android.support.wearable.activity.InsetActivity がWearアプリの基本的なベースActivityクラスだとわかりますので、WatchActivity を InsetActivityに置き換えます。

そして、InsetActivityでabstractで定義されている onReadyForContent()をオーバーライドして実装します。

TAGが定義されてないのでLog.d()も取り払いましょう。

public class MyActivity  extends InsetActivity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                mTextView = (TextView) stub.findViewById(R.id.text);
            }
        });
    }

    @Override
    public void onReadyForContent() {
    }
}

以上で、Wearアプリのビルドが通るようになります。

しかし、これを実際にWearにインストールして実行すると IllegalStateExceptionが発生して落ちます。

エラーメッセージによると、setContentView()以降はonReadyForContent()で実行しないといけないようです。ということで、以下のように修正します。

public class MyActivity  extends InsetActivity {

    private TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onReadyForContent() {
        setContentView(R.layout.activity_my);
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                mTextView = (TextView) stub.findViewById(R.id.text);
            }
        });
    }
}

これでアプリの実行が成功するようになります。

Let’s enjoy Android Wear app development!

Android Studio 0.8.0 で Wear プロジェクトのビルド失敗について

問題

新規作成したWearプロジェクトのビルド時に、依存ライブラリが見つからないというエラーが出て失敗します。

見つからないのは以下の2ライブラリです。

  • com.google.android.gms:play-services-wearable:+
  • com.google.android.support:wearable:+

原因

Wear プロジェクトの依存ライブラリがまだ配布されていません (PST 2014年6月26日 18時現在)

Android SDK Manager でSDKを最新に更新し、Android 4.4W のコンポーネントを全部インストールし、サポートライブラリとGogoleのレポジトリも最新に更新した状態でも、sdk/extras 以下に必要なライブラリファイルが配信されていないために発生しています。

解決策

待ちましょう。Android SDK Managerで配信されるコンポーネントが更新されないと解決しないため。

Google I/O 2014でGoogleの人にこの件は伝達し、さらにadt-devというAndroid開発環境に関するGoogle Groupsにも報告を送りました。

adt-dev への報告はモデレータさんがOKしないと出ないと思われるので、念のため issue tracker にも以下で登録しました。

Can’t build Android Wear application project generated by Android Studio 0.8.0

(緩和策について追記: 2014/06/27 00:16 PST)
緩和策は見つかっています。以下のURLからダウンロードしたファイルを所定のディレクトリに置くことです。

https://dl-ssl.google.com/android/repository/google_m2repository_r09.zip

(上記の説明だけで対応出来る人にしか緩和策の実行はおすすめしません。)

これは上記のissueへのコメントに示されたxmlファイルから推察されたURLであり、実際にそのファイルは存在するようです。

ただし、これは正式な方法ではありません。もしかすると同じファイル名で差し替えのファイルが提供され、その後に公式にAndroid SDK Managerからダウンロードが提供されるかもしれません。その場合、SDK Managerからダウンロードできるファイルが改善版であるにも関わらず、同じリビジョンであるためにダウンロードが行われないということが起こる可能性もあります。

リスクを勘案し、元に戻せるようにするなどの対策をした上で使用することをおすすめします。

おまけ: Android Wearについて

Google I/O 2014にて参加者に先行して端末が配布されました。

実際に半日使用してみたところ、スマートフォンを取り出して確認するという行動がかなり減り、いろいろと快適になった感じがします。

ハングアウト(Google+のチャット機能)やTwitterへの返信を、日本語の音声入力で腕時計から簡単に返すというようなことも出来て非常に便利です。

Wearにインストールするアプリ次第でさらにいろいろなことも出来るので、今後が楽しみです。

Android Studio 0.5.0への移行Tips

Android Studio 0.5.0がCanary Channel (いわゆる人柱向け)で公開されましたが、0.4.x時とはbuild.gradleのファイルの書き方などが違うので、移行(マイグレーション)の作業が必要です。

特に0.4.2からのアップデートの場合、公式サイトの情報だけでは足りない(気づきにくい)部分があるので、簡易に手順をまとめておきます。

gradle plugin を0.8(0.7)から0.9へ変更

当然ですが、まずはgradle pluginのバージョンを変更します。以下は例です。

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.9.+'
    }
}

SDKのアップデートでbuild toolsの19.0.3が出ているのでそれをインストールして、こちらも最新版を参照するようにしましょう(前のバージョンでもしばらく大丈夫じゃないかとは思いますが)。

android {
    compileSdkVersion 19 // お好みで
    buildToolsVersion "19.0.3"

    // ... 以下他の設定
}

公式サイトに移行ガイドが提示されていますが、ポイントは2つです。

  • instrumentTestXXX という記述を androidTestXXX に書き換える
  • ライブラリプロジェクトの記述を書き換える

src/instrumentTest というフォルダを使用している場合は、そのフォルダ名をandroidTestに変更してください。build.gradleで、instrumentTest.setRoot()で別のフォルダを指定している場合、androidTest.setRoot()に書き換えます。

//instrumentTest.setRoot('tests')
androidTest.setRoot('tests')

上記、以前にEclipseからbuild.gradle ファイルを生成した場合には含まれているはずです。

また、instrumentTestCompile も同様にandroidTestCompileに書き換えます。

ライブラリプロジェクトは変更の必要があればガイドに従ってください。

gradle-wrapper.properties の変更

公式サイトで配布されているAndroid Studio 0.4.2の場合、gradleプラグインの0.7を使用しているはずです。プラグインの0.7ではgradleの1.10は非対応だったので1.9を使っているでしょう。

ラッパーを通じてgradleを使っている場合、ラッパーが取得するgradleのバージョンを1.10に上げる必要があります。

プロジェクトの gradle/wrapper/gradle-wrappper.properties ファイルの以下の部分を書き換えます。

distributionUrl=http\://services.gradle.org/distributions/gradle-1.10-all.zip

以上です。

Android KITKAT: デフォルトSMSアプリ選択の表示バグ (Nexus5で確認)

Advent Calendar向けの記事で紹介し忘れていた情報を追加します。

こちらで紹介した、Android 4.4から導入されたデフォルトSMSアプリ選択ですが、コードからデフォルトSMS変更を要求する時に表示されるメッセージ、日本語版だとバグっています。ご注意ください。

同じ画面なのですが、英語版だと以下のようになります。

デフォルトSMSアプリ選択画面英語版

“Use KITKAT SMS Sample instead of Hangouts as your SMS app?”

私が作った”KITKAT SMS Sample”に切り替えようとしているので、この文言は正しいです。

それが日本語版だとこうなります。

デフォルトSMS選択画面日本語版

“SMSアプリとしてKITKAT SMS Sampleの代わりにハングアウトを使用しますか?”

と、意味が逆転しています。これはユーザさんが混乱してしまいます。

日本語版の文字列リソースで、%1$sと%2$sの位置を入れ替えるの忘れてしまっているのでしょうね。

ということで、日本人向けにデフォルトSMSアプリになる必要があるアプリを作っている方は、この点を注意書きとして足しておいた方がいいかもしれません。もしかすると他の言語でも似たような問題あるかもしれませんが。

Googleのissue trackerに報告はしておきましたので、よろしければ星マークを押して注目度を上げていただければと思います。そうすれば早く修正されることになるかと思います。

(上記のissue trackerによると、韓国語でも同じ問題起きているようですね。)

追記(2013/12/7): Android 4.4.1でも修正されていません。

追記(2013/12/10): Android 4.4.2でも未修正でした。

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

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

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

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

なお、筆者の環境ではMMSを取り扱えないため、SMSに絞って説明いたします。 ((NTT DOCOMO回線ではMMSを利用できないため。))

本記事は、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でも有効です。 ((Android 4.4でも、SMSを受け取って電話番号認証を行いたいようなアプリは、この方法を使うべきだと述べています。))

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

SMSの送信

android.permission.SEND_SMSのpermissionを持っていれば、アプリが直接SmsManagerクラスを使ってSMSの送信を行うことが可能です。以下に例を示します。 ((SmsManagerクラスはandorid.telephony.SmsManagerを使用してください。android.telephony.gsm.SmsManagerはdeprecatedです。))

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アプリになれるようにだけしているものもありますが、そういうマナーの悪いアプリは作らないようにしましょう。 ((SIMが挿入されている場合にはデフォルトSMSアプリにならないという方法もあるかもしれませんが、非常に悪いUXになるでしょう。))

サンプルアプリ

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

https://github.com/ynakanishi/KitKatSms

以上です。

iBeacon Tips: Estimoteビーコンモジュールのモニタリング

Estimote社からプレオーダーで発売されているビーコンモジュールですが、パッケージ内に情報が非常に少ないです。

またビーコンのUUIDなどを変更することができませんし、UUID情報もパッケージに示されていません。

Estimoteでは、iOSのAPIへのラッパーのSDKを用意していますが、UUIDのためだけにそれをわざわざ使うというのも面倒ですし、将来的にビーコンモジュールが潤沢に市場に出回ったら、コストや利便性などを勘案して機種選定を行うことになりますので、ロックインされると困ります。 ((ラッパーはMITライセンスで公開されていますが、バイナリ提供で情報が埋め込まれているようです))

ということで、UUID情報を探して、それを直接使って通常のiBeaconアプリの作り方で作っていくことをおすすめします。

では、UUID情報はどうすれば見つかるでしょうか?BLEをモニタリングするなどでも発見できるだろうと思いますが、世の中にはすでに情報を公開されている方がいますので、ありがたくそれを使わせていただくことにします。

上記ブログによると、EstimoteのビーコンのUUIDは”B9407F30-F5F8-466E-AFF9-25556B57FE6D”であるとのことです。

実際にその値を使用することで、弊社ではEstimoteのビーコンの検出に成功しております。

iBeacon Tips: 正しいビーコン監視方法

iBeacon 関連の情報をGoogleなどで検索すると、特に日本語ブログなどで示されているiBeaconのCentral(監視側)プログラムでの開始手順が不十分になっています。

そのせいで、ビーコンを試そうとしてうまくいかずに失敗されている方もいらっしゃるかもしれません。

たとえば、検索上位に出てくる、以下のクラスメソッドさんの以下のブログには不十分な手順が記載されています(2013年11月26日現在)。

上記ブログの例では、リージョンに入ったことをトリガにしてビーコンからの通知の受け取りを開始していますが、それではアプリの起動時にすでにリージョン内にいる場合に正常動作しません。

以下に、正しいビーコン監視の開始手順を示します。

例で参照しているプロパティは以下のように定義しています。

@interface ViewController () <CLLocationManagerDelegate>

@property (nonatomic) CLLocationManager *locationManager;
@property (nonatomic) NSUUID *proximityUUID;
@property (nonatomic) CLBeaconRegion *beaconRegion;

@end

viewDidLoad ではCLLocationManagerにビーコン監視の開始を要求しています。定数定義はご自身で適切なものにしてください(UUIDなどは自分で作る必要があります)。

- (void)viewDidLoad
{
    [super viewDidLoad];

    if ([CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
        self.locationManager = [CLLocationManager new];
        self.locationManager.delegate = self;

        self.proximityUUID = [[NSUUID alloc] initWithUUIDString:SERVICE_UUID];

        self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:self.proximityUUID
                                                               identifier:SERVICE_IDENTIFER];
        [self.locationManager startMonitoringForRegion:self.beaconRegion];
    }
}

ここまでは特に変わったことはありません。

次の、モニタリング開始が正常に始まった時に呼ばれるdelegateメソッドが重要です。

- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region
{
    [self.locationManager requestStateForRegion:self.beaconRegion];
}

ここでiOS7から追加された”CLLocationManager requestStateForRegion:”を呼び出し、現在自分が、iBeacon監視でどういう状態にいるかを知らせてくれるように要求します。

これによって、次のdelegateメソッドが呼ばれます。

- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
    switch (state) {
    case CLRegionStateInside: // リージョン内にいる
        if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
            [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
        }
        break;
    case CLRegionStateOutside:
    case CLRegionStateUnknown:
    default:
        break;
    }
}

CLRegionStateInside が渡ってきていれば、すでになんらかのiBeaconのリージョン内にいるので、iOS7から追加された”CLLocationManager startRangingBeaconsInRegion:”を呼び、通知の受け取りを開始します。

あとは、リージョンの境界を越えて入った時にも同じく通知を開始するようにします。

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
        [self.locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region];
    }
}

以上です。

How to invoke AppOps in Android 4.4 KITKAT

In Android 4.3, we were able to invoke AppOps activity, a hidden brand-new security functionality. However the activity was completely hidden in Android 4.3.1 and later.

Can we never invoke AppOps in Android KITKAT?

Yes, there is a way to invoke AppOps not only in Android 4.3 but also 4.4.

UPDATE(Dec 10, 2013): Unfortunately, we cannot invoke AppOps setting fragment in Android 4.4.2.

If you activated developer mode in your KITKAT device, connect your device to PC and then try the following command.

% adb shell am start -n com.android.settings/com.android.settings.Settings \
-e :android:show_fragment com.android.settings.applications.AppOpsSummary \
--activity-clear-task --activity-exclude-from-recents

The new way invokes AppOps fragment via the Setting app!!

Of course, you can invoke AppOps from your application.

Intent intent = new Intent();
intent.setClassName("com.android.settings",
        "com.android.settings.Settings");
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
        Intent.FLAG_ACTIVITY_CLEAR_TASK |
        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(":android:show_fragment",
        "com.android.settings.applications.AppOpsSummary");
startActivity(intent);

I confirmed that works fine both in Android 4.3 and 4.4.

The original method was discovered by @adakoda. Great job!!

I only fix some issues. For instance, @adakoda’s method didn’t work when the Setting app is on recent apps.

Reference:

Android 4.3以降でAppOpsを呼び出す方法 (KITKAT対応)

Android 4.3.1以降で呼び出せなくなっていると思われたAppOpsですが、以下の方法で呼び出すことが可能であることがわかりました。

追記(2013/12/10): Android 4.4.2では以下の方法でも呼び出せなくなってしまいました。

開発者オプションを有効にしてUSBデバッグ接続している場合、PCから以下のようにコマンドを入れるとAppOpsを呼び出すことができます。

% adb shell am start -n com.android.settings/com.android.settings.Settings \
-e :android:show_fragment com.android.settings.applications.AppOpsSummary \
--activity-clear-task --activity-exclude-from-recents

改行なしだと読みにくくなるため、バックスラッシュで行を区切っていますが、1行で入力しても構いません。

それをコードで書くと以下のようになります。

Intent intent = new Intent();
intent.setClassName("com.android.settings",
        "com.android.settings.Settings");
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
        Intent.FLAG_ACTIVITY_CLEAR_TASK |
        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(":android:show_fragment",
        "com.android.settings.applications.AppOpsSummary");
context.startActivity(intent);

いずれもAndroid 4.3でも動作することを確認しております。

追記(2013/12/6): Android 4.4.1にアップデートしたNexus5でも動作することを確認しました。

なお上記の方法は、@adakoda さんが発見された方法を基にし、別の設定画面が最近表示したアプリ一覧に残っていても確実に呼び出せるようにするために若干手を加えさせていただいたものです。

また、呼び出した結果を履歴や最近呼び出したアプリ一覧には残さないようにもしています。

元手順を発見した@adakodaさんに感謝します!

AppOpsがどういうものか、設定状態の確認をどのようにすれば良いかなどについては、当ブログの過去記事「Android 4.3 の隠し機能 AppOpsについていろいろ」や「Effective Android」(達人出版会)をご参照ください。

参考サイト:

Android 4.4 NFCホストカードエミュレーションへの道 (導入編)

本記事は、Android 4.4 KitKat 冬コミ原稿リレーというちょっと気の早いアドベントカレンダー的なものの11/8担当分となります。

Android 4.4にて導入された Host card emulation (以下、HCE)を試したいけど、まず何をすれば良いのかわからないという方向けの導入編となります。

本記事よりも少し踏み込んだ話については、TechBooster様から冬コミに発行されるAndroid 4.4本に寄稿する予定です。

そもそもホストカードエミュレーションとはなにものか?

これまでも、(交通系カードや電子マネーカードなどのように)Android端末が非接触ICカードとして振る舞うことができていましたが、Androidアプリとして独自のカード機能を追加するようなことができませんでした。

Android 4.4 (API Level 19)からは、Androidアプリとして非接触ICカードのふりをするアプリを作成することが可能になりました。NFC規格に含まれる通信方式のうちType A/Bに対応し、通信コマンドはISO 7816-4に規定されたAPDUコマンドを用います。

カードをエミュレートするAndroidアプリがあると何がうれしいのか?

たとえば、以下のような利用方法が考えられます。

  • ICカードアプリ開発のプロトタイピング
  • ICカードを利用した既存システムと、Andorid端末の連携システムの開発

高機能なデバッガを利用できる環境でカードアプリのプロトタイピングができるというのは、システム構築の一部としてカスタマイズしたICカードを開発されている現場では役立つのではないでしょうか。

また、HCEはISO7816-4という標準仕様ベースですので、その標準を用いた既存システムとAndroid端末の連携に利用するということも考えられます。

出来るとしても気軽に手を出してはいけないものはなにか?

この機能を用いて電子マネーシステムを新たに作ろうとするのは高いリスクを伴います。

既存の電子マネーは、Secure Element (以下、SE)と呼ばれる耐タンパ性の非常に高いチップによって、決済に必要となる情報の保護や暗号化の処理を行っています。これと同等レベルのことをソフトウェアで実現することは不可能です。ソフトウェア耐タンパ技術といったものもありますが、ハードウェアによる保護よりも破られるリスクは非常に高いものです。 ((そのリスクを負ってでもチャレンジしようとされる会社さんは出てくると思いますが。))

HCE (API Level 19)でやりやすいこと

いろいろありますが、「Type 4のNFC Forum Tag のエミュレーション」は比較的簡単にできることの一つです。

NFC Forum TagのType 4というのは、ISO7816-4をベースにしていますし、タッチすれば誰でも読めるタグということでセキュリティがかかっていないためです。

HCE (API Level 19)でできないこと

「Felicaを利用したカードのエミュレーション」はできません。FelicaはISO7816-4とは異なるコマンド体系を持っているためです。特に日本ではFelicaベースのシステムが普及しているため、残念な仕様かもしれませんが諦めてください。 ((最近の非接触ICカードR/WはTypeA/B/Felica全対応のものも多いのでハードウェアは使いまわせる場合が多いでしょう。))

NFC HCEアプリ開発のハマリポイント

実は、GoogleのHCEドキュメントですが、間違いがあります(11/8現在)。この通りに実装してもHCEは動作しません。

HCEはサービスとして実装しインストールします。AndroidMannifest.xmlに適切に宣言することで、AndroidシステムがそのHCEサービスを起動してくれます。 ((正しく実装されていれば、インストール後すぐにサービスが起動します。))

しかし、前述のドキュメントには以下のようなマニフェスト断片が提示されていますが、抜けがあるため、システムによるサービスの起動が行われません。自分でサービスを起動しても権限が適切ではないため動作しません。

<service android:name=".MyHostApduService" android:exported="true"
        android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
    </intent-filter>
    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice"/>
</service>

何が間違っているかというと、intent-filter にてカテゴリが宣言されていないためです。以下のようにカテゴリを追加してください。

<service android:name=".MyHostApduService" android:exported="true"
        android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice"/>
</service>

また、NFCを利用するため、以下のpermission宣言も必要です。忘れないようにしましょう。

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

カードアプリを書くために必要な情報

さて、これでカードのふりをするサービスを起動するところまでは出来ました。ではここからどうやってカードアプリを書けばいいでしょうか?

カードアプリには、必ず対向するシステム(たとえばNFCタグリーダ、クーポン読み取り機など)があります。そのシステムが求める機能を実現してください。

え? それでは不親切すぎる? ではとりあえず何か試してみたいということであれば、Type 4 の NFC Forum Tag を試しに実装してみてはいかがでしょうか。仕様書はNFC Forumから入手可能です。 ((弊社でも現在お試し実装にチャレンジ中です。))

Type 4のNFC Forum Tagの仕様書だけあれば、ISO7816-4のAPDUの詳しい知識が無くても実装できます。

暗号化や認証の機能もカードアプリとそれを用いるシステムにて行いたいのであれば、ISOから7816の仕様書(もしくはそれに対応したJIS規格書)を購入する必要が出てくるでしょう。 ICカードに関するISOとJISの対応関係は、こちらのサイトが詳しいです。

実際にお試しいただく際にあると良いもの

  1. NFC HCEに対応したAndroid 4.4端末
  2. 非接触ICカードR/W側となるAndroid 4.4端末

1は11/8時点ではNexus5しか存在しないかもしれません。2はGalaxy NexusにAOSPのAndroid 4.4をビルドして焼いたGalaxy NexusでOKでした。

自分でビルドしても当然良いのですが、@androidsola さんがビルドしたイメージを以下のページにて公開していらっしゃいますので、そちらを利用させていただくのも良いでしょう。

なお、Galaxy Nexus + 前述のAOSP版4.4 ROMイメージでは、Host card emulation非対応のようです。 ((対応端末では、NFCをONにしていれば、設定画面の「アプリ」の下に「タップ&ペイ」というメニュー項目が表示されるのですが、それが表示されませんでした。))

もう一つのハマリポイント、カードリーダによる動作確認

Android端末は古いものでも、NFC対応であればNFC Forum Tagを読み取れるはずと思った方もいるでしょう。NFC Forumタグを実現したHCEアプリを確認するのに、なぜ4.4端末をもう一台用意しないといけないのかと。

実は、NFC Forum Tag を実現したHCEアプリを、API Level 18以下のNFC対応端末に近づけた場合、せっかく作ったHCEアプリが呼び出されません。

なぜそんなことになるかというと、NFCをオンにしたAndroid端末同士を近づけると、まずNFCで規定されたLLCPというプロトコルで接続をしてしまうのです。LLCPが先に解決されてしまい、TypeA/BによるNFC Forum TagのHCEアプリにまで辿り着かないのです。

これを回避するためには、API Level 19にて NfcAdapterに追加されたAPIを用いて、R/W側の端末アプリを作らなければなりません。このことはNfcAdapterのenableReaderMode()メソッドのリファレンスにも以下のように記載されています。

For interacting with tags that are emulated on another Android device using Android’s host-based card-emulation, the recommended flags are FLAG_READER_NFC_A and FLAG_READER_SKIP_NDEF_CHECK.

enableReaderMode()はAPI Level 19からしか使えませんので、どうしてももう一台Android 4.4端末が必要となるのです。

以下に、NFC Forum Tagを確認するR/Wアプリを作るために役立つコード断片を記しておきます。参考にしてください。

public class MainActivity extends Activity {
    .... 
    private NfcAdapter mNfcAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ......

        mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
    }

    @Override
    protected void onResume() {
        super.onResume();

        mNfcAdapter.setNdefPushMessage(null, this);

        mNfcAdapter.enableReaderMode(this, new NfcAdapter.ReaderCallback() {
            @Override
            public void onTagDiscovered(Tag tag) {
                // TODO: ここに受け取ったタグに対しての処理を記載する
            }
        }, NfcAdapter.FLAG_READER_NFC_A, null);
        // NDEFタグ (NFC Forum Tag)を対象にしているので、ここでは enableReaderMode()に
        // 記載されたフラグ FLAG_READER_SKIP_NDEF_CHECK は使わないで、システムに
        // NDEFタグのスキャンを任せる。
    }

    @Override
    protected void onPause() {
        super.onPause();

        mNfcAdapter.disableReaderMode(this);
    }

最後に

本記事の情報を用いることで、ホストカードエミュレーションを試す入り口までたどり着くことはできるのではないかと思いますが、実際にそれを用いてアプリを実装するにはICカード(特にISO7816系コマンド)の知識を必要とします。

また、NFC Forum Tagカードのような静的なものを除けば、カードアプリだけを作ることには意味がありません。システムと連携することが非常に重要です。非接触ICカードの特徴の一つは、近接距離、すなわちタッチ動作をユーザの意思表示として利用するというところにあります。この性質によってユーザ体験(UX)を改善できるシステムでなければ、Bluetoothなどの他の通信を利用したシステム構築も選択肢とすべきです。

と、少しハードルを上げてしまいましたが、何はともあれどんな技術についても言えることですが、まずは触れてみましょう。その上で、自分が作りたいものに向いているのかいないのかを考えましょう。

触れてみて理解してから導入可否を検討するのは大変だ? その場合、弊社にて相談に乗れるかもしれませんのでお気軽にご連絡ださい(とさりげなく宣伝)。