こんにちは。
何をしようと思い立ったのか
「Androidプリインストールアプリ「Contacts」にContentResolverでアクセス」という記事を投稿しましたが、この記事では自作Androidアプリから、プリインストールされているContactsという連絡先アプリの持つデータを取得(検索・読み込み)しかしていませんでした。マニフェストにも、<uses-permission android:name="android.permission.READ_CONTACTS" />
しか書いてませんでしたし。
そこで今回は、連絡先アプリにデータの登録(挿入・書き込み)をチャレンジしてみようと思い立ったのです。
趣旨、が途中から変わってきた
ところが。
Android 6.0(API Level 23・Marshmallow)以降、パーミッションの仕様が変更されました。Android Developersサイトの「システム パーミッション」の特に「Normal パーミッションと Dangerous パーミッション」にご注目です。
アプリが Dangerous パーミッションが必要性であると宣言している場合、ユーザーはアプリに明示的にパーミッションを付与しなければなりません。
Marshmallowより以前では、アプリをインストールする際に「このアプリケーションに許可する権限」と一覧表示されて、ユーザにインストールするか否かの選択を迫ってきました。
しかし、Marshmallow以降の端末では、インストールはサクッとしちゃって、いざそのアプリでパーミッションを使わなければならない段になった際にユーザに以下の画面のように問うてくるようになりました。
私は当初、「んじゃあ、こういうDialog(がAPIに在るんだろうから)を作ってnewしてshowしてOnClickListenerを実装して...」ということを推測したのですが、真相はさにあらず。このダイアログ、作らない(定義しない)のです。
などなど、当初は単に『連絡先アプリに書き込みたい』だけが目的だったのですが、やおらMarshmallow未満と以降のパーミッションの扱いについて調べる、という風向きというか趣旨が変わってきちゃって、の顛末をここに記します。
結論
Marshmallow以降では、以下のようなプログラムの流れでパーミッションの要求・許可・利用がなされます。
- マニフェストには、READ_CONTACTSとWRITE_CONTACTSの両方を書く
- [ContextCompat#checkSelfPermission](https://developer.android.com/reference/android/support/v4/content/ContextCompat.html#checkSelfPermission(android.content.Context, java.lang.String))で、パーミッションがアプリに付与されているか確認する
- [ActivityCompat#requestPermissions](https://developer.android.com/reference/android/support/v4/app/ActivityCompat.html#requestPermissions(android.app.Activity, java.lang.String[], int))で、READ_CONTACTSパーミッションを要求する(ユーザに許可を求めるダイアログを表示する)
- あら?WRITE_CONTACTSパーミッションの要求はしなくてもイイの?
- しなくても、イイんです。その2つのパーミッションは、同じ「パーミッション グループ」に所属しているので、片方だけ許可すればOK
- あら?WRITE_CONTACTSパーミッションの要求はしなくてもイイの?
- [ActivityCompat.OnRequestPermissionsResultCallback#onRequestPermissionsResult](https://developer.android.com/reference/android/support/v4/app/ActivityCompat.OnRequestPermissionsResultCallback.html#onRequestPermissionsResult(int, java.lang.String[], int[]))は、ユーザ選択の結果を受けるコールバックメソッドだ
なお、Marshmallow未満の場合のプログラムの挙動についてもあとで記します。
環境
Android Studio 3.0.1
Build #AI-171.4443003, built on November 10, 2017
JRE: 1.8.0_152-release-915-b01 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
使用言語はJavaオンリー。Kotlin使いません。
作ってみたアプリ(をMarshmallowのエミュレータ上で動作確認)
開発さんのように、こんなアプリ開発してみた。ブンブン、ハローQiita。
なお、事前に連絡先アプリに2件登録しておきます。
作ったアプリの初期画面では、ボタンがあります。Marshmallowのエミュレータ上で動作確認しました。
アプリ名は気になさらないでください。いろいろ趣旨から外れだして、Normal/Dangerousパーミッションのことを調べていたら、こんな名前のアプリにしちゃっていました。
ボタンを押します。
パーミッションの許可をユーザに求めるダイアログが出ます。1回試しに「許可しない」と天邪鬼ってみます。
そんな天邪鬼には、トーストを出してやりましょう。
ごめんなさい、改心しました。もう一度「連絡先を読みこむ」のボタンをクリックします。
今度こそ、「許可」のボタンをクリックします。
3件出ます。そのココロは、徳川家康を登録したうえで全件検索したからです。
念のため、今一度、連絡先アプリを見てみます。
私のアプリのせいで、やっぱり徳川家康は登録されていました。
プログラム
初期画面
package jp.co.casareal.normaldangerouspermissions;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
/**
* 連絡先アプリの持つデータを読み込むパーミッションが付与されている判定する画面
*/
public class MainActivity extends AppCompatActivity {
// この定数は要件に応じて用意する
private static final int REQUEST_CODE_READ_CONTACTS = 123;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View button = findViewById(R.id.button);
button.setOnClickListener(new OnClickReadContactsListener());
}
/**
* クリックイベントのリスナー
*/
private class OnClickReadContactsListener implements View.OnClickListener {
@Override
public void onClick(View v) {
// パーミッションがアプリに付与されているか確認する
if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
Log.v("MainActivity", "API Level = " + Build.VERSION.SDK_INT + ": パーミッションが付与されていない");
// パーミッションが付与されていない場合、
// パーミッションを要求する(ユーザに許可を求めるダイアログを表示する)
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE_READ_CONTACTS);
} else {
// API Level 23未満(Marshmallow)は、問答無用でこのelseブロックになります
Log.v("MainActivity", "API Level = " + Build.VERSION.SDK_INT + ": パーミッションが付与されている");
// パーミッションが付与されているので、画面遷移します
transitionContactList();
}
}
}
/**
* ユーザ選択の結果を受けるコールバックメソッド
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
Log.v("MainActivity", "API Level = " + Build.VERSION.SDK_INT + ": requestCode = " + requestCode);
switch (requestCode) {
case REQUEST_CODE_READ_CONTACTS:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// パーミッションの利用が許可されている
transitionContactList();
} else {
// パーミッションの利用が許可されていない
// 今回はトーストを出してお茶を濁す程度
Toast.makeText(this, String.format("連絡先アプリの利用が許可されていません"), Toast.LENGTH_LONG).show();
}
return;
}
}
/**
* 連絡先一覧を表示する画面へ遷移する
*/
private void transitionContactList() {
Intent intent = new Intent(getApplicationContext(), ContactAccessActivity.class);
startActivity(intent);
}
}
初期画面(MainActivity.java)のポイント
private static final int REQUEST_CODE_READ_CONTACTS = 123;
のように、リクエストコードを任意のint値で用意しておきます。
ContextCompat#checkSelfPermissionメソッドで、パーミッションがアプリに付与されているか確認します。お目当てのパーミッションは第2引数で指定します。
この時if文で評価し、偽の場合、ユーザに許可を求めるダイアログを表示するべくの、ActivityCompat#requestPermissionsメソッドを呼び出しています。
このActivityCompat#requestPermissionsメソッドの第2引数と第3引数が、コールバックメソッドであるonRequestPermissionsResultメソッドの第1引数と第2引数となります。
というわけで、onRequestPermissionsResultメソッドをオーバーライドし、ifやswitchなどでパーミッション利用の許可の可否に応じた処理をコーディングします。
ポイントをまとめます。
- ContextCompat#checkSelfPermissionメソッドで、利用したいパーミッションがアプリに付与されているか確認する
- 『ユーザに許可を求めるダイアログ』は作らない
- そのダイアログを出すのは、ActivityCompat#requestPermissionsメソッドだ
- コールバックメソッドonRequestPermissionsResultをオーバーライドして処理を書く
-
ユーザに許可を求めるパーミッションは、READ_CONTACTSだけだ!
-
WRITE_CONTACTSは求めていない!
-
でも大丈夫!WRITE_CONTACTSは、READ_CONTACTSと同じ、Permission Groupに所属しているので!
-
でも大丈夫!WRITE_CONTACTSは、READ_CONTACTSと同じ、Permission Groupに所属しているので!
-
WRITE_CONTACTSは求めていない!
Android Developersサイトに、以下のように記載されています。
アプリがマニフェストに記載された Dangerous パーミッションをリクエストし、そのアプリがすでに同じパーミッション グループに属する別の Dangerous パーミッションを付与されている場合は、システムはユーザーに確認することなく、すぐにパーミッションを付与します。たとえば、アプリが以前に READ_CONTACTS パーミッションをリクエストして許可されており、さらにWRITE_CONTACTS をリクエストした場合、システムはパーミッションを即時に付与します。
第2画面(一覧表示画面)
遷移先の画面です。
package jp.co.casareal.normaldangerouspermissions;
import android.app.ListActivity;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Contacts.Data;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import android.widget.ArrayAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* 連絡先アプリに、まずは1件登録(WRITE_CONTACTSパーミッション)して、
* そして全件取得(READ_CONTACTSパーミッション)して表示する画面
*/
public class ContactAccessActivity extends ListActivity {
// 連絡先アプリのContentProviderにアクセスするべくの、ContentResolver
private ContentResolver contentResolver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_contact_list);
contentResolver = getContentResolver();
// まずは、1件登録
addToContact();
// ContactsContract.Contacts.CONTENT_URIは、標準APIで提供されている連絡先アプリを指すUri型の定数
Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (cursor != null) {
List<String> contactDataList = new ArrayList<>();
while (cursor.moveToNext()) {
// ContactsContract.Contacts.DISPLAY_NAMEは、標準APIで提供されているString型の定数(連絡先アプリ上の氏名を指す)
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
// 今回は、氏名だけを収集
contactDataList.add(name);
}
cursor.close();
setListAdapter(new ArrayAdapter<String>(this, R.layout.contact_row, contactDataList));
}
}
/**
* 一件だけなのにContentResolver#applyBatchを使って、連絡先アプリに登録
*/
private void addToContact() {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
int rawContactInsertIndex = ops.size();
ops.add(ContentProviderOperation
.newInsert(RawContacts.CONTENT_URI)
.withValue(RawContacts.ACCOUNT_TYPE, null)
.withValue(RawContacts.ACCOUNT_NAME, null)
.build());
// 氏名の設定
ops.add(ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
.withValue(StructuredName.FAMILY_NAME, "徳川")
.withValue(StructuredName.GIVEN_NAME, "家康")
.withValue(StructuredName.PHONETIC_FAMILY_NAME, "とくがわ")
.withValue(StructuredName.PHONETIC_GIVEN_NAME, "いえやす")
.build());
// 電話番号の設定
ops.add(ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
.withValue(Phone.NUMBER, "056-109-0183")
.withValue(Phone.TYPE, Phone.TYPE_MOBILE)
.build());
// メールアドレスの設定
ops.add(ContentProviderOperation
.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
.withValue(ContactsContract.Data.MIMETYPE, Email.CONTENT_ITEM_TYPE)
.withValue(Email.DATA, "ieyasu@shogun.edo")
.withValue(Email.TYPE, Email.TYPE_MOBILE)
.build());
try {
ContentProviderResult[] res = contentResolver.applyBatch(ContactsContract.AUTHORITY, ops);
Log.v("ContactListActivity", res[0].toString());
} catch (Exception e) {
Log.e("ContactListActivity", e.getLocalizedMessage(), e);
e.printStackTrace();
}
}
}
登録の処理がやけに行数が嵩みました。全件検索はCursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
の一行ですんだのに。
第2画面(ContactAccessActivity.java)のポイント
今回は、徳川家康のたったの一件だけの登録しかしないのに、[ContentResolver#applyBatch](https://developer.android.com/reference/android/content/ContentResolver.html#applyBatch(java.lang.String, java.util.ArrayList))メソッドを使ったのは大袈裟でした(おかげで行数が嵩みました)が、まあそこはprivateなメソッドで纏めたのでご勘弁ください。
マニフェスト
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="jp.co.casareal.normaldangerouspermissions">
<!-- アプリで利用するパーミッションを利用宣言する -->
<!-- 連絡先アプリを読み取るパーミッション (Protection Level: Dangerous) -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 連絡先アプリに書き込むパーミッション (Protection Level: Dangerous) -->
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<!-- ネット通信するパーミッション (Protection Level: Normal) -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ContactAccessActivity"></activity>
</application>
</manifest>
MainActivity.javaでは、『READ_CONTACTSだけを許可すれば、イチイチWRITE_CONTACTSの許可は取らなくても自動付与される』でしたが、マニフェストでは、ちゃんとREAD_CONTACTSもWRITE_CONTACTSも<uses-permission>
に記載しておかなければなりません。なお、INTERNETは今回出番なし、なのですがオマケで書いておきました。
Android 5.1(API Level 22・Lollipop)でも動作確認してみた
同じアプリを🍭Lollipopにインストールして動作確認してみます。
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "jp.co.casareal.normaldangerouspermissions"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
}
minSdkVersionを19にしておいたので、Lollipopでも動きます。
Lollipopエミュレータの連絡先アプリにも、2名事前に登録しておきました。けど、面倒くさくなってしまったので、氏は端折りました。
さて、作ったアプリですが、ボタンを押しても許可を問うダイアログは出ず、いきなり第2画面に遷移します。
連絡先アプリを見返してみれば、ちゃんと登録されています。
つまりは、インストールしてある、すなわち、もうとっくに許可済み、ということですね。
一応、アプリ情報も見てみましょう。
「連絡先の読み取り」「連絡先の変更」が見受けられました。
謝辞
一般社団法人日本スマートフォンセキュリティ協会の『Androidアプリのセキュア設計・セキュアコーディングガイド』とそのサンプルコードが非常に参考になりました。ありがとうございました。
以上です。