[Delhi11 BUG] パス情報がない OpenPictureDialog1.FileName
編集:2023.10.23
[Delhi11 BUG] パス情報がない OpenPictureDialog1.FileName
再現方法
Windows11でAndoroidデバイスを接続して、AndoroidデバイスのSDカード内のファイルを選択する
OpenPictureDialog1は利用できないフォルダも表示されるので選択後に拒否する
LFileName := OpenPictureDialog1.Files[0];テキストなどを編集を選択すると
if Pos('\',LFileName) = 0 then
begin
ShowMessage('ドライブ情報が取得できないため操作できません。');
Exit;
end;
AppData\Local\Microsoft\Windows\INetCache\IE\**
に飛ばされる
エクスプローラーに表示されているドライブ情報自体が実在しないフェイクみたい
アンドロイドアプリ作ったことないので急遽Windowsアプリでしのごうと数時間で作ったのに
アクセスできない つかえなーい!!
いちいちAndroidからSDカード抜かないと操作できないので、どうしよう
・却下:ネットワークドライブに移動。 一番無難かもしれないが勝手にパケ食いされると困る。
・(めんどう):Andoridアプリ作る
とりあえず、電源切ってSDカードを移動して対応しよう。
昨日の夜 頑張って作ったのにショックだ!!
根本的解決は
頑張って Andoridアプリ作るしかなさそう
ヘッダー/フッター
保存先選択
Android64ビット
スタイル Android
実行
[PAClient エラー] エラー: E7684 プログラムが見つかりません、'Z:\bin\java.exe'
これか!!
RAD Studio11でFireMonkeyアプリをビルドし、Androidデバイスへデプロイすると、”エラー: E2308 プログラムが見つかりません”というエラーが発生する
RAD Studio 11.3のインストールメニューの[追加オプション]の"Eclipse Open JDK 11"を必ず選択してください。
"Eclipse Open JDK 11"をインストール
Delphi再起動
実行
エラー
---------------------------
Android アプリケーションを実行したりデバッグするには、Android デバイスを PC に接続するか、Android エミュレータを作成する必要があります。
詳細は、下の [ヘルプ] ボタンをクリックしてください。
---------------------------
OK ヘルプ(H)
---------------------------
[Window Title]
エラー
[Content]
RAD Studio のヘルプがインストールされていません。RAD Studio のドキュメントを再インストールしてください。
エラーが変わった!!
Universal adb driverをインストール
エミュレーターが表示されないので実機を接続
C:\Users\Public\Documents\Embarcadero\Studio\22.0\CatalogRepository\AndroidSDK-2525-22.0.48361.3236\platform-tools
エクスプローラーにピン止め!
ついでに PATHにも追加しておこう
接続したAndroid端末で承認
adb shell ls
adb kill-server
adb shell ls
Delphi11の Android64ビット 右横の更新マークをクリック!!
接続したデバイスを選択できるようになりました!!
実行♪
端末にアプリが表示されました!!!
やったね!
ともったのもつかの間。
サンプル実行。
TakePhotoFromLibraryAction1
TBitmapで帰ってくるため
・写真が一枚しか選択できない
・ファイルパスが取得できない
使えない ばかじゃないの!!!
さすが Delphi 最初のころのDelphiバージョンはコピペで超簡単がが売りだったはずだが DIY志向が強くなっている!!
https://developer.android.com/about/versions/13/features/photopicker?hl=ja#java
に書いてあるような複数選択ができません!!
Delphiだと情報が少なすぎて時間の無駄!!
選択した値を取得する方法がわからない
Linux起動して、Androidスタジオで作り直します!!
と思ったけど、
検索結果をつぎはぎして、組みなおしたら 取得できました!!
コンテンツパスはわかるがリアルパスのフォルダ情報が取得できないという問題が発生した。Androidの仕様だから無理かな。
%2F は \ なのでいいとして
作るアプリはフォルダによって動作がかわるアプリなのでちょっと困る。
解析できないパスは使えません!!とか こっちに保存しますよ!
と表示すればとりあえず解決だね. SDカード抜き差しして使うよりははるかにまし。
例外が発生するとアプリが落ちるので try .. except で保護したほうがいい
const
PHOTO_PICKER_REQUEST_CODE = 12345; // 競合しないように適当に大きい数字を割り当てる
procedure THeaderFooterForm.Button1Click(Sender: TObject);
var
Intent: JIntent; // Androidapi.JNI.GraphicsContentViewText
begin
FMessageSubscriptionID := TMessageManager.DefaultManager.SubscribeToMessage(
TMessageResultNotification, HandleActivityMessage);
Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_PICK);
intent.setType(StringToJString('image/*'));
intent.setAction(TjIntent.JavaClass.ACTION_GET_CONTENT);
Intent.putExtra(TJIntent.JavaClass.EXTRA_ALLOW_MULTIPLE,true);
TAndroidHelper.Activity.startActivityForResult(Intent, PHOTO_PICKER_REQUEST_CODE);
// ActivityResultLauncher.launch(intent);
end;
procedure THeaderFooterForm.onActivityResult(requestCode, resultCode: Integer; data: JIntent);
var
i: integer;
procedure SetBMP(AURI: Jnet_Uri); // Jnet_Uri - Androidapi.JNI.Net
var
fileDescriptor : JFileDescriptor;
parcelFileDescriptor : JParcelFileDescriptor;
LJBitmap : JBitmap;
LBitmapSurface, LBitmapSurface2: TBitmapSurface; // FMX.Helpers.Android
r: Double;
begin
parcelFileDescriptor := TAndroidHelper.Activity.getContentResolver.
openFileDescriptor(AURI,StringToJString('r'));
fileDescriptor := parcelFileDescriptor.getFileDescriptor;
LJBitmap := TJBitmapFactory.JavaClass.decodeFileDescriptor(fileDescriptor);
LBitmapSurface := TBitmapSurface.Create;
try
// TBitmap.Assign(LJBitmap) は対応していない
// TBitmapSurface に変換する
JBitmapToSurface(LJBitmap, LBitmapSurface);
LBitmapSurface2 := TBitmapSurface.Create;
try
// Image1.Bitmap.Assign(LBitmapSurface); // 遅延しすぎで実用性なし
// 1MBくらいなら問題ない。 2MBとかになると10秒くらいかかる / 1ドットコピーしてるのかも
// 素のままだとBitmap.Assignが遅いので縮小イメージを作る
// 同じサイズだと荒れるのでImage1のサイズの2倍にしておく。
if LBitmapSurface.Width >= LBitmapSurface.Height then
r := Math.Max(500, 2 * Image1.Width) / LBitmapSurface.Width
else
r := Math.Max(500, 2 * Image1.Height) / LBitmapSurface.Height;
LBitmapSurface2.StretchFrom(LBitmapSurface,
Trunc(LBitmapSurface.Width * r),
Trunc(LBitmapSurface.Height * r));
Image1.Bitmap.Assign(LBitmapSurface2);
finally
LBitmapSurface2.Free;
end;
finally
LBitmapSurface.Free;
end;
end;
procedure DoFiles(AURI: Jnet_Uri);
begin
//TDialogService.ShowMessage(JStringToString(AURI.toString()));
//TDialogService.ShowMessage(JStringToString(AURI.getEncodedPath()));
Label1.Text := JStringToString(AURI.toString());
FFiles.Add(JStringToString(AURI.toString()));
if FFiles.Count = 1 then
SetBMP(AURI);
end;
begin
TMessageManager.DefaultManager.Unsubscribe(TMessageResultNotification, FMessageSubscriptionID);
FMessageSubscriptionID := 0;
if RequestCode = PHOTO_PICKER_REQUEST_CODE then
begin
try
FFiles.Clear;
// 1個選択の場合: data.getClipData() = nil
if data.getClipData() = nil then
begin
DoFiles(data.getData());
end
else
for i := 0 to data.getClipData().getItemCount() - 1 do
begin
DoFiles(data.getClipData().getItemAt(i).getUri());
end;
except on E: Exception do
TDialogService.ShowMessage(E.ToString());
end;
end;
end;
procedure THeaderFooterForm.HandleActivityMessage(const Sender: TObject; const M: TMessage);
begin
if M is TMessageResultNotification then
OnActivityResult(
TMessageResultNotification(M).RequestCode,
TMessageResultNotification(M).ResultCode,
TMessageResultNotification(M).Value);
end;
URIから SDカードのアドレスを返すAPIがないようだ.
野良.javaを組み込んで呼び出してもやはり無理。
Androidくそすぎる
・fstabなどからカードを探す
・FolderViewを自分で作る
をしないと実パスを得ることができないっぽい
ほとんど移植したので(保存処理だけ残ってます)。
親パス名を取得してパス付で保存するだけなのに超絶簡単なことができない。
選択ファイルを自動加工して子フォルダを作って数個自動保存するだけなのに・・・
わかったこと
Delphiのヘルプの内容が古すぎて使えない
・インストールされているのに外部からダウンロードするように記述されている。
"C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\converters\java2op\java2op"
・サンプルの構文やネームスペースが違う
・その他いろいろ
ファイル操作
・1個ずつ選択してURIでの操作が基本みたい
https://developer.android.com/training/data-storage/shared/documents-files?hl=ja
・どひゃーっとリアルパスでSDカードに書き込むような仕様はないみたい。
細かいことはjavaで書いてDelphiで呼び出したほうが簡単。
なので、javaからjarファイルを作る方法と
Delphiプロジェクトに取り込みをする方法を調べたほうが快適。
java2opは J とか TJとか 変数名が いい加減なので
ファイルを全選択して、リファクタリングで、変数を選択して名称の一括修正が必要
JStringToString関数は多用するかも。
TDialogService.ShowMessage(JStringToString(TJDocumentsContract.JavaClass.getDocumentId(AURI)));
TJEnvironment.JavaClass.getExternalStorageDirectory()
TJDocumentsContract.JavaClass.getDocumentId(AURI) : split(":")
場所 | URI | リアルパス |
内部ストレージ | content://com.android.externalstorage.documents/document/primary%3A | getExternalStorageDirectory |
SDカード | content://com.android.externalstorage.documents/document/[数値-数値]%3A | 不明 |
AUriText := JStringToString(AURI.toString());
使う端末は固定で、TJDocumentsContract.JavaClass.getDocumentId(AURI)で デコードされたパス情報は取得できるので
あとはSDカードの物理アドレスだけ。 これも1個の端末しか使わないのでハードコートすれば解決。
function JUriToRealPath(AURI: Jnet_Uri): string;
var
LJDocId: JString;
LDocId, LDocType, LPath, LUriText: String;
begin
// 解決できないときは空を返します。
Result := '';
// https://developer.android.com/training/data-storage/shared/documents-files?hl=ja
LUriText := JStringToString(AURI.toString());
if (Pos('content://com.android.externalstorage.documents/document/', LUriText) = 1) then
begin
LJDocId := TJDocumentsContract.JavaClass.getDocumentId(AURI);
LDocId := JStringToString(LJDocId);
LDocType := Copy(LDocId, 1, Pos(':', LDocId) - 1);
LPath := Copy(LDocId, Pos(':', LDocId) + 1, Length(LDocId));
if LDocType = 'primary' then
Result := JStringToString(TJEnvironment.JavaClass.getExternalStorageDirectory().toString()) + '/' + LPath
else if DirectoryExists('/storage/' + LDocType, False) then
Result := '/storage/' + LDocType + '/' + LPath
else if DirectoryExists('/mnt/' + LDocType, False) then
Result := '/mnt/' + LDocType + '/' + LPath;
end;
end;
/fstabで情報を探るのも一手だが、使う端末は固定なので、その必要はない!!
adb shell "mount" でみるとか?
あとは、保存時に拒否されるかどうかの問題。
TDialogService.ShowMessage('LBaseSaveDir: ' + LBaseSaveDir);
FFiles.SaveToFile(LBaseSaveDir + '/テスト書き込み.txt');
exit;
実行!
を作成できません。Operaton not permitted
MkDirは普通に動作するみたい。なぞだ!
LJFileOutputStream: JFileOutputStream;
LJFile := TJFile.JavaClass.init(StringToJString('/sdcard/Download/test-debug2.txt'));
LJFile.createNewFile();
LJFileOutputStream := TJFileOutputStream.JavaClass.init(LJFile);
LJFileOutputStream.write(TAndroidHelper.TBytesToTJavaArray(TEncoding.UTF8.GetBytes('OK!')));
LJFileOutputStream.close();
保存できました!!
どうやらDelphiの関数で直接、読み込み/書き込みすると 拒否られるようです。
javaで操作しよう!
ガーン
内部のユーザー領域には書き込めるが、差し込んだ SDカードには書き込めない!!
内部に移動してもいいけど 6GBくらいあるから本体をいじめたくない。
外部SDカードなら壊れても1000円だし。
検索:
sdcard write android delphi
Android 11(API レベル 30)をターゲットとし、Android 11 で追加された MANAGE_EXTERNAL_STORAGE 権限
MANAGE_EXTERNAL_STORAGE
これか!! Android10と思っていたら端末情報みたら Android11だった! いつのまにアップグレードしたのだろう?
オプション 使用する権限 シグニチャ
外部ストレージ管理
アプリのプロパティにすべてのファイルへのアクセスが増えた。
でも実行すると
ACTION_OPEN_DOCCUMENT or related APIs.
メディア管理やドキュメントの管理もチェック。
FPermissionManageExternalStorageをいれたらハングアップしたけど、
強制停止して、もう一度アプリの権限確認して、起動したら
外部SDカードに書き込み出来るようになりました!!
やったー!! アプリ完成!! 移植成功♪
よくわかりませんが、目的の動作をしてくれるようになったのでヨシとします!
あと androidでBitmap.create後にサイズ変更すると、汚染されたメモリ利用するようで、メモリに残った残像がでてくるので
きちんと 最初に clean(色指定)で全部ピクセル消去しないと情報漏洩しますよ
ファイラー以外のほかのアプリの画像一覧が表示されない
MediaStoreインデクスに登録しないといけないらしい
1. MediaStoreインデクスに登録( ContentResolver.insertで登録 )して ファイルのJUriを取得する
LJContentValues := TJContentValues.JavaClass.init;
LJContentValues.put(TJMediaStore_MediaColumns.JavaClass.DISPLAY_NAME,
StringToJString(ExtractFileName(LContentFilename)));
LJContentValues.put(TJMediaStore_MediaColumns.JavaClass.MIME_TYPE,
StringToJString('image/jpeg'));
LJContentValues.put(TJMediaStore_MediaColumns.JavaClass.DATE_ADDED,
TJDouble.JavaClass.init(TJClock.JavaClass.systemDefaultZone.millis));
if TJBuild_VERSION.JavaClass.SDK_INT >= 29 then
begin // (Android10 API 29)
LRelativePath := ''; // 子フォルダに入れる場合は、ディレクトリの相対パス名をいれる、 (画像ファイル名は取り除く)
LJContentValues.put(StringToJString('relative_path'), StringToJString(LRelativePath));
LJContentValues.put(StringToJString('is_pending'), TJBoolean.JavaClass.TRUE);
end;
// DATA: ファイルのパスなので指定したい場合は必ず必要 : パス形式 - Unix形式[/fullpath]
LJContentValues.put(TJMediaStore_MediaColumns.JavaClass.DATA, // _data
StringToJString(LContentFilename));
if (TJBuild_VERSION.JavaClass.SDK_INT >= 29) then // (Android10 API 29)
// MediaStore.VOLUME_EXTERNAL_PRIMARY : Constant Value: "external_primary"
LVolume := 'external_primary'
else
LVolume := 'external';
// LVolume := 'xxxx-xxxx'; // SDカードの場合は数値を書いて有効にする
LContentUri := TJImages_Media.JavaClass.getContentUri(StringToJString(LVolume));
//LContentUri := TJMyAndroidUtils.JavaClass.getContentUriFromFilename(
// TAndroidHelper.Context, StringToJString(LContentFilename));
// MediaStoreインデクスに登録
LJNewURI := TAndroidHelper.ContentResolver.insert(LContentUri, LJContentValues);
2. (1) の uriを使って ファイルハンドルを作り、画像を書き込む。
try
LJOutputStream.write(TAndroidHelper.TBytesToTJavaArray(LStream.Bytes));
finally
LJOutputStream.close();
end;
3. (Android10 API 29)以上の場合は仮登録を確定する
procedure updateMetadata();
var
LJContentValues: JContentValues;
ret: Integer;
begin
if (TJBuild_VERSION.JavaClass.SDK_INT >= 29) And (LJNewURI <> nil) then
begin
LJContentValues := TJContentValues.JavaClass.init;
LJContentValues.put(StringToJString('is_pending'), TJBoolean.JavaClass.False);
ret := TAndroidHelper.ContentResolver.update(LJNewURI, LJContentValues, nil, nil);
ToastShow(Format('%d: Update Result for is_pending[false]', [ret]));
end;
end;