Android Wear向けにアプリを作ってみた (2014/07/01 訂正)

(2014/07/01 アプリのインストール手順とapkファイルを更新しました。)

Android Wear端末での音声入力と、Wear端末とコンパニオン端末の相互通信のプログラミング方法の調査ついでにちょっとしたアプリを作ってみました。

コンパニオン端末(スマートフォン)用アプリをダウンロードして、コンパニオン端末に adb install などでインストールしてください。

このアプリのassetsにwear用アプリのapkも含まれており、コンパニオン端末にアプリをインストールすると、wear端末にはassets内に含まれたapkファイルがインストールされるようです。wear端末へのアプリインストール完了には少し時間がかかるので少々お待ちください。

なお、コンパニオン端末にアプリをインストールしたタイミングだけwear端末にアプリがインストールされるのか、新規のwear端末に自動的にコンパニオン端末上のアプリが持っているwearアプリがインストールされるのかといったタイミングについてはまだ調査しておりません。

device-2014-06-29-231622

両端末へのアプリインストールが成功していれば、コンパニオン端末アプリのボタンを押すとWear端末側のアプリが起動します。

device-2014-06-29-231444

Wear端末の音声入力ボタンを押してください。

device-2014-06-29-231454

音声入力画面が起動するので、「バンザイ」と音声に入力してみてください。

device-2014-06-29-231648

すると、コンパニオン端末上のドロイドくん(Android Robot)がバンザイします。3秒ほどすると元のポーズに戻ります。

実は他に一つポーズを隠しで入れてあります。Wearが腕時計型であることとWearアプリに出ている言葉から類推すると隠しコマンドを見つけやすいかもしれません。

ソースはもうちょっと整理したら公開するかもしれません。

Enjoy Android Wear!

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」(達人出版会)をご参照ください。

参考サイト: