このサイトでは、分析、カスタマイズされたコンテンツ、および広告に Cookie を使用します。このサイトを引き続き閲覧すると、Cookie の使用に同意するものと見なされます。
Hi, Developers,
straightapps.com ロゴ
作成 June 15, 2020、一部修正 March 5, 2021
トップページ > Android 開発トップ > 動的にパーミッションを取得する
line
Android 開発
line

ここでは、ネイティブアプリでストレージ書き込み権限を得る方法について、書いています。

Android 6 より前では、AndroidManifest.xml で外部ストレージの権限を記録しただけで、 Download フォルダなど、パブリックなフォルダにアクセス可能となっていました。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

だいぶ前に作成した記事「C/C++ によるログ出力2021 年 3 月に更新済みでは、 これだけで書き込みしていましたが、それは当時 Android 4.4 だったからです。 Android 6 以降では、AndroidManifest.xml で外部ストレージの権限を記録しただけでは、実際にはアクセスできませんので、 今ある機種ではほぼ間違いなく、書き込みできません。

ここでは、ネイティブアプリで、実行時にストレージ書き込みの権限を得る方法を検討しています。

良くは知りませんが、Android Studio や Xamarin、Gradle などを利用すれば比較的簡単なようですが、 ネイティブアプリでは、そう簡単にはいきません。

本来 Windows 開発者の私は、開発環境 Win10 Pro + Visual Studio Community 2019(Visual C++)による アプリ開発をしようとしています。 android_main 関数から始まる、ネイティブアプリ(NativeActivity)です。 これが、何をするにも難しくしている原因ではあるのですが・・・。 テストしているデバイスは、ASUS の Zenfone Max M2 で、Android 8 を Android 9 に更新しています。

【 2021 年 3 月 5 日】
Java コードの準備」にある Java のコードを、 アプリのインストール直後でも正しく動作するように、修正しています。

▼ セクション一覧

ネットでみつかる、できない手法
Java コードの準備
Java 関数の呼び出し手順
非 static 関数を呼び出す場合

ご利用に際しては、必ずプライバシーポリシー(免責事項等)をご参照ください。
また、本サイトが初めての方は、まずこのページの注意事項をご覧ください。

ネットでみつかる、できない手法

投稿 June 15, 2020

NativeActivity から、どのようにすれば「実行時に権限を得る」コードを実装できるのか、 ネットで調べると、すぐにいくつか見つかります。

まずは何より、Android Developers 公式サイト 「アプリの権限をリクエストする」です。 丁寧に書かれていて、もちろん一般的には参考になる情報です。

「パーミッションを確認する」の Java のコードを見ると、 ContextCompat クラスcheckSelfPermission 関数を呼び出して、すでにパーミッションを得ているかを調べています。 ContextCompat クラスは、 androidx.core.content.ContextCompat とされています。

トップレベルにある AndroidX とは、 API Level 28 ( Android 9 ) で導入されたライブラリのようです。 詳しくは「AndroidX の概要」に書かれています。 android.support ライブラリの置き換え版となっているようですが、 設定が必要なのか、今の環境では android.support ライブラリも使えません。 また、API Level 28 以降の新しい SDK のほか、gradle.properties ファイルでの設定が必要とされていますが、 VC++ で作成した NativeActivity プロジェクトには、そのようなファイルはありません。

API レベルとバージョンの対応については、 「Google Play の対象 API レベルの要件を満たす」 に書かれています。

他には、 Android OS のバージョン ( API レベル ) を得て、権限の取得が必要かを判断するのに、 Build.VERSION.SDK_INT を参照する記事が見つかります。 これを見て、デバイスの API レベルが 23 以上の場合、Android 6 以降なので権限の取得が必要、という流れなのですが、 Build を参照できないので、使えません。

Context クラスの checkCallingPermission 関数も、使えそうですが、呼び出しまでの道のりは遠そうです。

いろいろ調べた結果、Java のコードで実装できましたので、次のセクションに記録します。 なお、ターゲット API は 25 としていますが、おそらく関係ありません。

▲ページ先頭へ

Java コードの準備

投稿 June 15, 2020、一部修正 March 5, 2021

まずは、Java のコードを用意します。

VC++ の NativeActivity プロジェクトへの Java コードの追加については、 「Java クラス関数の作成と呼び出し」をご覧ください。

以下、ドメイン名、パッケージ名は、プロジェクトのものに書き換えてください。 クラス名や関数名は、もちろん自由です。

package com.<ドメイン名>.<パッケージ名>;

import android.app.NativeActivity;
import android.content.pm.PackageManager;

class ObtainPermission
{
    /**
    *  WRITE/READ_EXTERNAL_STORAGE のパーミッションを確認・取得します。
    *    すでに取得済み、あるいは取得できた場合は true を返します。
    *    すでに拒否済み、あるいは取得できなかった場合は false を返します。
    */
    public static boolean obtainPermission_storage(NativeActivity app)
    {
        //  すでにパーミッションを取得済みかどうかを調べます。
        if (app.checkCallingPermission("android.permission.WRITE_EXTERNAL_STORAGE") == PackageManager.PERMISSION_GRANTED){
            return true;
        }

        // 「今後表示しない」にチェックを入れて「許可しない」されたかどうかを調べます。
        if (app.shouldShowRequestPermissionRationale("android.permission.WRITE_EXTERNAL_STORAGE") == false){
            return false;
        }


        //  標準の問い合わせダイアログを出して、パーミッションを要求します。
        app.requestPermissions(new String[] { "android.permission.WRITE_EXTERNAL_STORAGE" }, 100);

        //  許可されたかどうかを調べ、戻り値を決定します。
        if (app.checkCallingPermission("android.permission.WRITE_EXTERNAL_STORAGE") != PackageManager.PERMISSION_GRANTED){
            return false;
        }

        //  許可されました。
        return true;
    }
}

すでに権限を取得しているかどうかを、 checkCallingPermission 関数で調べています。 プロセス間通信 IPC を処理していない場合は、 checkCallingOrSelfPermission 関数を使うべきとされていて、 同じように動作はしているようですが、 ドキュメントには「注意して使用!」と書かれていますので、checkCallingPermission 関数のほうが、安全なのでしょう。 取得済みなら true を返します。

端末内の写真、メディア、ファイルへのアクセスを許可しますか?(今後表示しない)

過去に「今後表示しない」にチェックを入れて「許可しない」を選択した場合、 もう問い合わせるべきではありません。

shouldShowRequestPermissionRationale 関数で、 すでに拒否されているかどうかを判断できます。 拒否済みの場合は false が返りますので、この関数も false を返します。

【 2021 年 3 月 5 日追記】
この関数呼び出しにより、アプリをインストールしたあと false が返ってしまい、問い合わせが表示させないようです。 いったん設定アプリから、そのアプリの権限を許可したり拒否したりすると、この関数は false を返さなくなります。 下記の通り、この判定は必要ではないようですから、削除としてください。
【 2021 年 3 月 5 日追記 ここまで】

この判定をしなくても、拒否されている場合は requestPermissions 関数は問い合わせを出さないようです。 この判定が必要なのは、アプリに必要な権限なのに、拒否しながらもその機能を呼び出すユーザーに対して、 権限の必要性を説明する、とされています。 rationale は、「論理的根拠」「理由づけ」という意味だそうです。

なお、いったん拒否したあと、また聞かれるようにするには、アプリのアイコンを長押しするなどして「アプリ情報」を表示し、 「権限」設定をいったん ON にしたあと、OFF にします。

未許可の場合、requestPermissions 関数で問い合わせを出します。

端末内の写真、メディア、ファイルへのアクセスを許可しますか?

android.permission.WRITE_EXTERNAL_STORAGE は、 読み込み権限も含み、また、写真やメディアにアクセスしないとしても、標準的にはこのようなメッセージになります。 図で「Clip-2」と書かれている部分は、 アプリ名 ( プロジェクト名ではありません。パッケージの res / values / strings.xml で指定された app_name 文字列です。 ) になります。

requestPermissions 関数の最終パラメータは、他のリクエストと重複しない自由な値とされています。 他の権限を使う場合は、重複しない値を指定しないといけないようです。

最後に、許可されたかどうかを調べて、終了しています。 許可されなかった場合にはログを書かないなど、マナー的には工夫が必要です。書き込もうとしても書き込めませんが。

▲ページ先頭へ

Java 関数の呼び出し手順

投稿 June 15, 2020

まずは、呼び出すタイミングを決めなくてはいけません。

今回は、ログを(Download などの見える場所に)出力する目的でしたので、アプリ起動時、メインループ開始前に問い合わせるようにしました。 テスト目的で「画面のどこかがタッチされたら」とするなら、 engine_handle_input 関数の、
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION){
}

の中にコードを置くといいでしょう。

VC++ の NativeActivity プロジェクトへの Java コードの追加については、 「Java クラス関数の作成と呼び出し」に詳しく書いていますが、 新しく Java クラスを追加するたびに大変な呼び出しを書くのは嫌なので、CaxJavaCaller と名付けたクラスにしました。

なお、API レベルが 23 未満である場合は、呼び出す必要はありません。 Build.VERSION.SDK_INT 相当の値は、ネイティブアプリでは、次のいずれの方法でも取得できました。 返る値は、どちらも同じです。Android 9 の場合は、28 になりました。

・ AConfiguration_getSdkVersion(app->config)
・ app->activity->sdkVersion

クラスの実装の前に、Java 関数の呼び出し手順です。

CaxJavaCaller jc;

jc.prepare(app);
if (jc.obtainJavaClass(app, "com/<ドメイン名>/<プロジェクト名>/ObtainPermission") == true) {
    jmethodID obtainPermissionMethod = jc.env->GetStaticMethodID(jc.classRequested, "obtainPermission_storage", "(Landroid/app/NativeActivity;)Z");

    jboolean b = jc.env->CallStaticBooleanMethod(jc.classRequested, obtainPermissionMethod, app->activity->clazz);
    if (b == true) {
        // 権限を得られました
    }
    else {
        // 権限は得られませんでした
    }
}
jc.finish(app);

prepare 関数では、JavaVM をカレントスレッドにアタッチし、メンバ変数とした JNIEnv* env を設定しています。 すなわち、
app->activity->vm->AttachCurrentThread(&env, NULL);
を実行しています。

obtainJavaClass 関数で、パブリックで定義された jclass classRequested に、実装したクラスを読み込んでいます。 実体は、以下のようになっています。

なお、ここでクラス名を間違えて指定すると、 この関数は false を返しますが、再び呼び出されるとアプリが強制終了となってしまいます。 メモリの解放等に問題があるかもしれませんので、ご注意ください。

bool CaxJavaCaller::obtainJavaClass(struct android_app* app, char* className)
{
    jclass activityClass;			// NativeActivity クラス
    jmethodID getClassLoader;			// NativeActivity クラスの getClassLoader 関数
    jobject classLoaderInstance;		// NativeActivity クラスの ClassLoader クラスのインスタンス
    jclass classLoader;				// NativeActivity クラスの ClassLoader クラス
    jmethodID loadClass;			// NativeActivity クラスの ClassLoader クラスの loadClass 関数

    //  事前に prepare が呼び出されている必要があります。
    if (env == NULL) {
        return false;
    }

長いので分割しています。 まずは、指定のクラス className を読み込むまでに必要なクラスやメソッドを格納する変数を定義しています。

    //  getSystemService 関数等を持つ NativeActivity クラスを取得します。
#if 1
    activityClass = env->FindClass("android/app/NativeActivity");
#else
    activityClass = env->GetObjectClass(app->activity->clazz);
#endif
    if (activityClass == 0) {
        return false;
    }

jclass の activityClass に、FindClass 関数で android.app.NativeActivity クラスを取得しています。 static 定義されたものだけを呼び出すのであれば、これで問題ありません(定義の取得)。 そうでない場合は、
env->GetObjectClass(app->activity->clazz);
を使うといいようです(インスタンスの取得)。 なお、手元の環境では、static 関数の呼び出しでも、後者を使って問題ないようです。

    //  NativeActivity クラスの getClassLoader 関数を取得します。
    getClassLoader = env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
    if (getClassLoader == 0) {
        return false;
    }

NativeActivity クラスに実装されている、getClassLoader 関数を取得します。 最後の引数は、getClassLoader 関数には引数はなく、戻り値は java.lang.ClassLoader であることを意味しています。 引数や戻り値の指定方法については、 「型のシグニチャー」に書かれています。

    //  getClassLoader 関数を呼び出して、ClassLoader のインスタンスを取得します。
    classLoaderInstance = env->CallObjectMethod(app->activity->clazz, getClassLoader);
    if (classLoaderInstance == 0) {
        return false;
    }

取得した getClassLoader 関数を CallObjectMethod 関数 ( static ではない、Object を返す関数の呼び出しに使用します。 ) で呼び出して、 ClassLoader のインスタンスを取得しています。

    //  ClassLoader クラスを取得します。
    classLoader = env->FindClass("java/lang/ClassLoader");
    if (classLoader == 0) {
        return false;
    }

ClassLoader クラスの loadClass 関数を取得するため、ClassLoader クラスの定義を取得しています。 GetMethodID 関数の第1引数が jclass なので仕方がないようですが、なんとなく面倒ですが、
(jclass)classLoaderInstance
を代わりに使用することはできないようです。

    //  ClassLoader の loadClass 関数を取得します。
    loadClass = env->GetMethodID(classLoader, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    if (loadClass == 0) {
        return false;
    }

ClassLoader クラスの loadClass 関数を取得しています。 文字列を渡し、java.lang.Class を返す関数です。

    //  Java コードに実装されたクラスを取得します。
    jstring strClassName = env->NewStringUTF(className);
    classRequested = (jclass)env->CallObjectMethod(classLoaderInstance, loadClass, strClassName);
    if (classRequested == 0) {
        return false;
    }
    return true;
}

そして、ついにやっと、loadClass 関数に指定されたクラス名を(jstring に変換して)渡して、 classRequested に入れます。 CallObjectMethod 関数はオブジェクトを返すので、jclass に変換して、classRequested に入れています。

詳細未確認ですが、取得したクラスは、オブジェクトの初期化をしていないので、オブジェクトとしては使用できないと思われます。 非 static 関数を呼び出す必要がでたときに確認しますが、原則的には、次のセクションに書いているように、初期化する必要があるはずです。

指定のクラスを読み込んだら、パブリックなメンバの JNIEnv* envjclass classRequested を使用して、関数を呼び出します。 実装した obtainPermission_storage 関数は、 static 定義されていて、boolean を返しますので、 GetStaticMethodID 関数で jmethodID を取得し、CallStaticBooleanMethod 関数で呼び出します。

最後に、finish 関数で終了しています。
app->activity->vm->DetachCurrentThread();
を実行しています。

▲ページ先頭へ

非 static 関数を呼び出す場合

投稿 June 15, 2020

static で定義された関数は、クラスの定義だけ取得できれば、呼び出すことができます。

jclass classX = env->FindClass("java/lang/classX");
jmethodID jmID = env->GetStaticMethodID(classX, "methodX", "()Z");
jboolean jb = env->CallStaticBooleanMethod(classX, jmID);

のように、呼び出しやすいです。

static でない関数を呼び出すには、インスタンスを作成する必要があります。

以下、ここでは動作未確認ですが、コンストラクタを次のように取得します。 この場合は、引数なし、戻り値なしの、ただ作成するだけのコンストラクタです。

jmethodID classRequestedInitMethod = env->GetMethodID(classRequested, "<init>", "()V");
if (classRequestedInitMethod == 0) {
    // classRequested クラスのコンストラクタを取得できませんでした。
}

取得したコンストラクタを呼び出すと、classRequested が表すクラスのインスタンス(オブジェクト) objRequested を得られます。

jobject objRequested = env->NewObject(classRequested, classRequestedInitMethod);
if (objRequested == 0) {
    // objRequested クラスのインスタンスを作成できませんでした。
}

これで、Call〜Method 関数で、呼び出せると思います。

▲ページ先頭へ
line
関連トピックス
line

Java クラス関数の作成と呼び出し

プロジェクトに Java コードを追加する方法や、その呼び出し方法について、まとめています。

JNI によるパブリックなパスの取得

JNI を利用して、Download などのパスを取得する方法を検討しています。

C/C++ による現在日時の取得

C/C++ で現在時刻を取得する方法を検討しています。

line
その他のおすすめ
line

Android 開発トップ

Android ネイティブアプリ開発に関する情報は、こちらからどうぞ。

JavaScriptが無効です
▲ページ先頭へ


© 2017-2021 StraightApps.com 無断転載を禁じます。No reproduction without permission.