この記事は Androidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、記事の最後で紹介します)
AI機能によって快適な写真撮影ができるカメラ性能が高いスマホです。
ポイント
これまでREAD_MEDIA_IMAGESやREAD_MEDIA_VIDEO権限の使用を宣言、ユーザ承認を獲得すればスマホ本体にある画像や動画に自由にアクセスできました。
Photo and Video Permissions policyではREAD_MEDIA_IMAGES及び、READ_MEDIA_VIDEO権限の使用を制限します。
このポリシーに準拠するには、Photo Pickerでスマホ本体にある画像や動画を選択する実装に変更する必要があります。
Google Play の写真と動画の権限に関するポリシーの詳細
Photo and Video Permissions policy
READ_MEDIA_IMAGESやREAD_MEDIA_VIDEOはAndroid13で追加された権限です。
この権限はメディアストアを経由して、スマホ本体にある画像や動画にアクセスする際のフィルターの役割を担います。
アプリ自体が登録した画像や動画は権限の有無にかかわらず、自由にアクセスできます。
サンプルでは、Android13以降の場合に外部ストレージにある動画をvideoItemMapに展開しています。
次にメディアストアを経由して動画リストを取得、videoItemMapに展開しています。
Android13以降の場合はvideoItemMapに展開したvideoItemをファイル更新日の降順でソートします。
:
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
try {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
List<File> paths = externalStorageReader.getFiles(2);
for (File file : paths) {
String ext = externalStorageReader.extension(file.getName());
switch (ext) {
case ".mp4":
Uri contentUri = externalStorageReader.getUri(file.getName(), 2);
String jpg = file.getName().replace(".mp4", ".jpg");
Bitmap bitmap = null;
long width;
long height;
long duration;
long updated;
try (MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever()) {
mediaMetadataRetriever.setDataSource(context, contentUri);
width = Long.parseLong(Objects.requireNonNull(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)));
height = Long.parseLong(Objects.requireNonNull(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)));
duration = Long.parseLong(Objects.requireNonNull(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)));
updated = file.lastModified();
}
if (externalStorageReader.setFile(jpg, 2)) {
bitmap = externalStorageReader.ReadFileBitmap();
} else {
FFmpegKit.execute(String.format("-i '%s' -ss 0 -vframes 1 -s %dx%d '%s'", file.getPath(), width, height, context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + jpg));
if (externalStorageReader.setFile(jpg, 2))
bitmap = externalStorageReader.ReadFileBitmap();
}
if (bitmap != null) {
videoItemMap.put(file.getName(), new VideoItem(file.getName().replace(".mp4", ""), duration, contentUri, bitmap, file.getName(), file.getPath(), contentUri, width, height, updated));
}
break;
case ".jpg":
case ".jpeg":
break;
default:
}
}
}
ContentResolver resolver = context.getContentResolver();
Cursor cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null, null, "_ID DESC");
if (cursor != null) {
while (cursor.moveToNext()) {
if (Arrays.asList(mimeList).contains(cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)))) {
String w = cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH) > 0 ? cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH)) : "0";
String h = cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT) > 0 ? cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT)) : "0";
String d = cursor.getColumnIndex(MediaStore.Video.VideoColumns.DURATION) > 0 ? cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DURATION)) : "0";
String o = cursor.getColumnIndex(MediaStore.Video.VideoColumns.ORIENTATION) > 0 ? cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.ORIENTATION)) : "0";
// スクリーンサイズ、再生時間の数値チェック
if (isNumber(w) && isNumber(h) && isNumber(d)) {
// 画面サイズ以下が対象
if ((Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH))) <= mainActivity.WIDTH && Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT))) <= mainActivity.HEIGHT)
|| (Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH))) <= mainActivity.HEIGHT && Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT))) <= mainActivity.WIDTH)) {
Uri contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, Long.parseLong(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))));
Uri baseUri = contentUri;
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DATA));
String source = cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME));
String ext = source.substring(source.lastIndexOf(".")).toLowerCase();
long width = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.WIDTH)));
long height = Long.parseLong(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.HEIGHT)));
long updated = externalStorageReader.lastModified(contentUri);
// ファイル名からスペース削除と拡張子変更
String file = source.toLowerCase().replace(" ", "").replace(" ", "").replace(ext, ".mp4");
String jpg = file.replace(".mp4", ".jpg");
long duration = Long.parseLong(d);
// 拡張子がMP4以外はコーディック
if (!ext.equals(".mp4") && !ext.isEmpty()) {
path = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + file;
if (!externalStorageReader.setFile(file, 2)) {
// mp4ファイル変換
String mts = FFmpegKitConfig.getSafParameterForRead(context, contentUri);
String mp4 = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + file;
FFmpegSession session = FFmpegKit.execute(mainActivity.CODEC ? String.format("-i '%s' -qmin %d -qmax %d -acodec libmp3lame -ab 192000 -ar 48000 -s %dx%d '%s'", mts, mainActivity.QMIN, mainActivity.QMAX, width, height, mp4) :
String.format("-i '%s' -qmin %d -qmax %d -c:v copy -acodec libmp3lame -ab 192000 -ar 48000 -s %dx%d '%s'", mts, mainActivity.QMIN, mainActivity.QMAX, width, height, mp4));
if (ReturnCode.isSuccess(session.getReturnCode())) {
// 変換成功
contentUri = externalStorageReader.getUri(file, 2);
} else {
contentUri = null;
Log.d(TAG, session.getOutput());
Log.d(TAG, String.format("FFmpegKit: %s trace: %s(%s)", session.getState(), session.getFailStackTrace(), session.getReturnCode()));
}
} else {
// エンコード済みのファイルあり
contentUri = externalStorageReader.getUri(file, 2);
}
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(context, contentUri);
// 再生時間を取得
String METADATA_KEY_DURATION = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
duration = METADATA_KEY_DURATION != null ? Long.parseLong(METADATA_KEY_DURATION) : 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
try {
mediaMetadataRetriever.close();
} catch (IOException ex) {
Log.d(TAG, String.format("MediaMetadataRetriever:%s", ex.getMessage()));
}
}
}
width = Long.parseLong(o.equals("0") || o.equals("180") ? w : h);
height = Long.parseLong(o.equals("0") || o.equals("180") ? h : w);
Bitmap bitmap = null;
if (externalStorageReader.setFile(jpg, 2)) {
// サムネイルが存在する
bitmap = externalStorageReader.ReadFileBitmap();
} else {
message = R.string.thumnail; // サムネイルを作成しています…(TOAST)
FFmpegKit.execute(String.format("-i '%s' -ss 0 -vframes 1 -s %dx%d '%s'", path, width, height, context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + File.separator + jpg));
if (externalStorageReader.setFile(jpg, 2))
bitmap = externalStorageReader.ReadFileBitmap();
}
if (bitmap != null) {
videoItemMap.put(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME)),
new VideoItem(cursor.getString(cursor.getColumnIndex(MediaStore.Video.VideoColumns.TITLE)), duration, contentUri, bitmap, source, path, baseUri, width, height, updated));
}
remains.add(jpg);
}
}
}
}
cursor.close();
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
Map<Long, VideoItem> updatedMap = new TreeMap<>(Comparator.reverseOrder());
videoItemMap.forEach((key, value) -> updatedMap.put(value.updated, value));
videoItemMap = new LinkedHashMap<>();
updatedMap.forEach((key, value) -> videoItemMap.put(value.source, value));
}
:
externalStorageReaderは外部ストレージにアクセスするためのクラスのインスタンス変数、videoItemは動画情報のエンティティクラスです。
Android13以降の場合に追加した部分がPhoto and Video Permissions policyで対応した箇所です。
MP4以外の動画はAndroidでは再生できないため、動画ファイルや形式を変換する必要があります。
Photo Picker
Photo and Video Permissions policyに準拠するために、Photo Pickerを使用してスマホ本体にある画像や動画を選択、外部ストレージに動画をコピーします。
サンプルでは、メニュー選択でPhoto Pickerを起動、選択した動画のURLリストを取得しています。
取得した動画のURLリストを外部ストレージにコピーしています。
ActivityResultLauncher<PickVisualMediaRequest> pickMultipleMedia =
registerForActivityResult(new ActivityResultContracts.PickMultipleVisualMedia(), uris -> {
if (!uris.isEmpty()) {
new Thread(new Runnable() {
@Override
public void run() {
handler.post(() -> new ExternalStorageWriter(context).copyMovies(context, uris));
}
}).start();
}
});
:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
:
actionMenuView.setOnMenuItemClickListener(menuItem -> {
itemId = menuItem.getItemId();
Intent intent;
Uri uri;
Bundle bundle = new Bundle();
switch (itemId) {
case 14: // 本体動画を取り込む
pickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
.setMediaType(ActivityResultContracts.PickVisualMedia.VideoOnly.INSTANCE)
.build());
break;
:
ExternalStorageWriterは外部ストレージにアクセスするためのクラスです。
外部ストレージにコピーすることで、videoItemMapに展開できるようになります。
今回は、ここまでです。
Photo and Video Permissions policyに準拠しているAndroidアプリです。
誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にあるコメントか、
こちらから、お願いいたします♪
ポチッとして頂けると、
次のコンテンツを作成する励みになります♪
これからAndroidのアプリ開発やJavaでの開発を始めたい方へ
アプリケーション開発経験がない方や、アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、Android のアプリ開発ができるようになるには、かなりの時間がかかります。
オンラインスクールでの習得を、強くおススメします。
未経験者からシステムエンジニアを目指すのに最適です。まずは無料相談から♪
未経験者からプログラマーを目指すのに最適です。まずは無料カウンセリングから♪
カリキュラムとサポートがしっかりしています。お得なキャンペーンとかいろいろやっています♪
ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適です。まずは無料オンライン相談から♪
参考になったら、💛をポッチとしてね♪
コメント欄