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

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

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

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() が使われていますので、それらの機能にアクセスしているアプリでは例外処理の追加についても考慮しなければならないかもしれません。