IntelliJ IDEAでScala on Android using sbt Part.2[マシュマロパーミッション]
こちらの記事を参考に、AndroidのM Permissionsの仕組みを確認できるアプリをScalaで書きました。
IDEはIntelliJ IDEA CE 2016.2で、ビルドツールはsbt 0.13.12を使っています。
動作確認は、API level 23のAndroidエミュレータで実施しました。
目次
Scala on AndroidでM Permissions
Request Permission
のボタンを押すとカメラを使用する権限*1を要求し、AppSettings
のボタンを押すと、アプリの設定画面*2を開いて直接的に権限の設定ができます。
View Image
のボタンを押すと、SDカードに保存されているデータを読み込む権限*3を要求して、SDカードに画像がある場合は最初の1枚を表示します。
ソースコードをGitHubに置いてますので、動かしてみる場合はこちらの記事を参考にして下さい。
プロジェクトディレクトリの構成
~/MarshmallowPermission
|- build.sbt // sbtのビルド定義ファイル
|- /project // sbt関連の設定ファイルを配置するディレクトリ
|- /src // アプリのモジュールを構成するディレクトリ
| |- /main // アプリのメインソースセットを配置するディレクトリ
| | |- AndroidManifest.xml // アプリに関する情報を記述するマニフェストファイル
| | |- /libs // 外部ライブラリを配置するディレクトリ
| | |- /res // リソースファイルを配置するディレクトリ
| | | |- /drawable-mdpi // 中解像度の画像を配置するディレクトリ
| | | |- /drawable-hdpi // 高解像度の画像を配置するディレクトリ
| | | |- /drawable-xhdpi // 超高解像度の画像を配置するディレクトリ
| | | | |- ic_launcher.png // アプリのアイコン画像
| | | |- /drawable-xxhdpi // 超超高解像度の画像を配置するディレクトリ
| | | |- /drawable-xxxhdpi // 超超超高解像度の画像を配置するディレクトリ
| | | |- /layout // レイアウトの定義ファイルを配置するディレクトリ
| | | | |- activity_main.xml // 画面レイアウトの定義ファイル
| | | |- /values // 配色や文字の定義ファイルを配置するディレクトリ
| | |
| | |- /scala // Scalaのコードを配置するディレクトリ
| | |- /com.b0npu.mpermissions // アプリのパッケージディレクトリ
| | |- MainActivity.scala // アプリのメインファイル
| |
| |- /test // テストコードを配置するディレクトリ
|
|- /target // ビルドで生成された成果物の出力先ディレクトリ
主なソースコード
Empty Activityテンプレートのactivity_main.xml
にImageViewとButtonのWidgetを配置し、MainActivity.scala
でButtonを押した際の動作を定義しています。
ランチャーのラベルにMarshmallowPermissionは長すぎたので、アプリ名を定義しているres/values/strings.xmls
のapp_name
とは別にlauncher_label
を定義しました。
APKファイルの名前は、build.sbt
のname :=
で定義しています。
activity_main.xml
- Text
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context="com.b0npu.mpermissions.MainActivity"> <ImageView android:id="@+id/backgroundImage" android:scaleType="fitCenter" android:layout_width="match_parent" android:layout_height="match_parent" /> <TextView android:text="You can check M Permissions operation" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Request Permission" android:id="@+id/requestButton" android:layout_above="@+id/settingsButton" android:layout_centerHorizontal="true"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="AppSettings" android:id="@+id/settingsButton" android:layout_centerVertical="true" android:layout_centerHorizontal="true"/> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="View Image" android:id="@+id/viewImageButton" android:layout_below="@+id/settingsButton" android:layout_centerHorizontal="true"/> </RelativeLayout>
- Design
MainActivity.scala
package com.b0npu.mpermissions import android.Manifest import android.content.pm.PackageManager import android.content.{ContentResolver, ContentUris, DialogInterface, Intent} import android.database.Cursor import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.provider.{BaseColumns, MediaStore, Settings} import android.support.v4.app.ActivityCompat import android.support.v4.content.PermissionChecker import android.support.v7.app.{AlertDialog, AppCompatActivity} import android.util.Log import android.view.View import android.view.View.OnClickListener import android.widget.{Button, ImageView, Toast} class MainActivity extends AppCompatActivity with TypedFindView { /** * フィールドの定義 * * requestPermissionsメソッドで権限を要求した際に * コールバックメソッドのonRequestPermissionsResultメソッドに渡す定数を定義 * Logを所々で表示するのでTagの変数も定義 * (自クラスで使うだけのフィールドはprivateにして明示的に非公開にしてます) */ private val REQUEST_CAMERA_PERMISSION_CODE: Int = 0x01 private val REQUEST_READ_STORAGE_PERMISSION_CODE: Int = 0x02 private val TAG: String = "M Permission" /** * アプリの画面を生成 * * アプリを起動するとonCreateが呼ばれてActivityが初期化され * setContentViewでレイアウトがビューに表示される */ override def onCreate(savedInstanceState: Bundle): Unit = { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) /** * Request Permissionボタンを押す * * CAMERAのパーミッションの状態を確認して * 権限が許可されていない場合はrequestCameraPermissionメソッドを呼んで * 権限の許可を要求する * 権限が許可されていればToastで通知だけする(CAMERAは起動しません) */ val requestButton: Button = findView(TR.requestButton) requestButton.setOnClickListener(new OnClickListener { override def onClick(view: View): Unit = { if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { requestCameraPermission } else { Log.d(TAG, "checkSelfPermission: CAMERA GRANTED") Toast.makeText( MainActivity.this, "パーミッションは取得済みです!やり直す場合はアプリをアンインストールするか設定から権限を再設定して下さい", Toast.LENGTH_LONG ).show /* TODO: 必要ならここでカメラを起動する */ } } }) /** * AppSettingsボタンを押す * * パーミッションの状態の確認や手動設定を行うために * openSettingsメソッドを呼んでアプリの設定画面を開く */ val settingsButton: Button = findView(TR.settingsButton) settingsButton.setOnClickListener(new OnClickListener { override def onClick(view: View): Unit = { openSettings } }) /** * View Imageボタンを押す * * READ_EXTERNAL_STORAGEのパーミッションの状態を確認して * 権限が許可されていない場合はrequestReadStoragePermissionソッドを呼んで * 権限の許可を要求する * 権限が許可されていればToastで通知しつつviewBackgroundImageメソッドを呼んで * SDカードに保存されている画像の最初の1枚を背景に表示する */ val viewImageButton: Button = findView(TR.viewImageButton) viewImageButton.setOnClickListener(new OnClickListener { override def onClick(view: View): Unit = { if (PermissionChecker.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { requestReadStoragePermission } else { Log.d(TAG, "checkSelfPermission: READ STORAGE GRANTED") Toast.makeText( MainActivity.this, "パーミッションは取得済みです!", Toast.LENGTH_LONG ).show /* 画像を背景に表示する */ viewBackgroundImage } } }) } /** * openSettingsメソッドの定義 * * インテントを使ってアプリの設定画面を開く */ private def openSettings: Unit = { val appSettingsIntent: Intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val appPackageUri: Uri = Uri.fromParts("package", getPackageName, null) /* インテントにアプリのURIを指定してアプリ情報の画面を開く */ appSettingsIntent.setData(appPackageUri) startActivity(appSettingsIntent) } /** * viewBackgroundImageメソッドの定義 * * SDカードに保存されている画像の最初の1枚を背景に表示する * TODO: SDカードに画像が無い場合のエラー処理をしてないので注意 */ private def viewBackgroundImage: Unit = { /* SDカードの画像データのURIに問い合わせをして検索結果をCursorに格納する */ val imageMediaStoreUri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI val mediaContentResolver: ContentResolver = getContentResolver val pictureCursor: Cursor = mediaContentResolver.query(imageMediaStoreUri, null, null, null, null) Log.v("Media", "Cursor Column Names" + pictureCursor.getColumnNames.toString) Log.v("Media", "Image File:" + pictureCursor.getCount) /* Cursorに格納した画像データの検索結果から1枚目の画像のIDを取得する */ pictureCursor.moveToFirst val pictureId: Long = pictureCursor.getLong(pictureCursor.getColumnIndex(BaseColumns._ID)) /* 画像データのURIとIDから画像(ビットマップ画像)を取得する */ val bmpImageUri: Uri = ContentUris.withAppendedId(imageMediaStoreUri, pictureId) val bmpImage: Bitmap = MediaStore.Images.Media.getBitmap(mediaContentResolver, bmpImageUri) /* ImageViewに画像(ビットマップ画像)を表示する */ val backgroundImageView: ImageView = findView(TR.backgroundImage) backgroundImageView.setImageBitmap(bmpImage) } /** * requestCameraPermissionメソッドの定義 * * CAMERAのパーミッションの許可(権限取得)を要求する * shouldShowRequestPermissionRationaleメソッドを使って * 以前にパーミッションの許可を拒否されたことがあるか確認し * 拒否されたことがある場合はパーミッションの許可が必要な理由を * ダイアログに表示してからパーミッションの許可を要求する */ private def requestCameraPermission: Unit = { /* パーミッションの許可を拒否されたことがあるか確認する */ if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CAMERA)) { Log.d(TAG, "shouldShowRequestPermissionRational: CAMERAの権限取得に関する追加説明") /* パーミッションの許可を拒否されたことがあれば許可が必要な理由を説明してから許可を要求する */ new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッションの追加説明") .setMessage("このアプリで写真を撮るにはパーミッションが必要です") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* パーミッションの許可を要求 */ ActivityCompat.requestPermissions( MainActivity.this, Array[String](Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION_CODE ) } }) .create .show } else { /* 初回要求時か「今後は確認しない」を選択されている場合のパーミッションの許可の要求 */ ActivityCompat.requestPermissions( MainActivity.this, Array[String](Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION_CODE ) } } /** * requestReadStoragePermissionメソッドの定義 * * READ_EXTERNAL_STORAGEのパーミッションの許可(権限取得)を要求する * shouldShowRequestPermissionRationaleメソッドを使って * 以前にパーミッションの許可を拒否されたことがあるか確認し * 拒否されたことがある場合はパーミッションの許可が必要な理由を * ダイアログに表示してからパーミッションの許可を要求する */ private def requestReadStoragePermission: Unit = { /* パーミッションの許可を拒否されたことがあるか確認する */ if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) { Log.d(TAG, "shouldShowRequestPermissionRational: ReadStorageの権限取得に関する追加説明") /* パーミッションの許可を拒否されたことがあれば許可が必要な理由を説明してから許可を要求する */ new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッションの追加説明") .setMessage("このアプリで画像を表示するにはパーミッションが必要です") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* パーミッションの許可を要求 */ ActivityCompat.requestPermissions( MainActivity.this, Array[String](Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_READ_STORAGE_PERMISSION_CODE ) } }) .create .show } else { /* 初回要求時か「今後は確認しない」を選択されている場合のパーミッションの許可の要求 */ ActivityCompat.requestPermissions( MainActivity.this, Array[String](Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_READ_STORAGE_PERMISSION_CODE ) } } /** * onRequestPermissionsResultメソッドをオーバーライド * * このメソッドはrequestPermissionsメソッドのコールバックメソッドで * requestPermissionsメソッドでパーミッションの許可を要求した結果を取得する * 引数のrequestCodeで要求されたパーミッションを区別し * grantResultの要素でパーミッションの許可・不許可を確認する */ override def onRequestPermissionsResult(requestCode: Int, permissions: Array[_root_.java.lang.String], grantResults: Array[Int]): Unit = { /* 要求されたパーミッションによって対応が変わるので何のパーミッションか確認する */ requestCode match { case REQUEST_CAMERA_PERMISSION_CODE ⇒ /* パーミッションの要求が拒否されていた場合はダイアログに表示する */ if (grantResults.length != 1 || grantResults(0) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "onRequestPermissionResult: DENIED") /* 「今後は確認しない」が選択されていなければ許可が必要な理由を説明する */ if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CAMERA)) { Log.d(TAG, "[show error]") new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッション取得エラー") .setMessage("再取得する場合は再度Requestボタンを押して下さい") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* TODO: ここでrequestCameraPermissionsでも良い */ } }) .create .show } else { /* 「今後は確認しない」を選択されている場合はアプリの設定画面を開く */ Log.d(TAG, "[show app settings guide]") new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッション取得エラー") .setMessage("今後は許可しないが選択されました!!アプリ設定>権限を確認してください(権限をON/OFFすることで状態はリセットされます)") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* アプリの設定画面を開いて手動で許可してもらう */ openSettings } }) .create .show } } else { /* パーミッションが許可された場合はToastで通知する(CAMERAは起動しません) */ Log.d(TAG, "onRequestPermissionsResult: CAMERA GRANTED") /* TODO: 必要ならカメラを起動する */ Toast.makeText( MainActivity.this, "パーミッションを取得しました!", Toast.LENGTH_LONG ).show } case REQUEST_READ_STORAGE_PERMISSION_CODE ⇒ /* パーミッションの要求が拒否されていた場合はダイアログに表示する */ if (grantResults.length != 1 || grantResults(0) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "onRequestPermissionResult: DENIED") /* 「今後は確認しない」が選択されていなければ許可が必要な理由を説明する */ if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)) { Log.d(TAG, "[show error]") new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッション取得エラー") .setMessage("再取得する場合は再度Requestボタンを押して下さい") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* TODO: ここでrequestCameraPermissionsでも良い */ } }) .create .show } else { /* 「今後は確認しない」を選択されている場合はアプリの設定画面を開く */ Log.d(TAG, "[show app settings guide]") new AlertDialog.Builder(MainActivity.this) .setTitle("パーミッション取得エラー") .setMessage("今後は許可しないが選択されました!!アプリ設定>権限を確認してください(権限をON/OFFすることで状態はリセットされます)") .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener { override def onClick(dialogInterface: DialogInterface, i: Int): Unit = { /* アプリの設定画面を開いて手動で許可してもらう */ openSettings } }) .create .show } } else { /* パーミッションが許可された場合は画像を表示する */ Log.d(TAG, "onRequestPermissionsResult: GRANTED") viewBackgroundImage } } } }
res/values/strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">Marshmallow Permission</string> <string name="launcher_label">M Permissions</string> </resources>
build.sbt
・
・
・
name := "MPermissions"
・
・
・
Run/Debugの実行結果
なんとなくですが、M Permissionsの仕組みが確認できます。
参考記事
- 初心者のためのM Permissions入門 | TechBooster
- Requesting Permissions at Run Time | Android Developers
- 画像情報を取得する - Androidプログラマへの道 〜 Moonlight 明日香 〜
- Scalacheat - Scala Documentation
開発環境
- OSX 10.11.6 El Capitan
- IDE: InteiijJ IDEA Community Edition 2016.2
- Java Development Kit: Java SE Development Kit 8u101
- Android SDK Tools: android-sdk_r24.4.1-macosx
- Android Virtual Device: Android 6.0(Google APIs) API level 23 - Scala 2.11.8
- ビルドツール: sbt 0.13.12