Open16

Example (Android)

s16as16a

ContentProvider

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- MEMO:ContentProviderへのアクセス権限を宣言 -->
    <permission android:name="com.example.testcontentprovider001.READ_DATA" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TestContentProvider001"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!--MEMO:ContentProviderの定義を追加-->
        <!--
            MEMO:
            android:enabled
                システムがコンテンツ プロバイダをインスタンス化できるかどうかを指定する
                できる場合は "true"、できない場合は "false"、デフォルト値は "true"
            android:exported
                true: 他のアプリがプロバイダを使用できる
                false: 他のアプリはプロバイダを使用できない、対象アプリに限定される
                ref) https://developer.android.com/guide/topics/manifest/provider-element?hl=ja
        -->
        <provider
            android:name=".MyContentProvider"
            android:authorities="com.example.testcontentprovider001.mycontentprovider"
            android:enabled="true"
            android:exported="true"
            android:readPermission="com.example.testcontentprovider001.READ_DATA" />

    </application>

</manifest>

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java:

package com.example.testcontentprovider001;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

MyContentProvider.java:

package com.example.testcontentprovider001;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;

public class MyContentProvider extends ContentProvider {
    private static String TAG = " TestContentProvider";

    // ContentProviderのURIに関する定義
    private static final String AUTHORITY = "com.example.testcontentprovider001.mycontentprovider";
    public static final Uri CONTENT_URI_BOOL = Uri.parse("content://" + AUTHORITY + "/bool_data");
    public static final Uri CONTENT_URI_INT = Uri.parse("content://" + AUTHORITY + "/int_data");
    public static final Uri CONTENT_URI_STRING = Uri.parse("content://" + AUTHORITY + "/string_data");

    // URIに対するコード定義
    private static final int BOOL_DATA = 1;
    private static final int INT_DATA = 2;
    private static final int STRING_DATA = 3;

    // UriMatcherを使ってURIとコードを関連付ける
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY, "bool_data", BOOL_DATA);
        sUriMatcher.addURI(AUTHORITY, "int_data", INT_DATA);
        sUriMatcher.addURI(AUTHORITY, "string_data", STRING_DATA);
    }

    // データを保持する変数
    private boolean boolValue = true;
    private int intValue = 42;
    private String stringValue = "Hello, ContentProvider!";

    // ContentProviderのメソッドを実装

    @Override
    public boolean onCreate() {
        //MEMO:ここでSharedPreferencesから読み込む準備をする、失敗したらfalseを返す
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        switch (sUriMatcher.match(uri)) {
            case BOOL_DATA:
                // Bool値のデータを返す
                return createCursor(this.boolValue);
            case INT_DATA:
                // 数値のデータを返す
                return createCursor(this.intValue);
            case STRING_DATA:
                // 文字列のデータを返す
                return createCursor(this.stringValue);
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // データの挿入処理を実装
        // 今回の例では静的なデータを持つため省略、外からデータを読み取るだけならこのままでも問題ない
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        // データの更新処理を実装
        // 今回の例では静的なデータを持つため省略、外からデータを読み取るだけならこのままでも問題ない
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // データの削除処理を実装
        // 今回の例では静的なデータを持つため省略、外からデータを読み取るだけならこのままでも問題ない
        return 0;
    }

    @Override
    public String getType(Uri uri) {
        // MIMEタイプを返す処理を実装
        // 今回の例では静的なデータを持つため省略、外からデータを読み取るだけならこのままでも問題ない
        return null;
    }

    // MEMO:データからCursorを生成するユーティリティメソッド、
    //      データがSQLiteテーブルにあるなら引数Cursorをそのまま返すが、
    //      データがSQLiteテーブルでないなら、MatrixCursorを使って擬似的なテーブル構造でデータを返す
    private Cursor createCursor(Object value) {
        Log.d(TAG, "createCursor called, value=" + value);

        if (value instanceof Boolean) {
            Log.d(TAG, "createCursor: value id Boolean:" + value);
            //int value = (boolean) value ? 1 : 0;
        }

        String[] columns = {"value"};
        //return new MatrixCursor(columns, 1).newRow().add(value).build();
        //return new MatrixCursor(columns, 1).newRow().add(value);
        MatrixCursor cursor = new MatrixCursor(columns, 1);

        Log.d(TAG, "createCursor: cursor.getCount()=" + cursor.getCount());

        cursor.addRow(new Object[]{value});

        Log.d(TAG, "createCursor: cursor.getCount()=" + cursor.getCount());
        //Log.d(TAG, "createCursor: cursor.getColumnNames()=" + cursor.getColumnNames());
        //Log.d(TAG, "createCursor: cursor.getType(0)=" + cursor.getType(0));

        //if (value instanceof Boolean) {
        //    Log.d(TAG, "createCursor: cursor.getInt(0)=" + cursor.getInt(0));
        //}

        Log.d(TAG, "createCursor done, return:" + cursor);
        return cursor;
    }

}
s16as16a

ContentResolver

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- MEMO:ContentProvider側の <permission> と同じ定義を <uses-permission> で宣言 -->
    <uses-permission android:name="com.example.testcontentprovider001.READ_DATA" />

    <queries>
        <!-- MEMO:ContentProvider側の <provider> の authorities と同じものを指定 -->
        <provider android:authorities="com.example.testcontentprovider001.mycontentprovider" />
    </queries>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TestContentResolver001"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java:

package com.example.testcontentresolver001;

import androidx.appcompat.app.AppCompatActivity;

import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    private static String TAG = " TestContentResolver";

    private static final String authority = "com.example.contentprovidertest.myprovider";
    private static final String uri = "content://$authority";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate called");

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        boolean boolValue = DataRetrievalHelper.getBoolValue(this);
        Log.d(TAG, "onCreate: boolValue=" + boolValue);

        int intValue = DataRetrievalHelper.getIntValue(this);
        Log.d(TAG, "onCreate: intValue=" + intValue);

        String stringValue = DataRetrievalHelper.getStringValue(this);
        Log.d(TAG, "onCreate: stringValue=" + stringValue);

        Log.d(TAG, "onCreate done");
    }

}

DataRetrievalHelper.java:

package com.example.testcontentresolver001;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

public class DataRetrievalHelper {
    private static String TAG = " TestContentResolver";

    // ContentProviderのURIに関する定義
    private static final String AUTHORITY = "com.example.testcontentprovider001.mycontentprovider";
    public static final Uri CONTENT_URI_BOOL = Uri.parse("content://" + AUTHORITY + "/bool_data");
    public static final Uri CONTENT_URI_INT = Uri.parse("content://" + AUTHORITY + "/int_data");
    public static final Uri CONTENT_URI_STRING = Uri.parse("content://" + AUTHORITY + "/string_data");


    public static boolean getBoolValue(Context context) {
        Log.d(TAG, "getBoolValue called");
        boolean result = false;

        ContentResolver contentResolver = context.getContentResolver();
        Uri uri = CONTENT_URI_BOOL;
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getBoolValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex("value");
            Log.d(TAG, "getBoolValue: column=" + column);
            
            if (column > -1) {
                //result = cursor.getInt(column) != 0;
                Log.d(TAG, "getBoolValue: cursor.getInt(column)=" + cursor.getInt(column));
                result = false;
                cursor.close();
            }
        }

        Log.d(TAG, "getBoolValue done, return:" + result);
        return result;
    }

    public static int getIntValue(Context context) {
        Log.d(TAG, "getIntValue called");
        int result = 0;

        ContentResolver contentResolver = context.getContentResolver();
        Uri uri = CONTENT_URI_INT;
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getIntValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex("value");
            Log.d(TAG, "getIntValue: column=" + column);

            if (column > -1) {
                result = cursor.getInt(column);
                cursor.close();
            }
        }

        Log.d(TAG, "getIntValue done, return:" + result);
        return result;
    }

    public static String getStringValue(Context context) {
        Log.d(TAG, "getStringValue called");
        String result = "";

        ContentResolver contentResolver = context.getContentResolver();
        Uri uri = CONTENT_URI_STRING;
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getStringValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex("value");
            Log.d(TAG, "getStringValue: column=" + column);

            if (column > -1) {
                result = cursor.getString(column);
                cursor.close();
            }
        }

        Log.d(TAG, "getStringValue done, return:\"" + result + "\"");
        return result;
    }
}
s16as16a

SharedPreferences

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.TestApp002"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml:

<?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"
    tools:context=".MainActivity">

    <CheckBox
        android:id="@+id/checkBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save Bool Value"
        android:layout_marginTop="20dp"
        android:layout_centerHorizontal="true"/>

    <EditText
        android:id="@+id/editTextInt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter an integer"
        android:inputType="number"
        android:layout_below="@id/checkBox"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"/>

    <EditText
        android:id="@+id/editTextString"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Enter a string"
        android:layout_below="@id/editTextInt"
        android:layout_marginTop="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Save Preferences"
        android:layout_below="@id/editTextString"
        android:layout_marginTop="20dp"
        android:layout_centerHorizontal="true"
        android:onClick="onSaveButtonClick"/>
</RelativeLayout>

MainActivity.java:

package com.example.testapp002;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    private static String TAG = "TestApp002";

    private static final String PREF_NAME = "MyPrefs";
    private static final String KEY_BOOL = "boolValue";
    private static final String KEY_INT = "intValue";
    private static final String KEY_STRING = "stringValue";

    private CheckBox checkBox;
    private EditText editTextInt, editTextString;

    private SharedPreferences sharedPreferences;

    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate called");

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkBox = findViewById(R.id.checkBox);
        editTextInt = findViewById(R.id.editTextInt);
        editTextString = findViewById(R.id.editTextString);

        // SharedPreferencesの取得
        sharedPreferences = getSharedPreferences(PREF_NAME, MODE_PRIVATE);

        // 保存されている値を復元
        restorePreferences();

        Log.d(TAG, "onCreate done");
    }

    private void restorePreferences() {
        Log.d(TAG, "restorePreferences called");

        // Bool値の復元
        boolean savedBoolValue = sharedPreferences.getBoolean(KEY_BOOL, false);
        checkBox.setChecked(savedBoolValue);
        Log.d(TAG, "restorePreferences: savedBoolValue=" + savedBoolValue);

        // 数値の復元
        int savedIntValue = sharedPreferences.getInt(KEY_INT, 0);
        editTextInt.setText(String.valueOf(savedIntValue));
        Log.d(TAG, "restorePreferences: savedIntValue=" + savedIntValue);

        // 文字列の復元
        String savedStringValue = sharedPreferences.getString(KEY_STRING, "");
        editTextString.setText(savedStringValue);
        Log.d(TAG, "restorePreferences: savedStringValue=" + savedStringValue);

        Log.d(TAG, "restorePreferences done");
    }

    // ユーザーの設定を保存
    public void onSaveButtonClick(View view) {
        Log.d(TAG, "onSaveButtonClick called");

        // Bool値
        boolean boolValue = checkBox.isChecked();
        SharedPreferences.Editor editor = sharedPreferences.edit();
        editor.putBoolean(KEY_BOOL, boolValue);
        Log.d(TAG, "onSaveButtonClick: boolValue=" + boolValue);

        // 数値
        try {
            int intValue = Integer.parseInt(editTextInt.getText().toString());
            editor.putInt(KEY_INT, intValue);
            Log.d(TAG, "onSaveButtonClick: intValue=" + intValue);
        } catch (NumberFormatException e) {
            // 数値の入力が不正な場合のエラーハンドリング
        }

        // 文字列
        String stringValue = editTextString.getText().toString();
        editor.putString(KEY_STRING, stringValue);
        Log.d(TAG, "onSaveButtonClick: stringValue=" + stringValue);

        // 保存を確定
        editor.apply();

        Log.d(TAG, "onSaveButtonClick done");
    }

}
s16as16a

ContentProvider 002

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- MEMO:独自定義Signature Permissionの利用を宣言 -->
    <uses-permission
        android:name="com.example.provider002.MY_PERMISSION" />

    <queries>
        <!-- MEMO:利用するContentProviderの定義と同じ authorities を指定 -->
        <provider android:authorities="com.example.provider002.mycontentprovider" />
    </queries>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Resolver002"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
package com.example.provider002;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.content.pm.Signature;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.util.Log;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MyContentProvider extends ContentProvider {
    private static String TAG = " TestContentProvider";

    public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.example.contenttype";
    public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.example.contenttype";

    // ContentProviderのURIに関する定義
    private static final String AUTHORITY = "com.example.provider002.mycontentprovider";
    public static final Uri CONTENT_URI_BOOL = Uri.parse("content://" + AUTHORITY + "/bool_data");
    public static final Uri CONTENT_URI_INT = Uri.parse("content://" + AUTHORITY + "/int_data");
    public static final Uri CONTENT_URI_STRING = Uri.parse("content://" + AUTHORITY + "/string_data");
    public static final Uri CONTENT_URI_VERSION_CODE = Uri.parse("content://" + AUTHORITY + "/version_code");
    public static final Uri CONTENT_URI_VERSION_NAME = Uri.parse("content://" + AUTHORITY + "/version_name");
    public static final Uri CONTENT_URI_SETTLEMENT_DATE = Uri.parse("content://" + AUTHORITY + "/settlement_date");

    // 自身で宣言している Signature Permission
    private static final String SIGNATURE_PERMISSION = "com.example.provider002.MY_PERMISSION";

    // URIに対するコード定義
    private static final int BOOL_DATA = 1;
    private static final int INT_DATA = 2;
    private static final int STRING_DATA = 3;
    private static final int VERSION_CODE = 4;
    private static final int VERSION_NAME = 5;
    private static final int SETTLEMENT_DATE = 6;

    // UriMatcherを使ってURIとコードを関連付ける
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        sUriMatcher.addURI(AUTHORITY, "bool_data", BOOL_DATA);
        sUriMatcher.addURI(AUTHORITY, "int_data", INT_DATA);
        sUriMatcher.addURI(AUTHORITY, "string_data", STRING_DATA);
        sUriMatcher.addURI(AUTHORITY, "version_code", VERSION_CODE);
        sUriMatcher.addURI(AUTHORITY, "version_name", VERSION_NAME);
        sUriMatcher.addURI(AUTHORITY, "settlement_date", SETTLEMENT_DATE);
    }

    //MEMO:データを保持する変数(実際は取得して更新する)
    private boolean mBoolValue = true;
    private int mIntValue = 24;
    private String mStringValue = "Hello, ContentProvider!";

    private int mVersionCode = 33;
    private String mVersionName = "1.0.0";
    private String mSettlementDate = "2024-03-03 11:30";


    // 証明書のハッシュ値
    private static String sMyCertHash = null;
    private static String getCertHash(Context context) {
        if (sMyCertHash == null) {
            if (isDebuggable(context)) {
                // debug.keystoreの"androiddebugkey"の証明書ハッシュ値
                sMyCertHash = "C7B59F4D 833C6985 51531BEF AC2DC62A 34F86153 4A0A13FC 3C30F400 B3B87801";
            } else {
                // keystoreの"my company key"の証明書ハッシュ値
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    private static boolean isDebuggable(Context context) {
        try {
            PackageManager pm = context.getPackageManager();
            ApplicationInfo ai = pm.getApplicationInfo(context.getPackageName(), 0);
            if ((ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) == ApplicationInfo.FLAG_DEBUGGABLE)
                return true;
        } catch (PackageManager.NameNotFoundException e) {
        }
        return false;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case VERSION_NAME:
            case VERSION_CODE:
            case SETTLEMENT_DATE:
                return CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {

        // Signature Permissionが自身で定義されていることを確認する
        if (!checkPermission(SIGNATURE_PERMISSION, getCertHash(getContext()))) {
            throw new SecurityException("独自定義Signature Permissionが自社アプリにより定義されていない。");
        }

        switch (sUriMatcher.match(uri)) {
            case BOOL_DATA:
                // Bool値のデータを返す
                return createCursor("state", this.mBoolValue);
            case INT_DATA:
                // 数値のデータを返す
                return createCursor("ages", this.mIntValue);
            case STRING_DATA:
                // 文字列のデータを返す
                return createCursor("message", this.mStringValue);
            case VERSION_CODE:
                // バージョン情報を返す
                return createCursor("code", this.mVersionCode);
            case VERSION_NAME:
                // バージョン情報を返す
                return createCursor("name", this.mVersionName);
            case SETTLEMENT_DATE:
                // 決済日時を返す
                return createCursor("date", this.mSettlementDate);
            default:
                    throw new IllegalArgumentException("Invalid URI: " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    // MEMO:データからCursorを生成するユーティリティメソッド、
    private Cursor createCursor(String key, Object value) {
        Log.d(TAG, "createCursor called, key=" + key + ", value=" + value);

        if (value instanceof Boolean) {
            throw new IllegalArgumentException("Invalid arg: " + value);
        }

        MatrixCursor cursor = new MatrixCursor(new String[] {key}, 1);

        // for debug ...
        //Log.d(TAG, "createCursor: cursor.getCount()=" + cursor.getCount());

        cursor.addRow(new Object[]{value});

        // for debug ...
        //Log.d(TAG, "createCursor: cursor.getCount()=" + cursor.getCount());
        //Log.d(TAG, "createCursor: cursor.getColumnNames()=" + cursor.getColumnNames());
        //Log.d(TAG, "createCursor: cursor.getType(1)=" + cursor.getType(1));

        Log.d(TAG, "createCursor done, return:" + cursor);
        return cursor;
    }

    private boolean checkPermission(String sigPermName, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try{
            // sigPermNameを定義したアプリのパッケージ名を取得する
            PackageManager pm = getContext().getPackageManager();
            PermissionInfo pi = pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            Log.d(TAG, "test: pkgname=" + pkgname);

            Log.d(TAG, "test: pi.protectionLevel=" + pi.protectionLevel);
            Log.d(TAG, "test: PermissionInfo.PROTECTION_SIGNATURE=" + PermissionInfo.PROTECTION_SIGNATURE);

            // 非Signature Permissionの場合は失敗扱い
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE) return false;

            Log.d(TAG, "test: Build.VERSION.SDK_INT=" + Build.VERSION.SDK_INT);

            // pkgnameの実際のハッシュ値と正解のハッシュ値を比較する
            if (Build.VERSION.SDK_INT >= 28) {
                // ★ API Level >= 28 ではPackage ManagerのAPIで直接検証が可能
                return pm.hasSigningCertificate(pkgname, hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
                // API Level < 28 の場合はPkgCertを利用し、ハッシュ値を取得して比較する
                return correctHash.equals(hash(pkgname));
            }
        } catch (PackageManager.NameNotFoundException e){
            return false;
        }
    }

    private String hash(String pkgname) {
        if (pkgname == null) return null;
        try {
            PackageManager pm = getContext().getPackageManager();
            PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);
            if (pkginfo.signatures.length != 1) return null;    // 複数署名は扱わない
            Signature sig = pkginfo.signatures[0];
            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            return byte2hex(sha256);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    private byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }

    private String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }

}
s16as16a

ContentResolver 002

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- MEMO:独自定義Signature Permissionの利用を宣言 -->
    <uses-permission
        android:name="com.example.provider002.MY_PERMISSION" />

    <queries>
        <!-- MEMO:利用するContentProviderの定義と同じ authorities を指定 -->
        <provider android:authorities="com.example.provider002.mycontentprovider" />
    </queries>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Resolver002"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
package com.example.resolver002;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;

import java.util.concurrent.Executor;


public class MainActivity extends AppCompatActivity {
    private static String TAG = " TestContentResolver";

    // ContentProviderのURIに関する定義
    private static final String AUTHORITY = "com.example.provider002.mycontentprovider";
/*
    public static final Uri CONTENT_URI_BOOL = Uri.parse("content://" + AUTHORITY + "/bool_data");
    public static final Uri CONTENT_URI_INT = Uri.parse("content://" + AUTHORITY + "/int_data");
    public static final Uri CONTENT_URI_STRING = Uri.parse("content://" + AUTHORITY + "/string_data");
*/
    public static final Uri CONTENT_URI_VERSION_CODE = Uri.parse("content://" + AUTHORITY + "/version_code");
    public static final Uri CONTENT_URI_VERSION_NAME = Uri.parse("content://" + AUTHORITY + "/version_name");
    public static final Uri CONTENT_URI_SETTLEMENT_DATE = Uri.parse("content://" + AUTHORITY + "/settlement_date");

    // プロバイダ側で宣言している Signature Permission
    private static final String SIGNATURE_PERMISSION = "com.example.provider002.MY_PERMISSION";

    // 証明書のハッシュ値
    private static String sMyCertHash = null;
    private static String getCertHash(Context context) {
        if (sMyCertHash == null) {
            if (Utils.isDebuggable(context)) {
                // debug.keystoreの"androiddebugkey"の証明書ハッシュ値
                sMyCertHash = "C7B59F4D 833C6985 51531BEF AC2DC62A 34F86153 4A0A13FC 3C30F400 B3B87801";
            } else {
                // keystoreの"my company key"の証明書ハッシュ値
                sMyCertHash = "D397D343 A5CBC10F 4EDDEB7C A10062DE 5690984F 1FB9E88B D7B3A7C2 42E142CA";
            }
        }
        return sMyCertHash;
    }

    // 利用先Content Providerのパッケージ名を取得
    private static String getProviderPkgname(Context context, Uri uri) {
        Log.d(TAG, "getProviderPkgname called, uri=" + uri);

        String pkgname = null;
        PackageManager pm = context.getPackageManager();
        ProviderInfo pi = pm.resolveContentProvider(uri.getAuthority(), 0);
        if (pi != null) pkgname = pi.packageName;

        Log.d(TAG, "getProviderPkgname done, pkgname:" + pkgname);
        return pkgname;
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.d(TAG, "onCreate called");

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, "onCreate done");
    }

    @Override
    protected void onResume() {
        Log.d(TAG, "onResume called");
        super.onResume();
/*
        // ---------------------------------------------
        //MEMO:例外発生することを確認する(ContentsProvider経由でboolean値は取得できないので)
        try {
            boolean boolValue = DataRetrievalHelper.getBoolValue(this, CONTENT_URI_BOOL, "state");
            Log.d(TAG, "onResume: boolValue=" + boolValue);
        } catch (Exception e) {
            Log.d(TAG, "onResume: error=" + e);
        }

        // ---------------------------------------------
        //MEMO:数値が取得できることを確認する
        int intValue = DataRetrievalHelper.getIntValue(this, CONTENT_URI_INT, "ages");
        Log.d(TAG, "onResume: intValue=" + intValue);

        // ---------------------------------------------
        //MEMO:文字列が取得できることを確認する
        String stringValue = DataRetrievalHelper.getStringValue(this, CONTENT_URI_STRING, "message");
        Log.d(TAG, "onResume: stringValue=" + stringValue);
*/
        // ---------------------------------------------
        // Signature Permissionが ContentsProvider側で定義されている確認する ->Signature Permissionで宣言されていない場合、ここで処理終了
        if (!SigPerm.test(this, SIGNATURE_PERMISSION, getCertHash(this))) {
            Log.d(TAG, "onResume done, Signature Permissionが ContentsProvider側で定義されていません");
            return;
        }

        // ---------------------------------------------
        // 利用先ContentsProviderパッケージ名を取得する
        String pkgname = getProviderPkgname(this, CONTENT_URI_VERSION_CODE);
        Log.d(TAG, "onResume: pkgname=" + pkgname);

        // 利用先Content Providerの証明書が自身の証明書と同一であることを確認する
        if (!PkgCert.test(this, pkgname, getCertHash(this))) {
            Log.d(TAG, "onResume done, 利用先 Content Provider と自身の証明書が異なります");
            return;
        }

        // バージョンコードが数値で取得できることを確認する
        int versionCode = DataRetrievalHelper.getIntValue(this, CONTENT_URI_VERSION_CODE, "code");
        Log.d(TAG, "onResume: versionCode=" + versionCode);

        // ---------------------------------------------
        // 利用先ContentsProviderパッケージ名を取得する
        pkgname = getProviderPkgname(this, CONTENT_URI_VERSION_NAME);
        Log.d(TAG, "onResume: pkgname=" + pkgname);

        // 利用先Content Providerの証明書が自身の証明書と同一であることを確認する
        if (!PkgCert.test(this, pkgname, getCertHash(this))) {
            Log.d(TAG, "onResume done, 利用先 Content Provider と自身の証明書が異なります");
            return;
        }

        // バージョンネームが数値で取得できることを確認する
        String versionName = DataRetrievalHelper.getStringValue(this, CONTENT_URI_VERSION_NAME, "name");
        Log.d(TAG, "onResume: versionName=" + versionName);

        // ---------------------------------------------
        // 利用先ContentsProviderパッケージ名を取得する
        pkgname = getProviderPkgname(this, CONTENT_URI_SETTLEMENT_DATE);
        Log.d(TAG, "onResume: pkgname=" + pkgname);

        // 利用先Content Providerの証明書が自身の証明書と同一であることを確認する
        if (!PkgCert.test(this, pkgname, getCertHash(this))) {
            Log.d(TAG, "onResume done, 利用先 Content Provider と自身の証明書が異なります");
            return;
        }

        // 決済日時が文字列で取得できることを確認する
        String settlementDate = DataRetrievalHelper.getStringValue(this, CONTENT_URI_SETTLEMENT_DATE, "date");
        Log.d(TAG, "onResume: settlementDate=" + settlementDate);

        Log.d(TAG, "onResume done");
    }
}
package com.example.resolver002;

import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

public class DataRetrievalHelper {
    private static String TAG = " TestContentResolver";


    public static boolean getBoolValue(Context context, Uri uri, String key) {
        Log.d(TAG, "getBoolValue called, uri=" + uri + ", key=" + key);
        boolean result = false;

        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getBoolValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex(key);
            Log.d(TAG, "getBoolValue: column=" + column);
            
            if (column > -1) {
                //result = cursor.getInt(column) != 0;
                Log.d(TAG, "getBoolValue: cursor.getInt(column)=" + cursor.getInt(column));
                result = false;
                cursor.close();
            }
        }

        Log.d(TAG, "getBoolValue done, return:" + result);
        return result;
    }

    public static int getIntValue(Context context, Uri uri, String key) {
        Log.d(TAG, "getIntValue called, uri=" + uri + ", key=" + key);
        int result = 0;

        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getIntValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex(key);
            Log.d(TAG, "getIntValue: column=" + column);

            if (column > -1) {
                result = cursor.getInt(column);
                cursor.close();
            }
        }

        Log.d(TAG, "getIntValue done, return:" + result);
        return result;
    }

    public static String getStringValue(Context context, Uri uri, String key) {
        Log.d(TAG, "getStringValue called, uri=" + uri + ", key=" + key);
        String result = "";

        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        Log.d(TAG, "getStringValue: cursor=" + cursor);

        if (cursor != null && cursor.moveToFirst()) {
            int column = cursor.getColumnIndex(key);
            Log.d(TAG, "getStringValue: column=" + column);

            if (column > -1) {
                result = cursor.getString(column);
                cursor.close();
            }
        }

        Log.d(TAG, "getStringValue done, return:\"" + result + "\"");
        return result;
    }

}
package com.example.resolver002;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;
import android.util.Log;

import static android.content.pm.PackageManager.CERT_INPUT_SHA256;

public class SigPerm {
    private static String TAG = "SigPerm";

    public static boolean test(Context ctx, String sigPermName, String correctHash) {
        if (correctHash == null) return false;
        correctHash = correctHash.replaceAll(" ", "");
        try{
            //MEMO:sigPermNameはプロバイダでマニフェストに宣言した署名パーミッション、
            //        つまりプロバイダのパッケージ名を取得してる
            // sigPermNameを定義したアプリのパッケージ名を取得する
            PackageManager pm = ctx.getPackageManager();
            PermissionInfo pi = pm.getPermissionInfo(sigPermName, PackageManager.GET_META_DATA);
            String pkgname = pi.packageName;
            Log.d(TAG, "test: pkgname=" + pkgname);

            Log.d(TAG, "test: pi.protectionLevel=" + pi.protectionLevel);
            Log.d(TAG, "test: PermissionInfo.PROTECTION_SIGNATURE=" + PermissionInfo.PROTECTION_SIGNATURE);

            // 非Signature Permissionの場合は失敗扱い
            if (pi.protectionLevel != PermissionInfo.PROTECTION_SIGNATURE) return false;

            Log.d(TAG, "test: Build.VERSION.SDK_INT=" + Build.VERSION.SDK_INT);

            // pkgnameの実際のハッシュ値と正解のハッシュ値を比較する
            if (Build.VERSION.SDK_INT >= 28) {
                // ★ API Level >= 28 ではPackage ManagerのAPIで直接検証が可能
                return pm.hasSigningCertificate(pkgname, Utils.hex2Bytes(correctHash), CERT_INPUT_SHA256);
            } else {
                // API Level < 28 の場合はPkgCertを利用し、ハッシュ値を取得して比較する
                return correctHash.equals(PkgCert.hash(ctx, pkgname));
            }
        } catch (NameNotFoundException e){
            return false;
        }
    }
}
package com.example.resolver002;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.Signature;
import android.util.Log;

public class PkgCert {
    private static String TAG = "PkgCert";

    //MEMO:自身の署名してる証明書のハッシュ値と、相手先の署名してるハッシュ値と同じが同じかチェックしてる
    public static boolean test(Context ctx, String pkgname, String correctHash) {
        Log.d(TAG, "test called, pkgname=" + pkgname);

        if (correctHash == null) return false;

        correctHash = correctHash.replaceAll(" ", "");

        //return correctHash.equals(hash(ctx, pkgname));
        boolean b1 = correctHash.equals(hash(ctx, pkgname));
        Log.d(TAG, "test done, return:" + b1);
        return b1;
    }

    //MEMO:パッケージを指定して、そのパッケージのアプリが署名しているハッシュ値を取得してる
    public static String hash(Context ctx, String pkgname) {
        Log.d(TAG, "hash called, pkgname=" + pkgname);

        if (pkgname == null) return null;
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pkginfo = pm.getPackageInfo(pkgname, PackageManager.GET_SIGNATURES);

            Log.d(TAG, "hash: pkginfo.signatures.length=" + pkginfo.signatures.length);

            if (pkginfo.signatures.length != 1) return null;    // 複数署名は扱わない

            Signature sig = pkginfo.signatures[0];
            Log.d(TAG, "hash: sig=" + sig);

            byte[] cert = sig.toByteArray();
            byte[] sha256 = computeSha256(cert);
            //return byte2hex(sha256);
            String s1 = byte2hex(sha256);
            Log.d(TAG, "hash done, return:" + s1);
            return s1;
        } catch (NameNotFoundException e) {
            return null;
        }
    }

    private static byte[] computeSha256(byte[] data) {
        try {
            return MessageDigest.getInstance("SHA-256").digest(data);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    private static String byte2hex(byte[] data) {
        if (data == null) return null;
        final StringBuilder hexadecimal = new StringBuilder();
        for (final byte b : data) {
            hexadecimal.append(String.format("%02X", b));
        }
        return hexadecimal.toString();
    }
}
package com.example.resolver002;

import java.util.List;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;

public class Utils {
    public static boolean isDebuggable(Context ctx) {
        try {
            PackageManager pm = ctx.getPackageManager();
            ApplicationInfo ai = pm.getApplicationInfo(ctx.getPackageName(), 0);
            if ((ai.flags & ApplicationInfo.FLAG_DEBUGGABLE) == ApplicationInfo.FLAG_DEBUGGABLE)
                return true;
        } catch (NameNotFoundException e) {
        }
        return false;
    }
    public static String getPackageNameFromPid(Context ctx, int pid) {
        ActivityManager am = (ActivityManager) ctx.getSystemService(Activity.ACTIVITY_SERVICE);
        List<RunningAppProcessInfo> processes = am.getRunningAppProcesses();
        for (int i = 0; i < processes.size(); i++) {
            RunningAppProcessInfo info = processes.get(i);
            if (info.pid == pid) {
                String[] packages = info.pkgList;
                if (packages.length > 0) {
                    return packages[0];
                }
                break;
            }
        }
        return null;
    }
    public static String getPackageNameFromUid(Context ctx, int uid) {
        PackageManager pm = ctx.getPackageManager();
        String[] packages = pm.getPackagesForUid(uid);
        // sharedUserIdを使用するアプリへの対応はサンプルにつき省略
        if (packages.length == 1) {
            return packages[0];
        }
        return null;
    }

    public static byte[] hex2Bytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                    + Character.digit(s.charAt(i+1), 16));
        }
        return data;
    }
}
s16as16a

TemporaryProvider

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.temporaryprovider">

  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >

    <activity
        android:name=".TemporaryActiveGrantActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <!-- 一時許可Content Provider -->
    <!-- ★ポイント1★ exported="false"により、一時許可するPath以外を非公開設定する -->
    <provider
        android:name=".TemporaryProvider"
        android:authorities="org.jssec.android.provider.temporaryprovider"
        android:exported="false" >

      <!-- ★ポイント2★ grant-uri-permissionにより、一時許可するPathを指定する -->
      <grant-uri-permission android:path="/addresses" />

    </provider>

    <activity
        android:name=".TemporaryPassiveGrantActivity"
        android:label="@string/app_name"
        android:exported="true" />
  </application>
</manifest>
/*
 * Copyright (C) 2012-2022 Japan Smartphone Security Association
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jssec.android.provider.temporaryprovider;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class TemporaryProvider extends ContentProvider {
    public static final String AUTHORITIY =
        "org.jssec.android.provider.temporaryprovider";
    public static final String CONTENT_TYPE =
        "vnd.android.cursor.dir/vnd.org.jssec.contenttype";
    public static final String CONTENT_ITEM_TYPE =
        "vnd.android.cursor.item/vnd.org.jssec.contenttype";

    // Content Providerが提供するインターフェースを公開
    public interface Download {
        public static final String PATH = "downloads";
        public static final Uri CONTENT_URI =
            Uri.parse("content://" + AUTHORITIY + "/" + PATH);
    }
    public interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
            Uri.parse("content://" + AUTHORITIY + "/" + PATH);
    }

    // UriMatcher
    private static final int DOWNLOADS_CODE = 1;
    private static final int DOWNLOADS_ID_CODE = 2;
    private static final int ADDRESSES_CODE = 3;
    private static final int ADDRESSES_ID_CODE = 4;
    private static UriMatcher sUriMatcher;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITIY, Download.PATH, DOWNLOADS_CODE);
        sUriMatcher.addURI(AUTHORITIY, Download.PATH + "/#", DOWNLOADS_ID_CODE);
        sUriMatcher.addURI(AUTHORITIY, Address.PATH, ADDRESSES_CODE);
        sUriMatcher.addURI(AUTHORITIY, Address.PATH + "/#", ADDRESSES_ID_CODE);
    }

    // DBを使用せずに固定値を返す例にしているため、queryメソッドで返すCursorを事前に定義
    private static MatrixCursor sAddressCursor =
        new MatrixCursor(new String[] { "_id", "pref" });
    static {
        sAddressCursor.addRow(new String[] { "1", "北海道" });
        sAddressCursor.addRow(new String[] { "2", "青森" });
        sAddressCursor.addRow(new String[] { "3", "岩手" });
    }
    private static MatrixCursor sDownloadCursor =
        new MatrixCursor(new String[] { "_id", "path" });
    static {
        sDownloadCursor.addRow(new String[] { "1", "/sdcard/downloads/sample.jpg" });
        sDownloadCursor.addRow(new String[] { "2", "/sdcard/downloads/sample.txt" });
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {

        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case ADDRESSES_CODE:
            return CONTENT_TYPE;

        case DOWNLOADS_ID_CODE:
        case ADDRESSES_ID_CODE:
            return CONTENT_ITEM_TYPE;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {

        // ★ポイント3★ 一時的に許可したアプリからのリクエストであっても、パラメータの安全性を確認する
        // ここではuriが想定の範囲内であることを、UriMatcher#match()とswitch caseで確認している。
        // その他のパラメータの確認はサンプルにつき省略。「3.2 入力データの安全性を確認する」を参照。
        // ★ポイント4★ 一時的に許可したアプリに開示してよい情報に限り返送してよい
        // queryの結果がセンシティブな意味を持つかどうかはアプリ次第。
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
        case DOWNLOADS_ID_CODE:
            return sDownloadCursor;

        case ADDRESSES_CODE:
        case ADDRESSES_ID_CODE:
            return sAddressCursor;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // ★ポイント3★ 一時的に許可したアプリからのリクエストであっても、パラメータの安全性を確認する
        // ここではuriが想定の範囲内であることを、UriMatcher#match()とswitch caseで確認している。
        // その他のパラメータの確認はサンプルにつき省略。「3.2 入力データの安全性を確認する」を参照。
        // ★ポイント4★ 一時的に許可したアプリに開示してよい情報に限り返送してよい
        // Insert結果、発番されるIDがセンシティブな意味を持つかどうかはアプリ次第。
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return ContentUris.withAppendedId(Download.CONTENT_URI, 3);

        case ADDRESSES_CODE:
            return ContentUris.withAppendedId(Address.CONTENT_URI, 4);

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        // ★ポイント3★ 一時的に許可したアプリからのリクエストであっても、パラメータの安全性を確認する
        // ここではuriが想定の範囲内であることを、UriMatcher#match()とswitch caseで確認している。
        // その他のパラメータの確認はサンプルにつき省略。「3.2 入力データの安全性を確認する」を参照。
        // ★ポイント4★ 一時的に許可したアプリに開示してよい情報に限り返送してよい
        // Updateされたレコード数がセンシティブな意味を持つかどうかはアプリ次第。
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 5;   // updateされたレコード数を返す

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 15;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {

        // ★ポイント3★ 一時的に許可したアプリからのリクエストであっても、パラメータの安全性を確認する
        // ここではuriが想定の範囲内であることを、UriMatcher#match()とswitch caseで確認している。
        // その他のパラメータの確認はサンプルにつき省略。「3.2 入力データの安全性を確認する」を参照。
        // ★ポイント4★ 一時的に許可したアプリに開示してよい情報に限り返送してよい
        // Deleteされたレコード数がセンシティブな意味を持つかどうかはアプリ次第。
        switch (sUriMatcher.match(uri)) {
        case DOWNLOADS_CODE:
            return 10;  // deleteされたレコード数を返す

        case DOWNLOADS_ID_CODE:
            return 1;

        case ADDRESSES_CODE:
            return 20;

        case ADDRESSES_ID_CODE:
            return 1;

        default:
            throw new IllegalArgumentException("Invalid URI:" + uri);
        }
    }
}
/*
 * Copyright (C) 2012-2022 Japan Smartphone Security Association
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jssec.android.provider.temporaryprovider;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class TemporaryActiveGrantActivity extends Activity {

    // User Activityに関する情報
    private static final String TARGET_PACKAGE =
        "org.jssec.android.provider.temporaryuser";
    private static final String TARGET_ACTIVITY =
        "org.jssec.android.provider.temporaryuser.TemporaryUserActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.active_grant);
    }

    // Content Provider側アプリが能動的に他のアプリにアクセス許可を与えるケース
    public void onSendClick(View view) {
        try {
            Intent intent = new Intent();

            // ★ポイント5★ 一時的にアクセスを許可するURIをIntentに指定する
            intent.setData(TemporaryProvider.Address.CONTENT_URI);

            // ★ポイント6★ 一時的に許可するアクセス権限をIntentに指定する
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

            // ★ポイント7★ 一時的にアクセスを許可するアプリに明示的Intentを送信する
            intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
            startActivity(intent);

        } catch (ActivityNotFoundException e) {
            Toast.makeText(this, "User Activityが見つからない。", Toast.LENGTH_LONG).show();
        }
    }
}
/*
 * Copyright (C) 2012-2022 Japan Smartphone Security Association
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jssec.android.provider.temporaryprovider;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;

public class TemporaryPassiveGrantActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.passive_grant);
    }

    // 一時的なアクセス許可を求めてきたアプリにContent Provider側アプリが受動的にアクセス許可を与えるケース
    public void onGrantClick(View view) {
        Intent intent = new Intent();

        // ★ポイント5★ 一時的にアクセスを許可するURIをIntentに指定する
        intent.setData(TemporaryProvider.Address.CONTENT_URI);

        // ★ポイント6★ 一時的に許可するアクセス権限をIntentに指定する
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

        // ★ポイント8★ 一時許可の要求元アプリにIntentを返信する
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    public void onCloseClick(View view) {
        finish();
    }
}
s16as16a

TemporaryUser

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="org.jssec.android.provider.temporaryuser">

  <queries>
    <package android:name="org.jssec.android.provider.temporaryprovider" />
  </queries>
  
  <application
      android:icon="@drawable/ic_launcher"
      android:label="@string/app_name" >
    
    <activity
        android:name=".TemporaryUserActivity"
        android:label="@string/app_name"
        android:exported="true" >
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>
  </application>

</manifest>
/*
 * Copyright (C) 2012-2022 Japan Smartphone Security Association
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jssec.android.provider.temporaryuser;

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class TemporaryUserActivity extends Activity {

    // Provider Activityに関する情報
    private static final String TARGET_PACKAGE =
        "org.jssec.android.provider.temporaryprovider";
    private static final String TARGET_ACTIVITY =
        "org.jssec.android.provider.temporaryprovider.TemporaryPassiveGrantActivity";

    // 利用先のContent Provider情報
    private static final String AUTHORITY = "org.jssec.android.provider.temporaryprovider";
    private interface Address {
        public static final String PATH = "addresses";
        public static final Uri CONTENT_URI =
            Uri.parse("content://" + AUTHORITY + "/" + PATH);
    }

    private static final int REQUEST_CODE = 1;

    public void onQueryClick(View view) {

        logLine("[Query]");

        Cursor cursor = null;
        try {
            if (!providerExists(Address.CONTENT_URI)) {
                logLine("  Content Providerが不在");
                return;
            }

            // ★ポイント9★ センシティブな情報をリクエストに含めてはならない
            // リクエスト先のアプリがマルウェアである可能性がある。
            // マルウェアに取得されても問題のない情報であればリクエストに含めてもよい。
            cursor = getContentResolver()
                    .query(Address.CONTENT_URI, null, null, null, null);

            // ★ポイント10★ 結果データの安全性を確認する
            // サンプルにつき割愛。「3.2 入力データの安全性を確認する」を参照。
            if (cursor == null) {
                logLine("  null cursor");
            } else {
                boolean moved = cursor.moveToFirst();
                while (moved) {
                    logLine(String.format("  %d, %s",
                                          cursor.getInt(0),
                                          cursor.getString(1)));
                    moved = cursor.moveToNext();
                }
            }
        } catch (SecurityException ex) {
            logLine("  例外:" + ex.getMessage());
        }
        finally {
            if (cursor != null) cursor.close();
        }
    }

    // このアプリが一時的なアクセス許可を要求し、Content Provider側アプリが受動的にアクセス許可を与えるケース
    public void onGrantRequestClick(View view) {
        Intent intent = new Intent();
        intent.setClassName(TARGET_PACKAGE, TARGET_ACTIVITY);
        try {
            startActivityForResult(intent, REQUEST_CODE);
        } catch (ActivityNotFoundException e) {
            logLine("Grantの要求に失敗しました。\nTemporaryProviderがインストールされているか確認してください。");
        }
    }

    private boolean providerExists(Uri uri) {
        ProviderInfo pi =
            getPackageManager().resolveContentProvider(uri.getAuthority(), 0);
        return (pi != null);
    }

    private TextView mLogView;

    // Content Provider側アプリが能動的にこのアプリにアクセス許可を与えるケース
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mLogView = (TextView)findViewById(R.id.logview);
    }

    private void logLine(String line) {
        mLogView.append(line);
        mLogView.append("\n");
    }
}
s16as16a

Flutterで作成したAndroidアプリの起動シーケンスは、以下のような流れで進行します。

1. アプリケーションのエントリポイント

Flutterアプリのエントリポイントは main.dart ファイルにある main 関数です。この関数は、アプリケーションの実行を開始します。

void main() {
  runApp(MyApp());
}

2. runApp の実行

runApp 関数は、ウィジェットツリーのルートとなるウィジェットを引数に取ります。ここで指定されたウィジェットが、アプリの最上位ウィジェットとして構築されます。

3. Flutterエンジンの起動

runApp の実行により、Flutterエンジンが起動されます。Flutterエンジンは、以下の役割を果たします。

  • Dartコードを実行する仮想マシン(VM)を初期化
  • Skiaグラフィックスライブラリを使用して描画を処理
  • プラットフォームチャネルを介してネイティブコードと通信

4. ウィジェットツリーの構築

runApp に渡されたルートウィジェット(例えば MyApp)から始まるウィジェットツリーが構築されます。このツリーは、アプリケーションのUIを表します。

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

5. プラットフォーム固有の初期化

Androidの場合、MainActivity が起動し、Flutterエンジンを初期化します。 MainActivity は通常、以下のように定義されます。

public class MainActivity extends FlutterActivity {
  // This class automatically initializes the Flutter engine and displays the Dart UI
}

この段階で、ネイティブコードとFlutterコード間の通信チャネルも初期化されます。

6. ウィジェットのレンダリング

Flutterエンジンは、ウィジェットツリーの構築が完了すると、build メソッドを順次呼び出して、各ウィジェットをレンダリングします。レンダリングされたウィジェットは、Skiaを使って画面に描画されます。

7. イベントループの開始

アプリケーションが起動した後、Flutterエンジンはイベントループを開始します。このループは、ユーザーインタラクション(タップ、スクロールなど)やシステムイベント(タイマー、ネットワークイベントなど)を処理します。

8. ライフサイクルの管理

Flutterは、アプリのライフサイクルイベント(起動、停止、再開など)を管理します。Androidのライフサイクルイベントは、Flutterエンジンを介してFlutterアプリに伝えられ、適切なコールバック(例:didChangeAppLifecycleState)が呼び出されます。

class MyApp extends StatefulWidget {
  
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    // Handle lifecycle state changes
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Lifecycle Example')),
        body: Center(child: Text('Hello World')),
      ),
    );
  }
}

以上が、Flutterで作成されたAndroidアプリの起動シーケンスの概要です。Flutterエンジンの初期化からウィジェットツリーの構築、描画、そしてアプリケーションのライフサイクル管理までの流れを通して、アプリがユーザーに表示され、インタラクションを受け付ける準備が整います。

s16as16a

Flutterで作成したAndroidアプリの起動シーケンスにおいて、MainActivity が起動された以降のプロセスについて具体的に説明します。

1. MainActivityの起動

MainActivity は、通常 FlutterActivity を継承して定義されます。これにより、Flutterエンジンの初期化が自動的に行われます。

public class MainActivity extends FlutterActivity {
  // FlutterActivity handles Flutter engine initialization
}

2. Flutterエンジンの初期化

FlutterActivityonCreate メソッドが呼び出されると、以下の処理が行われます:

  • Flutterエンジンのインスタンスが生成されます。
  • Dartエントリーポイント(通常 main 関数)が呼び出されます。
  • FlutterエンジンがAndroidプラットフォームにバインドされます。

3. FlutterViewの設定

FlutterActivityFlutterView を設定します。FlutterView は、Flutterエンジンがレンダリングするコンテンツを表示するビューです。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // This method sets up Flutter's rendering surface
    configureFlutterEngine(FlutterEngine flutterEngine);
}

4. Flutter/Dartコードの実行

Flutterエンジンが初期化されると、main.dart ファイルに定義された main 関数が実行されます。この関数は、アプリケーションのルートウィジェットを構築するために runApp を呼び出します。

void main() {
  runApp(MyApp());
}

5. ウィジェットツリーの構築とレンダリング

runApp が呼び出されると、Flutterは以下の処理を行います:

  • ルートウィジェット(例えば MyApp)がインスタンス化されます。
  • ルートウィジェットの build メソッドが呼び出され、ウィジェットツリーが構築されます。
  • ウィジェットツリーが構築されると、Flutterエンジンはそれを画面にレンダリングします。
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: Text('Hello, world!'),
        ),
      ),
    );
  }
}

6. プラットフォームチャネルの初期化

Flutterとネイティブコード(Android)との通信は、プラットフォームチャネルを介して行われます。プラットフォームチャネルは、メッセージの送受信を可能にするためのメカニズムです。

プラットフォームチャネルの例

Dart側:

import 'package:flutter/services.dart';

void sendMessageToNative() {
  const platform = MethodChannel('com.example.channel');
  platform.invokeMethod('nativeMethod', {'param': 'value'});
}

Android側:

public class MainActivity extends FlutterActivity {
  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.channel")
      .setMethodCallHandler(
        (call, result) -> {
          if (call.method.equals("nativeMethod")) {
            String param = call.argument("param");
            // Do something with the param
            result.success("Received: " + param);
          } else {
            result.notImplemented();
          }
        }
      );
  }
}

7. イベントループの開始

Flutterエンジンは、Dartイベントループを起動します。このループは、ユーザーインタラクション(タップ、スクロールなど)やシステムイベント(タイマー、ネットワークイベントなど)を処理します。

8. ライフサイクルの管理

FlutterActivity は、Androidのライフサイクルイベントを適切にFlutterエンジンに伝えます。これにより、Dartコードがこれらのイベントに応じて適切な処理を行えます。

ライフサイクルイベントの例

Android側:

@Override
protected void onPause() {
  super.onPause();
  // Notify Flutter engine about the pause event
  flutterEngine.getLifecycleChannel().appIsInactive();
}

@Override
protected void onResume() {
  super.onResume();
  // Notify Flutter engine about the resume event
  flutterEngine.getLifecycleChannel().appIsResumed();
}

Dart側:

class MyApp extends StatefulWidget {
  
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.paused) {
      // Handle app paused state
    } else if (state == AppLifecycleState.resumed) {
      // Handle app resumed state
    }
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Lifecycle Example')),
        body: Center(child: Text('Hello World')),
      ),
    );
  }
}

これで、Flutterで作成されたAndroidアプリの起動シーケンスの詳細を説明しました。MainActivity の起動以降、Flutterエンジンの初期化、ウィジェットツリーの構築とレンダリング、プラットフォームチャネルの初期化、イベントループの開始、ライフサイクルの管理が行われることになります。

s16as16a

MainActivity に続いて別のアクティビティである RenewalTopActivity が起動されるシーケンスについて説明します。このシーケンスは、一般的に MainActivity から RenewalTopActivity への遷移が含まれ、通常はある条件が満たされた場合に行われます。

1. MainActivityの起動

まず、MainActivity が起動されます。MainActivityFlutterActivity を継承しており、Flutterエンジンの初期化とDartコードの実行を開始します。

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Check some condition to determine if RenewalTopActivity should be launched
    if (shouldLaunchRenewalTopActivity()) {
      launchRenewalTopActivity();
    }
  }

  private boolean shouldLaunchRenewalTopActivity() {
    // Define your condition here
    return true; // This is just an example
  }

  private void launchRenewalTopActivity() {
    Intent intent = new Intent(this, RenewalTopActivity.class);
    startActivity(intent);
    finish(); // Optionally finish MainActivity if it's no longer needed
  }
}

2. RenewalTopActivityの起動

MainActivityonCreate メソッドで定義された条件が満たされた場合、RenewalTopActivity が起動されます。RenewalTopActivity も通常のAndroidアクティビティとして定義されますが、Flutterと統合されるために特定の処理が必要です。

RenewalTopActivityの定義

RenewalTopActivity がFlutterエンジンを使用する場合、FlutterActivity を継承するか、既存のFlutterエンジンを使用するように設定します。

public class RenewalTopActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Additional setup if needed
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    // Register any plugins or channels if needed
  }
}

3. RenewalTopActivityの起動処理

RenewalTopActivity が起動されると、以下のような手順でFlutterエンジンが再初期化されます。

  • FlutterActivity がFlutterエンジンをセットアップします。
  • Dart エントリーポイントが再度実行され、適切なウィジェットがレンダリングされます。

4. ウィジェットツリーの構築とレンダリング

RenewalTopActivity が起動すると、Flutterエンジンは再びDartコードを実行し、ウィジェットツリーを構築します。例えば、以下のようなトップ画面のウィジェットが表示されるとします。

class RenewalTopPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Renewal Top Page'),
      ),
      body: Center(
        child: Text('Welcome to the Renewal Top Page'),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    home: RenewalTopPage(),
  ));
}

5. プラットフォームチャネルの使用

もし RenewalTopActivity でネイティブコードとのやり取りが必要な場合、プラットフォームチャネルを使用します。これは MainActivity と同様の方法で実装されます。

Dart側のプラットフォームチャネル

import 'package:flutter/services.dart';

void sendMessageToNative() {
  const platform = MethodChannel('com.example.channel');
  platform.invokeMethod('nativeMethod', {'param': 'value'});
}

RenewalTopActivity側のプラットフォームチャネル

public class RenewalTopActivity extends FlutterActivity {
  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.channel")
      .setMethodCallHandler(
        (call, result) -> {
          if (call.method.equals("nativeMethod")) {
            String param = call.argument("param");
            // Do something with the param
            result.success("Received: " + param);
          } else {
            result.notImplemented();
          }
        }
      );
  }
}

6. イベントループとライフサイクルの管理

RenewalTopActivity でも、イベントループが開始され、アプリケーションのライフサイクルイベントが適切に管理されます。

RenewalTopActivityのライフサイクル管理

@Override
protected void onPause() {
  super.onPause();
  // Notify Flutter engine about the pause event
  flutterEngine.getLifecycleChannel().appIsInactive();
}

@Override
protected void onResume() {
  super.onResume();
  // Notify Flutter engine about the resume event
  flutterEngine.getLifecycleChannel().appIsResumed();
}

Dart側のライフサイクル管理

class RenewalTopPage extends StatefulWidget {
  
  _RenewalTopPageState createState() => _RenewalTopPageState();
}

class _RenewalTopPageState extends State<RenewalTopPage> with WidgetsBindingObserver {
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.paused) {
      // Handle app paused state
    } else if (state == AppLifecycleState.resumed) {
      // Handle app resumed state
    }
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Lifecycle Example')),
      body: Center(child: Text('Welcome to the Renewal Top Page')),
    );
  }
}

このシーケンスにより、MainActivity から RenewalTopActivity への遷移がスムーズに行われ、Flutterエンジンとウィジェットツリーの再初期化が行われます。これにより、ユーザーは新しい画面に移動し、引き続きFlutterの豊富なUI機能を享受できます。

s16as16a

RenewalTopActivity は、Flutterで作成されたAndroidアプリにおいて特定の条件下で起動される追加のアクティビティです。その役割について具体的に説明します。

1. 導入の背景

RenewalTopActivity は、通常、アプリの特定の更新や再構築が必要な場合に使用されます。このアクティビティは、ユーザーに新しい情報や重要な更新を提供するための画面として機能します。

2. 起動シーケンス

通常の起動シーケンスでは、MainActivity が最初に起動されますが、特定の条件が満たされると、RenewalTopActivity が起動されます。この条件には、アプリのバージョンアップ、重要なコンテンツ更新、または特別なイベントの通知などが含まれる場合があります。

MainActivityからの遷移

MainActivityonCreate メソッド内で、RenewalTopActivity を起動するロジックが含まれます。

public class MainActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 条件に基づいて RenewalTopActivity を起動
    if (shouldLaunchRenewalTopActivity()) {
      launchRenewalTopActivity();
    } else {
      // 通常の Flutter エンジン初期化と Dart エントリーポイント実行
      configureFlutterEngine(getFlutterEngine());
    }
  }

  private boolean shouldLaunchRenewalTopActivity() {
    // 条件を判定するロジック(例:バージョンチェック)
    return checkAppVersion();
  }

  private void launchRenewalTopActivity() {
    Intent intent = new Intent(this, RenewalTopActivity.class);
    startActivity(intent);
    finish(); // MainActivity を終了して RenewalTopActivity に遷移
  }

  private boolean checkAppVersion() {
    // アプリのバージョンをチェックするロジック
    return true; // ここでは常に true としていますが、実際にはバージョン比較などを行います
  }
}

3. RenewalTopActivityの役割

RenewalTopActivity の主な役割は、ユーザーにアプリの更新情報や重要なメッセージを提供することです。このアクティビティは以下のような機能を持つことが多いです。

アプリの更新通知

ユーザーに新しいバージョンのアプリが利用可能であることを通知し、更新を促す画面を表示します。

特別なイベントの通知

キャンペーンや特別なイベントに関する情報をユーザーに伝えます。

重要なコンテンツ更新

重要なコンテンツや機能が追加された場合、その情報をユーザーに提供します。

RenewalTopActivityの実装例

public class RenewalTopActivity extends FlutterActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ここで Flutter エンジンを初期化し、Dart エントリーポイントを設定
    configureFlutterEngine(getFlutterEngine());
  }

  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    // 必要に応じて Flutter プラグインを登録
    // GeneratedPluginRegistrant.registerWith(flutterEngine);
  }
}

4. ウィジェットツリーの構築

RenewalTopActivity が起動された後、Flutterエンジンが初期化され、Dart エントリーポイントが実行されます。これにより、特定の更新情報を表示するウィジェットツリーが構築されます。

RenewalTopActivityに対応するDartコード

以下は、更新情報を表示するためのDartコードの例です。

void main() {
  runApp(RenewalApp());
}

class RenewalApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: RenewalTopPage(),
    );
  }
}

class RenewalTopPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Renewal Top Page'),
      ),
      body: Center(
        child: Text('Welcome to the Renewal Top Page'),
      ),
    );
  }
}

5. プラットフォームチャネルの利用

RenewalTopActivity では、ネイティブコードとのやり取りが必要になる場合があり、プラットフォームチャネルを利用します。

Dart側のプラットフォームチャネル

import 'package:flutter/services.dart';

void sendMessageToNative() {
  const platform = MethodChannel('com.example.renewal/channel');
  platform.invokeMethod('notifyUpdate', {'message': 'App has been updated'});
}

RenewalTopActivity側のプラットフォームチャネル

public class RenewalTopActivity extends FlutterActivity {
  @Override
  public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
    super.configureFlutterEngine(flutterEngine);
    new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.renewal/channel")
      .setMethodCallHandler(
        (call, result) -> {
          if (call.method.equals("notifyUpdate")) {
            String message = call.argument("message");
            // ネイティブコードでの処理
            result.success("Received: " + message);
          } else {
            result.notImplemented();
          }
        }
      );
  }
}

6. イベントループとライフサイクルの管理

RenewalTopActivity でも、アプリケーションのライフサイクルイベントが管理されます。

RenewalTopActivityのライフサイクル管理

@Override
protected void onPause() {
  super.onPause();
  // Flutterエンジンに一時停止イベントを通知
  getFlutterEngine().getLifecycleChannel().appIsInactive();
}

@Override
protected void onResume() {
  super.onResume();
  // Flutterエンジンに再開イベントを通知
  getFlutterEngine().getLifecycleChannel().appIsResumed();
}

Dart側のライフサイクル管理

class RenewalTopPage extends StatefulWidget {
  
  _RenewalTopPageState createState() => _RenewalTopPageState();
}

class _RenewalTopPageState extends State<RenewalTopPage> with WidgetsBindingObserver {
  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.paused) {
      // アプリが一時停止されたときの処理
    } else if (state == AppLifecycleState.resumed) {
      // アプリが再開されたときの処理
    }
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Renewal Top Page'),
      ),
      body: Center(
        child: Text('Welcome to the Renewal Top Page'),
      ),
    );
  }
}

以上が、RenewalTopActivity の役割とその具体的な実装方法についての詳細です。このアクティビティは、アプリの重要な更新や通知をユーザーに提供するために利用されます。

s16as16a

Androidアプリのソフトウェア構成説明資料

概要

本アプリケーションは、会社の従業員の情報を管理・表示するためのAndroidアプリです。アプリは2つのActivity(FIRSTとSECOND)で構成されており、以下のような機能を提供します。

  1. 従業員の一覧表示(FIRST Activity)
  2. 選択された従業員の詳細情報表示(SECOND Activity)

アプリには2つの起動方法があり、それぞれに応じて表示する内容が異なります。

Activityの構成

1. FIRST Activity

  • 役割: 従業員の一覧を表示する。
  • 起動方法: アプリの通常の起動方法で最初に表示されるActivity。
  • 機能:
    • データベースから従業員のリストを取得し、RecyclerViewまたはListViewで表示する。
    • 従業員を選択すると、選択された従業員のIDをSECOND Activityに渡して起動する。

2. SECOND Activity

  • 役割: FIRST Activityで選択された従業員の詳細情報を表示する。
  • 起動方法: FIRST Activityから選択された従業員のIDを受け取って起動される。
  • 機能:
    • 受け取った従業員IDをもとにデータベースから詳細情報を取得し、画面に表示する。

起動方法

起動方法①: 通常の起動

  • フロー:
    1. FIRST Activityが起動する。
    2. FIRST Activityがデータベースから従業員のリストを取得し、一覧を表示する。
    3. ユーザーが従業員を選択すると、SECOND Activityが起動し、選択された従業員の詳細情報を表示する。

起動方法②: 従業員IDを指定して起動

  • フロー:
    1. FIRST Activityが従業員IDを含むScheme(Intent)を受け取って起動する。
    2. FIRST Activityは従業員の一覧を表示せず、プレースホルダ(読み込み中の表示)を表示する。
    3. FIRST Activityがデータベースから指定されたIDの従業員の詳細情報を取得する。
    4. データ取得が完了したら、SECOND Activityを起動し、従業員の詳細情報を表示する。

フローチャート

起動方法①: 通常の起動フロー

  1. アプリ起動
  2. FIRST Activity起動
  3. データベースから従業員リスト取得
  4. 従業員リスト表示
  5. ユーザーが従業員を選択
  6. SECOND Activity起動
  7. 従業員詳細情報表示

起動方法②: 従業員IDを指定して起動

  1. アプリ起動(従業員IDを含むScheme付きIntent)
  2. FIRST Activity起動
  3. プレースホルダ表示(読み込み中)
  4. データベースから指定IDの従業員詳細情報取得
  5. SECOND Activity起動
  6. 従業員詳細情報表示

実装例

FIRST Activity

public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_first);

        // Scheme付きIntentのチェック
        Intent intent = getIntent();
        if (intent != null && intent.getData() != null) {
            String employeeId = intent.getData().getLastPathSegment();
            showEmployeeDetails(employeeId);
        } else {
            showEmployeeList();
        }
    }

    private void showEmployeeList() {
        // データベースから従業員リストを取得し、表示する処理
    }

    private void showEmployeeDetails(String employeeId) {
        // プレースホルダを表示しながらデータを取得する処理
        // データ取得後にSECOND Activityを起動する処理
        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra("EMPLOYEE_ID", employeeId);
        startActivity(intent);
    }
}

SECOND Activity

public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        Intent intent = getIntent();
        String employeeId = intent.getStringExtra("EMPLOYEE_ID");

        // データベースから従業員の詳細情報を取得し、表示する処理
    }
}

まとめ

本アプリは、従業員の一覧表示と詳細情報表示を2つのActivity(FIRSTとSECOND)で行い、2つの起動方法(通常起動と従業員ID指定起動)に対応しています。FIRST Activityは起動方法に応じて表示内容を切り替え、SECOND Activityは従業員の詳細情報を表示します。

s16as16a

CameraXを使ってAndroidアプリのカメラプレビューを設定する基本的な方法を説明します。ここでは、CameraXの Preview 機能を使ってプレビューを開始し、プレビュー表示の設定を行う例を示します。

  1. CameraXの依存関係を追加

build.gradleファイルにCameraXの依存関係を追加します。

dependencies {
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
}

  1. CameraXのセットアップ

次に、CameraX のプレビュー機能を設定し、表示するコードの例を示します。

XMLレイアウトファイル

カメラプレビューを表示するための PreviewView をレイアウトに追加します。


<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

ActivityでのCameraX設定

MainActivity.kt で PreviewView を使ってプレビューの設定を行います。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

    // カメラの初期化
    startCamera()
}

private fun startCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        // プレビュー用のビルダーを作成
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }

        // カメラ選択 (今回は背面カメラ)
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // カメラプロバイダにバインドされているカメラを解除
            cameraProvider.unbindAll()

            // カメラをバインドしてプレビューを開始
            cameraProvider.bindToLifecycle(this, cameraSelector, preview)

        } catch (exc: Exception) {
            // エラーハンドリング
            exc.printStackTrace()
        }

    }, ContextCompat.getMainExecutor(this))
}

}

コードの説明

ProcessCameraProviderを使って、アプリのライフサイクルにカメラをバインドし、管理します。

Preview.Builderでプレビューのビルダーを作成し、setSurfaceProviderでPreviewViewをプレビューの表示先に設定します。

CameraSelector.DEFAULT_BACK_CAMERAを指定することで背面カメラを使用するように設定しています。

cameraProvider.bindToLifecycleで、Previewをカメラにバインドし、プレビュー表示が開始されます。

  1. 実行

この設定で、アプリ起動時に PreviewView にカメラプレビューが表示されるようになります。

以上がCameraXを使ったプレビュー設定の基本的な実装例です。

s16as16a

CameraXを使ってAndroidアプリのカメラプレビューを設定する基本的な方法を説明します。ここでは、CameraXの Preview 機能を使ってプレビューを開始し、プレビュー表示の設定を行う例を示します。

  1. CameraXの依存関係を追加

build.gradleファイルにCameraXの依存関係を追加します。

dependencies {
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
}

  1. CameraXのセットアップ

次に、CameraX のプレビュー機能を設定し、表示するコードの例を示します。

XMLレイアウトファイル

カメラプレビューを表示するための PreviewView をレイアウトに追加します。


<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

ActivityでのCameraX設定

MainActivity.kt で PreviewView を使ってプレビューの設定を行います。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

    // カメラの初期化
    startCamera()
}

private fun startCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        // プレビュー用のビルダーを作成
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }

        // カメラ選択 (今回は背面カメラ)
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // カメラプロバイダにバインドされているカメラを解除
            cameraProvider.unbindAll()

            // カメラをバインドしてプレビューを開始
            cameraProvider.bindToLifecycle(this, cameraSelector, preview)

        } catch (exc: Exception) {
            // エラーハンドリング
            exc.printStackTrace()
        }

    }, ContextCompat.getMainExecutor(this))
}

}

コードの説明

ProcessCameraProviderを使って、アプリのライフサイクルにカメラをバインドし、管理します。

Preview.Builderでプレビューのビルダーを作成し、setSurfaceProviderでPreviewViewをプレビューの表示先に設定します。

CameraSelector.DEFAULT_BACK_CAMERAを指定することで背面カメラを使用するように設定しています。

cameraProvider.bindToLifecycleで、Previewをカメラにバインドし、プレビュー表示が開始されます。

  1. 実行

この設定で、アプリ起動時に PreviewView にカメラプレビューが表示されるようになります。

以上がCameraXを使ったプレビュー設定の基本的な実装例です。

s16as16a

CameraXを使ってAndroidアプリで画像解析を行う方法を説明します。以下は、CameraXの ImageAnalysis を利用して画像解析を設定する具体的な例です。

  1. 依存関係の追加

build.gradle に CameraX の依存関係を追加します。プレビュー時と同様です。

dependencies {
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
}

  1. 画像解析の設定

画像解析のために、ImageAnalysis を使ってカメラのフレームを処理します。

XMLレイアウトファイル

カメラプレビューと同様に、PreviewView を使ってプレビューを表示します。


<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

ActivityでのCameraX設定

MainActivity.ktで ImageAnalysis の設定と、フレームごとに画像を処理するコードを追加します。

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {

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

    // カメラの初期化
    startCamera()
}

private fun startCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        val cameraProvider = cameraProviderFuture.get()

        // プレビュー用のビルダーを作成
        val preview = Preview.Builder()
            .build()
            .also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }

        // 画像解析用のビルダーを作成
        val imageAnalyzer = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build()
            .also {
                it.setAnalyzer(Executors.newSingleThreadExecutor(), MyImageAnalyzer())
            }

        // カメラ選択 (背面カメラ)
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // カメラプロバイダにバインドされているカメラを解除
            cameraProvider.unbindAll()

            // カメラをバインドしてプレビューと画像解析を開始
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)

        } catch (exc: Exception) {
            // エラーハンドリング
            exc.printStackTrace()
        }

    }, ContextCompat.getMainExecutor(this))
}

// 画像解析のロジック
private class MyImageAnalyzer : ImageAnalysis.Analyzer {
    override fun analyze(imageProxy: ImageProxy) {
        // 画像解析の処理 (ここでは例としてログ出力)
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        Log.d("ImageAnalysis", "Image rotation: $rotationDegrees")

        // 解析後にimageProxyを閉じてリソースを解放
        imageProxy.close()
    }
}

}

コードの説明

Preview:プレビュー表示のために設定。

ImageAnalysis:画像解析用に設定。

setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST):最新のフレームのみを解析する設定で、過去のフレームは破棄されます。

setAnalyzer:解析のための Analyzer クラスを指定します。ここでは MyImageAnalyzer クラスを作成し、そこで解析処理を実装します。

MyImageAnalyzer:画像解析ロジックを持つクラス。

analyze メソッド内で画像の回転情報や他の解析処理が行えます。解析が終わったら imageProxy.close() を呼んでリソースを解放します。

  1. 実行

この設定で、カメラのプレビューを表示しながら、フレームごとに画像解析を行うことができます。MyImageAnalyzer内で任意の画像解析を行い、結果を活用できるように設定することが可能です。

以上が、CameraXを使った画像解析設定の基本的な実装例です。

s16as16a

CameraXを使ってAndroidアプリで画像解析を行う設定方法を、具体的な例と共に説明します。CameraXの ImageAnalysis 機能を利用することで、カメラからのフレームをリアルタイムで解析することができます。

  1. CameraXの依存関係を追加

まず、build.gradle ファイルに CameraX の依存関係を追加します。

dependencies {
def camerax_version = "1.1.0"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
}

  1. レイアウトファイルの設定

次に、プレビューを表示するための PreviewView を XML レイアウトファイルに追加します。


<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

  1. ImageAnalysis の設定

MainActivity.kt に CameraX を使って画像解析を行う設定を行います。

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.Executors

class MainActivity : AppCompatActivity() {

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

    // カメラの初期化
    startCamera()
}

private fun startCamera() {
    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener({
        // CameraProviderを取得
        val cameraProvider = cameraProviderFuture.get()

        // プレビュー用のビルダーを作成
        val preview = Preview.Builder()
            .build()
            .also {
                // PreviewViewにプレビューを表示
                it.setSurfaceProvider(previewView.surfaceProvider)
            }

        // 画像解析用の設定
        val imageAnalyzer = ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)  // 最新フレームのみを保持
            .build()
            .also {
                it.setAnalyzer(Executors.newSingleThreadExecutor(), MyImageAnalyzer())
            }

        // 背面カメラを選択
        val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

        try {
            // 既存のバインディングを解除
            cameraProvider.unbindAll()

            // プレビューと画像解析をカメラのライフサイクルにバインド
            cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalyzer)

        } catch (exc: Exception) {
            // エラーハンドリング
            exc.printStackTrace()
        }

    }, ContextCompat.getMainExecutor(this))
}

// 画像解析のロジック
private class MyImageAnalyzer : ImageAnalysis.Analyzer {
    override fun analyze(imageProxy: ImageProxy) {
        // 画像解析の処理 (ここではログに回転角度を出力)
        val rotationDegrees = imageProxy.imageInfo.rotationDegrees
        Log.d("ImageAnalysis", "Image rotation: $rotationDegrees")

        // 解析が終わったらimageProxyを閉じてリソースを解放
        imageProxy.close()
    }
}

}

  1. コードの説明

CameraX設定の主要部分

ProcessCameraProvider
ProcessCameraProvider は、カメラのライフサイクルを管理し、プレビューや画像解析機能をカメラにバインドします。

Preview.Builder
カメラのプレビューを作成し、PreviewView に表示します。setSurfaceProvider() を使って PreviewView にカメラ映像を表示します。

ImageAnalysis.Builder
ImageAnalysis クラスを使用して、カメラからフレームを取得し解析します。

setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) は解析処理が遅れた場合でも、最新のフレームだけを解析するようにします。

setAnalyzer
解析処理を行うための Analyzer クラスを指定します。ここでは MyImageAnalyzer を使って、画像解析を行っています。

MyImageAnalyzer クラス

analyze(imageProxy: ImageProxy)
このメソッドでフレームごとに画像解析を行います。例では imageInfo.rotationDegrees をログに出力しています。 解析後には、必ず imageProxy.close() を呼び出してリソースを解放します。

  1. 実行

これで、アプリを実行するとカメラのプレビューが表示され、フレームごとに画像解析が行われます。MyImageAnalyzer クラスで任意の画像解析ロジックを追加することで、リアルタイムで画像解析ができるようになります。

まとめ

Preview を使ってプレビューを表示。

ImageAnalysis を利用してカメラフレームをリアルタイムで解析。

Analyzer クラスで任意の解析ロジックを実装。

この設定で、カメラプレビューを表示しながらフレームごとの画像解析が可能になります。