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

以上です。

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などの他の通信を利用したシステム構築も選択肢とすべきです。

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

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

2D/2.5Dゲームエンジン Playgroundのセットアップ

KLab株式会社さんが、9/25付で2Dゲーム(および2.5Dゲーム)用のゲームエンジンであるPlaygroundをオープンソースで公開されました(KLabさんによるプレスリリース )。

さっそく動作を確認してみようとされた方も多いと思いますが、Android版はハードルの高さに挫折された方も多いのではないでしょうか。弊社でも苦労しました。
まずドキュメントが英語です。この時点ですでに振り落とされる方も多いでしょう。そして、9/26時点でのドキュメントにはいくつか不備があります。

弊社では試行錯誤の結果、MacOS Xとその上で動作させているWindows7 VMを組み合わせて、Playgroundチュートリアルとして公開されているサンプルのビルドおよびAndroid上での動作確認まで行うことができました。その備忘録として、セットアップ手順をまとめておこうと思います。おそらく、数日中にPlayground側のドキュメントも改善され、この備忘録も不要になっていくだろうと思いますが。

(9/28追記: すでにかなりの修正がPlaygroundOSSで行われているので、ハマりポイントはこの記事執筆時点よりも減っていってます。)

Playgroundの取得

まずはgithubで公開されているPlaygroundのリポジトリからソースを取得します。ついでに作業用ブランチを作成します。

% git clone https://github.com/KLab/PlaygroundOSS.git
% cd PlaygroundOSS
% git checkout -b working

masterブランチ上で作業していて衝突(conflict)が発生すると面倒だったりする場合がありますので、ここでは念のためローカルブランチを作って作業しています。

Playgroudに含まれるもの

取得してきたものがそれぞれどういう役割を持っているか知っていた方がこれからどういう作業をしているのか理解しやすくなるので、少し説明します。

CSharpVersion
C#での開発に関連したファイルが入っています(調査中)。
Doc
ドキュメント類が入ってます。基本的に英語ドキュメントなので読むのに苦労します。また、9/26現在ではまだAndrodでのビルド方法などのドキュメントに不備がありますので、気をつけてください。
Engine
Playgroundのゲームエンジンそのものです。この下にある各プロジェクト用のひな形ファイルと、後述するassetsを組み合わせることで実行できるアプリをビルドすることができます。
SampleProject
紛らわしいですが、これはサンプルアプリが入っているディレクトリではありません。後述するエンジンのビルド時に用いるファイルが格納されています。エンジン本体をビルドする際には、このディレクトリ名を指定しなければなりません。
Tools
Windows用のassets作成ツールTobogganが入ってます。Tutorial以下に入っているサンプルをビルドするためには、このツールを使ってassetsを作成しないといけません。どうやらWindowsを用いなくても、monoでassetsの発行はできるらしいです(調査中)。
Tutorial
Luaを用いたサンプルプログラムがまとめられたzipファイルが入っています。本記事ではこちらのサンプルのビルドと実行までを確認します。

AndroidでPlayground を試すのための事前準備

開発環境インストール

Android SDKのインストール、Elicpseのインストール、EclipseへのADTのインストール、Android NDKのインストールを行ってください。
それぞれのインストールと設定の手順については本記事では省略します。各種のAndroid開発入門サイトや書籍を参考にしてください。

環境変数の設定

  • 環境変数 ANDROID_NDK_ROOT にNDKの場所を設定してください。.profileや.bash_profileなどに以下のような行を追加すると良いでしょう。export ANDROID_NDK_ROOT=$HOME/android-ndk-r9
  • 同様に、環境変数 ANDROID_SDK_ROOT にSDKの場所を設定してください。

    export ANDROID_SDK_ROOT=$HOME/adt-bundle-mac-x86_64-20130917/

  • 環境変数PATHにNDKとSDKを追加してください。

    PATH=$PATH:$ANDROID_NDK_ROOT:$ANDROID_SDK_ROOT/platform-tools

Playgroundエンジンのビルド

githubから取得した Playgroundは $HOME/PlaygroundOSS にあるという前提で説明します。

以下のコマンドを実行します(chmod は初回のみ必要です)。

% cd $HOME/PlaygroundOSS/Engine/porting/Android/GameEngine-android
% chmod +x ./build.py
% ./build.py --rebuild --project SampleProject

引数に指定するプロジェクト名は、SampleProjectでなければ成功しません。他のプロジェクト名を使いたい場合は、前述したSampleProjectディレクトリを複製して別の名前に変更し、さらには中のファイルもいろいろいじらないといけないと思われます。

ここではとりあえずサンプルのビルドと実行を目指していますので、上記のカスタマイズ方法については追求しないこととします。

環境設定が正しく行われていれば、$HOME/PlaygroundOSS/Engine/porting/Android/GameEngine-android/libs/ 以下の x86ディレクトリとarmeabiディレクトリの下に libGame.so ができるはずです。

Tobogganを用いて assets を発行する

ゲームエンジンはできましたが、それだけではゲームは動きません。ゲームの動作を記述したプログラムとデータを組み込む必要があります。
動作確認できるサンプルがTutorialディレクトリに用意されていますので、それで試しましょう。

先に述べたTobogganアプリを用いて、ゲームエンジンが利用できる形にassetsを発行する必要がありますので、ここからの作業はWindows上で行います。

Toboggan のインストールと実行

  • Tools/Toboggan-Tools.zip を展開してください。その中にできる Toboggan ディレクトリを適当な場所に置いてください。
  • Toboggan/KLBToolHost.exe を実行してください。

assetsの発行(publish)

  • Tutorial/Samples.zip をどこかに展開してください。たくさんのディレクトリができてしまいますので、どこか展開用のディレクトリを作成して、そこに展開するようにした方が良いかと思います。 9/28追記: 最新のリポジトリではすでに展開済みになっていますので、この手順は不要です。
  • 実行しているTobogganから、01.SimpleItemを開きます。 (メニューの File -> Open)
  • メニューの Tools -> Setup で開かれるダイアログに “Publish Targets” という項目がありますので、androidにチェックを入れてください。
  • メニューの File -> Publish All で発行処理を行ってください。
  • 01.SimpleItem/.publish/android ディレクトリに発行されたデータがありますので、このディレクトリをまるごとMacOS X環境にコピーしてください。

発行されたassetsをまとめる

ここから再び、作業環境はMacOS Xに戻ります。

前項で発行されたassetsは、AppAssets.zip というファイルにまとめなければなりません。Windowsでもこの作業は可能かとは思いますが、念のため、Playgroundの手順に従って、zip コマンドを用いて行いましょう。以下のコマンドを実行して下さい。

% cd .publish/android
% zip -0 -r <どこか適当なディレクトリ>/AppAssets.zip ./*

zip の最初のオプションは-O (オー)ではなく -0 (ゼロ)です気をつけてください。これは非圧縮でzipファイルを作成するためのオプションです。

また、再帰的にサブディレクトリもアーカイブしておかないと、参照するデータがAppAssets.zipに含まれないという事故も起こってしまうので、-r オプションもつけましょう。

Eclipseへのプロジェクトのインポート

Eclipse に Playground 用の Androidプロジェクトを作成しなければなりませんが、これはすでに用意されているコードをEclipseにインポートすればOKです。英語版Eclipseのメニューで説明しますので、日本語化されている場合は適宜読み替えてください。

  • File -> Import を選択し、開いたダイアログで Existing Android Code into Workspace を選びNextボタンで次に進みます。
  • Browsボタンを押して、$HOME/PlaygroundOSS/Engine/Android/GameEngine-android を選択します。
  • Copy projects into workspace にチェックを入れ、Finishボタンを押します。

AppAssets.zip のEclipseプロジェクトへの組み込み

インポートしたElicpseプロジェクトは、Playgroundのゲームエンジンが入っているだけで、ゲームプログラム部分はまだ入っていません。それは AppAssets.zip としてまとめたものになりますので、このファイルをプロジェクトの assets フォルダに入れてください。

また、プロジェクトのassetsフォルダに version というファイルを新規作成し、その中に 1 とだけ記載してください。Playgroundのドキュメントに記載されている通り、 echo コマンドを用いて version ファイルを作成しても構いません。

AppAssets.zip を別のファイルに入れ替える際には、versionファイルの中身も書き換えましょう(たとえば数字をインクリメントしましょう)。AppAssets.zip 入れ替え前と違う数になっていればOKです。

プロジェクトのビルド

ここまででPlaygroundドキュメントに記載されているビルドできるプロジェクトの準備ができました。ではビルドしてみましょう。たいていの方はここで失敗します。

さて、何が悪いのでしょうか?実はインポートしたEclipseプロジェクトがあなたの環境にあっていないのです。

ということで、プロジェクトを選択し、右クリックしてコンテキストメニューからPropertiesを選んで設定画面を開いてください。以下の設定項目をご自身の環境に合わせて修正してください。

  • C/C++ の下のEnvironmentを選び、右側に表示された環境変数の一覧のうち、ANDROID_NDK_ROOT, NDKROOT, NDK_ROOTをご自身の環境に合わせて修正します。
  • C/C++ を選び、右側に表示された Build Command の項目を、${ANDROID_NDK_ROOT}/ndk-build -j に修正します。

プロジェクトの実行

ビルドが成功したら実行してみましょう! PlaygroundゲームエンジンはOpenGLを用いているので、Androidではエミュレータではなく実機を使った方が良いでしょう。

最後に

ビルド手順についてまとめたドキュメントの不備については、すでにPlaygroundのオープンソースプロジェクトの方に伝達していますので、Github上のドキュメントも今後改善されていくでしょう(9/28追記:改善作業が着々と進んでいます)。

オープンソースの2Dゲームエンジンとしては、cocos2d-x が有名です。また製品では、Unity も今秋の4.3から2Dゲーム開発を支援する機能を提供していくことが発表されています。Playground はそれらと競合するという位置づけです。

プレスリリースによると、Playgroundゲームエンジンは、音の遅延が小さく、リズムアクションゲームなどに適しているなどの強みがあるとのことです。

Playgroundについては、すでにハッカソンの企画も動いています。弊社もハッカソンの参加や、開発情報の共有などを通じて貢献できればと考えております。

Android 4.3: NotificationListenerServiceマニアックスとAndroidセキュリティ観測

Android 4.3では、NotificationListnerService という、アプリが、他のアプリも含めて通知バーに追加された通知の情報を取得することが出来るという機能が追加されています。

このNotificationListnerServiceとリフレクションを用いたサンプルアプリをgithub上に公開しております

本記事では、NotificationListenerServiceとはどういうものか、基本的な使い方と踏み込んだ使い方、そしてNotificationListenerServiceを含めたAndroidのセキュリティについての考え方の方針転換の可能性についてを書きたいと思います。

NotificationListenerServiceとは?(基本的な使い方)

4.2以前のAndroidでは、自分のアプリの通知を通知バーに登録することは出来ました。4.3では、他のアプリが通知を行ったことを知り、それをトリガにして処理を実行することが出来るようになりました。 ((裏ワザ的なAccessibilityService使用によって、同様のことは出来たりはしますが))

ユーザは以下のように NotificationListnerService の派生クラスを作り、

public class MyListenerService extends NotificationListenerService {

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        /* 通知が追加された時に呼ばれる */
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        /* 通知が通知バーから削除された時に呼ばれる */
    }
}

AndroidManifest.xml にその派生クラスがNotificationListnerServiceの権限でのアクセスのみを受け付けることを宣言します。

    <service
        android:name=".MyListenerService"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
    </service>

これによって、MyListenerSeriviceはBIND_NOTIFICATION_LISTENER_SERVICE のpermissionを持つものからの要求のみを受け付けるようになります。
BIND_NOTIFICATION_LISTNER_SERVICE は一般ユーザが使用することが出来ないprotectionLevelを持っているため、このサービスの機能を呼び出すことが出来るのはAndroid 4.3のシステムに組み込まれて適切な権限を持っているものからのみとなります。

前述のpermission宣言により、アプリ自身からもこのサービスを起動することは出来ませんが、[設定]->[セキュリティ]->[通知へのアクセス]の設定に従って自動的にシステムにより起動されます。以下はセキュリティ設定画面のキャプチャです。

security-setting

「通知へのアクセス」という項目は、実際にNotificationListnerServiceを用いたアプリを手順に従って端末にインストールするまではあらわれません。
最初、アプリをインストールしただけでは、そのアプリの使用許可は与えられていない状態です(下図参照)。

setting-notificationlistener01

チェックボックスをオンにすることでインストールされたアプリを有効化することが出来ますが、その時に非常に刺激的な警告文が表示されます。

setting-notificationservice02

非常にいろんなことが出来るので、信頼出来るアプリにしか許可を与えてはいけないという警告文です。この警告を受けた上でOKボタンを押すと、チェックボックスが有効になり、インストールしたアプリのNotificationListnerServiceを派生したサービスが起動します。逆にチェックボックスを外すと、サービスは停止します。

このようにして、システムから起動されたサービスは新しい通知が通知バーに登録される度にonNotificationPosted()が呼び出され、通知が削除される度にonNotificationRemoved()が呼び出されますので、それらをトリガとして処理を行うことが可能となります。

NotificationListnerServiceの踏み込んだ使い方

NotificationListenerService#onNotificationPosted()の引数として渡されるStatusBarNotificationからは、通知元のパッケージ名や通知が作成された時間、Linux UID、Notificationクラスのインスタンスなどを取得することが出来ます。しかしそれらの情報を用いてできることはそんなにたいしたことではありません。先ほど、機能を有効にする際にダイアログで警告されたようなことはほとんど出来ません。

しかしあれは単なる脅しではありません。Androidでは非公開のAPIもリフレクションを用いて呼び出すことができます。リフレクションによって先のダイアログにて警告されたような機能を実現することは可能です(本記事では述べません)。

以下のサンプルコードは、StatusBarNotificationのインスタンスからメールが到着した時にそのGmailを表示するためにPendingIntentを取得し、そのPendingIntentに紐付けられたIntentを送る方法を示します。

public class MyListenerService extends NotificationListenerService {
    private static final String TARGET_APP_PACKAGE = "com.google.android.gm"; // Gmail Application

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
    // return if the notification isn't from the target
    if (!TARGET_APP_PACKAGE.equals(sbn.getPackageName())) {
        return;
    }

    Notification notification = sbn.getNotification();

    Intent intent = new Intent();
    PendingIntent pendingIntent = notification.contentIntent;
    try {
        pendingIntent.send(this, 0, intent);
    } catch (PendingIntent.CanceledException e) {
        e.printStackTrace();
    }
}

実際に動作するサンプルコード全体は github で公開しておりますので、そちらをご参照ください。

NotificationListenerService のセキュリティ

従来のpermissionモデルであれば、ユーザがアプリをインストールした時点で、そのアプリが宣言していた機能をすべて使用させることになりますが、NotificationListenerServiceはその方法で通知バーの情報を保護しません。

通知バーにアクセスをする機能を持ったアプリをユーザがインストールし、なおかつ、ユーザが明示的に通知バーへのアクセスを許可するように設定を変更することを求めます。そして、その設定変更時には非常に強い警告を表示することで、ユーザが安易に設定を変更しないことを求めています。

このようにして、アプリのインストール後にユーザがアプリに対して機能の利用をON/OFFするというのは、弊社ブログでも紹介したAppOpsでも行われています。これは、Androidのセキュリティについての考え方が変化してきている兆候だろうと弊社では考えています。

騙してインストールさせて、電話帳のデータや端末固有情報(機体番号や電話番号)などを吸い出し、サーバに勝手に送信してしまうようなアプリが社会的な問題となりました。そのため、アプリ作成時に利用する権限を宣言し、インストール時に確認をさせるだけでなく、インストール後にも与えた権限を剥奪出来るようにすることや、重大な機能はユーザが明示的に許可するまでは使用できなくするようにGoogleはAndroidのセキュリティについての考え方を変化させつつあると判断しています。

また、通知バー広告を使ったアプリの排除など、Google Playを通じたアプリの配信でもセキュリティ強化は進んでいます。

特にGoogle Playを通じて配信する場合は、Androidでのセキュリティについての考え方の変化についてもしっかりと考慮し、誠実なアプリ開発を行っていくことが必要です。

Android 4.3 の隠し機能 AppOpsについていろいろ

AppOpsとは?

Android 4.3 には、まだ公開されていない隠し機能として AppOps というものが追加されています。これまでとは別のアプローチで、 ユーザが自分自身の情報(電話帳や位置情報など)を守ることが出来るようにするというものです。

追記(2013年11月5日):Android 4.4 KITKATでは、AppOps機能の入口となるActivityが設定アプリのAndroidManifest.xmlから削除(コメントアウト)されています。よって、素のAndroid 4.4 KITKAT では AppOps機能を使用することはできません。 ((当該Activityをコメントアウトをすることで機能を使えなくしているため、将来バージョンにて正式版として復活させる可能性が高いのではないかと思われます。))

追記(2013年11月5日その2):Android 4.3.1 の時点ですでに塞がれていたという情報をいただきました。
Android 4.3端末で設定を行った後に、4.3.1または4.4にアップデートするとその設定をもとに戻せなくなる可能性がありますので、アップデートを予定されている方(アップデートが提供される端末をお持ちの方)は、その点にご注意の上でお試しください。

追記(2013年11月21日):Android 4.3.1と4.4でもAppOpsを呼び出す方法が発見されました

AppOpsの紹介記事はすでにいろいろあるようですので、そちらを参照されると大まかな機能についてはわかるかと思います。たとえば、engadget日本語版の記事など。

従来のAndroid 4.2まででは、アプリ作成時にそのアプリがどういう権限を使いますよ(どういう機能を使いますよ)ということを宣言しておき、ユーザがインストール時(アップデート時)に、それを確認して問題が無いと判断すればインストールしていました。一旦インストールされてしまえば、アプリは宣言した権限の範囲内の機能を自由に使用することが出来ます。

4.3では、アプリのインストール後に、インストール時に許可した機能であってもアプリに使わせなくすることが出来るようにユーザが設定できる機能が、隠し機能として搭載されています。 ((Android 4.1で隠し機能としてマルチユーザ機能が搭載され、4.2にて正式機能として格上げされた事例がありますので、この機能も近く正式なものになるものと思われます。))

AppOps時代に、アプリ開発者が気をつけないといけないことはあるのかどうかは知りたいところです。ということで、Android 4.3の公開されているソースコードから読み取れる情報などからいろいろと調べてみます。 ((正式機能となった時には変わっているかもしれませんので、あくまでもAndroid 4.3向けの情報です。))

アプリからAppOps画面を呼び出す方法

Google Playで”Permission Manager”という名称で、隠されているAppOpsの設定画面を呼び出す機能を持ったアプリがいくつか出ていますが、単にその画面を呼び出すだけなら自分で作るのも簡単です。

    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_MAIN);
    intent.setClassName("com.android.settings",
        "com.android.settings.Settings$AppOpsSummaryActivity");
    startActivity(intent);

単にこれだけでOKです。setAction()で指定するアクションはアプリでの呼び出し目的に合わせて調整する方がいいかもしれませんが。

AppOpsで制限された機能を使った場合、アプリはどうなる?

アプリが 、マニフェストファイルで uses-permission 宣言していない、権限が必要な機能を使用した場合、その機能を利用した瞬間に例外(SecurityException)が発生します。では、AppOpsでユーザが制限した機能を使用した場合はどうなるでしょうか。同様に例外が発生するでしょうか?

Android 4.3のソースコードを確認したところ、これまでのAPI仕様に記載されていないような例外を発生させないようにしつつ、その機能を使わせないようにしようという意図が見て取れます。たとえば、電話帳データへのアクセスなどの様々な機能のベースとして用いられているContentProviderを見てみます。そのインナークラスのTransportの query() メソッドは次のようになっています。

    @Override
    public Cursor query(String callingPkg, Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        ICancellationSignal cancellationSignal) {
        if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) {
            return rejectQuery(uri, projection, selection, selectionArgs, sortOrder,
                CancellationSignal.fromTransport(cancellationSignal));
        }
        return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder,
            CancellationSignal.fromTransport(cancellationSignal));
    }

ここから呼び出されている enforceReadPermission() メソッドでは、呼び出し元アプリのパッケージとアクセスするContentProviderのURIから、アクセス可否を判定する処理を行って結果を返します。アクセスが許可されている(ユーザがAppOpsでアクセス許可をOFFにしていない)場合、AppOpsManager.MODE_ALLOWED が返ってくるので、それと比較して処理を分けています。

MODE_ALLOWEDではない値が返ってきた場合は rejectQuery() を呼び出してその結果を返し、そうでない場合は通常のクエリを行って結果を返します。rejectQuery() では何をしているかというと、以下のようなコードで、絶対に検索結果が空になるクエリを行った結果を返します。 ((‘A’ = ‘B’ という必ずfalseになる条件を追加したクエリにすることで実現しています。))

    public Cursor rejectQuery(Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
        // The read is not allowed...  to fake it out, we replace the given
        // selection statement with a dummy one that will always be false.
        // This way we will get a cursor back that has the correct structure
        // but contains no rows.
        if (selection == null || selection.isEmpty()) {
            selection = "'A' = 'B'";
        } else {
            selection = "'A' = 'B' AND (" + selection + ")";
        }
        return query(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal);
    }

アプリからAppOpsによる制限状態を知る方法

自分の作ったアプリが使っている機能がAppOpsで制限されていたとしても、アプリはクラッシュしないから問題ないですね、ってそんなわけはありません。空っぽのデータが返ってきた時、データが無くて取れなかったのか、ユーザが制限していて取れなかったのかを区別しないといけない場合は多いのではないでしょうか。たとえば、アプリが利用している機能へのアクセスが制限されて必要な情報が得られない場合、アプリを活用するためには制限をかけないでくださいということをユーザに通知する必要があります。 ((ユーザにこっそり電話帳データを抜き取ってサーバに送るようなアプリは制限されてしかるべきだと思いますが、位置に基いた情報を提供する有益なアプリで、位置情報取得を制限されると何の役にも立たなくなってしまいます。))

ということで、アプリ開発者は、自分が使おうとしている機能がAppOpsで制限されているかどうか確認出来ないといけなくなるでしょう。しかし残念ながら、まだこの機能は非公開の隠し機能であるため制限状態を知る方法も公開されていません。

ただし、内部的にその確認のために利用しているメソッドが存在しているので、リフレクションでメソッドを呼び出すことで確認することが可能です。以下にそのコード例を示します(電話帳を読む機能が制限されているかどうかを確認する例です)。

    /* acquiring AppOpsManager class and its checkOpNoThrow method */
    Class appOpsClass = null;
    Method checkOpNoThrowMethod = null;
    try {
        appOpsClass = Class.forName("android.app.AppOpsManager");
        checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow",
            Integer.TYPE, Integer.TYPE, String.class);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

    /* invoking checkOpNoThrow method to get AppOps access mode */
    Object appOps = getSystemService("appops");
    Object ret = null;
    int appOpsMode = 1; /* AppOpsManager.MODE_IGNORED */
    if (checkOpNoThrowMethod != null && applicationInfo != null) {
        try {
            ret = checkOpNoThrowMethod.invoke(appOps,
                    4, /* AppOpsManager.OP_READ_CONTACTS */
                    applicationInfo.uid, /* Linux UID */
                    this.getPackageName() /* package name of this application */
            );
            appOpsMode = ( (Integer)ret ).intValue();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    if (appOpsMode == 0) { /* AppOpsManager.MODE_ALLOWED */
        /* execute reading contacts because allowed */
        Log.d(TAG, "allowed");
    } else if (appOpsMode == 1) { /* AppOpsManager.MODE_IGNORED */
        /* don't read contacts because not allowed */
        Log.d(TAG, "not allowed");
    } else { /* AppOpsManager.MODE_ERRORED */
        /* dont' read contacts because of error */
        Log.d(TAG, "error");
    }

少しサボって、いくつかの値を即値で書いているのはご勘弁を。このコードで何をやっているかというと以下の通りです。

  • AppOpsManagerクラスの中で制限状態取得処理を司る checkOpNoThrow() メソッドを取得する
  • getSystemService() で AppOps サービスのインタフェースオブジェクトを取得する
  • そのオブジェクトの checkOpThrow() に、チェックしたい機能、このアプリのLinux UID、このアプリのパッケージ名を渡して、利用可能かどうかの判定結果を取得する ((パッケージ名だけでなくLinux UIDも判定に用いているのは、Android 4.2から導入されたマルチユーザ機能でユーザごとに異なる制限をかけるためと思われます。))
  • 判定結果に応じてログを表示する

チェックしたい機能を示す数値は、AppOpsManagerの中で定義されていますので、自分がチェックしたいものに合わせて変更してみてください。

おまけ

通知機能へのアクセスや、クリップボードの読み書きなど、従来ではパーミッションによる制限が無かった機能に対しても、AppOpsは機能制限を行えるようにしています。パーミッションの機能を拡張するのではなく、AppOpsをパーミッション機構とは別のものとして導入しているからこそ出来ることです。

もしかすると、正式版の時点では、パーミッションとしては従来通りの粒度で大きく与えておき(例:READ_PHONE_STATE)、AppOpsによって細かい粒度で機能を制限するということをやってくるのかもしれません(例:AppOpsでIMEIなどの端末固有ID取得を制限する)。

おまけ2

AppOpsManager では、機能が利用可能かどうかを判定するメソッドとして、checkOpNoThrow() だけでなく checkOp() も提供しています。名称からわかるように、前者は判定に失敗した場合に例外を投げず、後者は例外(SecurityException)を投げるものとなっています。

クリップボードとカメラ機能の制限チェックでは checkOp() が使われていますので、それらの機能にアクセスしているアプリでは例外処理の追加についても考慮しなければならないかもしれません。

Android Studio (IntelliJ IDEA) の Language Injection

Android Studio (およびそのベースとなっているIntelliJ IDEA)にはLanguage Injection というおもしろ機能があります。I/Oのセッションでも簡単に説明していた正規表現チェックの例を使ってみます。

まず、以下は(Javaのリファレンスにも載っているような)非常に簡単な正規表現チェックコードの例です。aが0回以上とbという文字列が含まれるかどうかチェックしています。

正規表現コード例
正規表現コード例

ここで正規表現(“a*b”)にカーソルを合わせて、Alt+Returnでポップアップメニューを表示し、Inject Language を選択……

Inject Language
Inject Language

さらにどんな言語の機能を差し込むのか選択が出るので、Regular Expression(正規表現)を選択します。

Inject RegExp
Inject RegExp

これで準備完了です。再度 Alt+Returnでポップアップメニューを呼び出すと、正規表現チェックのメニューが増えているかと思いますので、それを選択肢ます。

Check RegExp
Check RegExp

すると、正規表現をチェックするダイアログのようなものが表示されます。

正規表現チェッカ登場
正規表現チェッカ登場

ではここに正規表現とマッチしない文字列を入れてみます。

正規表現がマッチしなければ赤
正規表現がマッチしなければ赤

マッチしないので赤表示となります。次にマッチする文字列を入れます。

正規表現がマッチすれば緑
正規表現がマッチすれば緑

マッチするので緑に変わりました。

ということで、プログラム開発中に、テストを動かすまでも無く簡易に正規表現が意図通りに動作するものとなっているかを確認することができます。 (( ちゃんとテストコードも書くべきなのは当然です。 ))

もうここではこのLanguage Injectionを使わないぞと思ったら、取り除くこともできます。

不要になったらun-inject
不要になったらun-inject

ちょっとした機能ではありますが、これも開発効率アップに貢献してくれる機能の一つです。