ExplorerやFinderのようなAndroidファイラーを作る ~Delphiソースコード集
0.完成スナップショット
以下のようなアプリケーションを作成します。※Android11(API Level30)以降は「ダウンロード」ディレクトリへの アクセス権限が無くなったようです
1.フォームの作成
FMXフレームワークで以下のようにフォームにコンポーネントを配置します。プロジェクトを「・・・\Project\MamExplorer\MamExplorer.dproj」に保存したとします。
2.AndroidManifest.template.xmlの編集
AndroidManifest.template.xmlを編集して既存の </application> の行の手前(上)に以下を追加する。<provider android:name="android.support.v4.content.FileProvider" android:authorities="%package%.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider>
3.provider_paths.xmlファイルの作成
プロジェクトフォルダ内(・・・\Project\MamExplorer\)に provider_paths.xml ファイルを作成し以下の内容で保存する<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="external_files" path="."/> </paths>
4.「provider_paths.xml」ファイルの配置
「プロジェクト」⇒「配置」をクリックし、「ファイルの追加」ボタンをクリックして プロジェクトフォルダ内の「provider_paths.xml」ファイルを選択する。リモートパスを「res\xml\」に設定する。
5.権限設定
「プロジェクト」⇒「オプション」をクリックし、「アプリケーション」⇒「使用する権限」を選択する。・外部ストレージへの読み取り
・外部ストレージへの書き込み
・Request install packages
にチェックを入れてtrueにする。
6.ソースコード
ソースコードの殆どは、権限設定とセキュリティ回避です。unit UMamExplorer; interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.ListView.Types, FMX.ListView.Appearances, FMX.ListView.Adapters.Base, FMX.ListView, FMX.Controls.Presentation, FMX.StdCtrls, System.ImageList, FMX.ImgList, FMX.ListBox ,Androidapi.JNI.JavaTypes ,System.IOUtils,System.Permissions,FMX.Platform; type TF_MamExplorer = class(TForm) Panel1: TPanel; ListView1: TListView; ImageList1: TImageList; StyleBook1: TStyleBook; Panel2: TPanel; Label1: TLabel; CB_RootPath: TComboBox; Panel3: TPanel; LPath: TLabel; Label2: TLabel; Panel4: TPanel; Button1: TButton; procedure FormCreate(Sender: TObject); procedure CB_RootPathChange(Sender: TObject); procedure ListView1ItemClick(const Sender: TObject; const AItem: TListViewItem); procedure Button1Click(Sender: TObject); procedure FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); private { private 宣言 } rPath:array[0..5] of String; sPath:array[0..5] of String; function getDirName(path: string): string; procedure ShowDirFile(path: String); procedure PermissionRequestResult( Sender: TObject; const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>); function AppEvent(iAppEvent: TApplicationEvent; iContext: TObject): Boolean; procedure RequestPermissions(); public { public 宣言 } end; function GetFileSize(const fName: string): Int64; var F_MamExplorer: TF_MamExplorer; implementation {$R *.fmx} uses System.StrUtils ,Androidapi.JNI.GraphicsContentViewText ,AndroidAPI.Helpers ,Androidapi.JNI.Webkit ,Androidapi.JNI.Support ,Androidapi.JNI.Net ,Androidapi.JNI.Os ,FMX.DialogService; function GetFileSize(const fName: string): Int64; //ファイルサイズの取得 var strm: TFileStream; begin strm := TFileStream.Create(fName, fmOpenRead or fmShareDenyNone); try Result := strm.Size; finally strm.DisposeOf; end; end; function TF_MamExplorer.AppEvent(iAppEvent: TApplicationEvent; iContext: TObject): Boolean; begin Result:=False; case iAppEvent of TApplicationEvent.BecameActive: begin //focus取得時 end; TApplicationEvent.WillBecomeInactive: begin //focus喪失時 end; end; end; procedure TF_MamExplorer.Button1Click(Sender: TObject); //上位ディレクトリへ ボタンを押したときの処理 var path:string; begin if LPath.Text=TPath.DirectorySeparatorChar then exit; path:=getDirName(LPath.Text); path:=LeftStr(LPath.Text,Length(LPath.Text)-Length(Path)-1); if path='' then path:=TPath.DirectorySeparatorChar; ShowDirFile(path); end; procedure TF_MamExplorer.CB_RootPathChange(Sender: TObject); begin ShowDirFile(TPath.DirectorySeparatorChar); end; procedure TF_MamExplorer.FormCreate(Sender: TObject); var i:integer; APPEventService:IFMXApplicationEventService; begin rPath[0]:=System.IOUtils.TPath.GetSharedDownloadsPath; rPath[1]:=System.IOUtils.TPath.GetSharedMoviesPath; rPath[2]:=System.IOUtils.TPath.GetSharedMusicPath; rPath[3]:=System.IOUtils.TPath.GetSharedCameraPath; rPath[4]:=System.IOUtils.TPath.GetSharedPicturesPath; rPath[5]:=System.IOUtils.TPath.GetSharedRingtonesPath; sPath[0]:='ダウンロード'; sPath[1]:='動画'; sPath[2]:='音楽'; sPath[3]:='カメラ'; sPath[4]:='画像'; sPath[5]:='着信音'; //ルートディレクトリ選択用コンボボックスの設定 CB_RootPath.Items.Clear; for i := 0 to Length(sPath)-1 do CB_RootPath.Items.Add(sPath[i]); CB_RootPath.ItemIndex:=0; if TPlatformServices.Current.SupportsPlatformService( IFMXApplicationEventService) then begin try APPEventService:=IFMXApplicationEventService( TPlatformServices.Current.GetPlatformService( IFMXApplicationEventService ) ); except APPEventService:=nil; end; end; if (APPEventService<>nil) then APPEventService.SetApplicationEventHandler(AppEvent); RequestPermissions(); end; procedure TF_MamExplorer.FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char; Shift: TShiftState); begin if Key = vkHardwareBack then Key := 0; end; function TF_MamExplorer.getDirName(path: string): string; var i:integer; begin path:=ReverseString(path); i:=Pos(TPath.DirectorySeparatorChar, path); if i>0 then path:=LeftStr(path,i-1) else path:=''; result:=ReverseString(path); end; procedure TF_MamExplorer.ListView1ItemClick(const Sender: TObject; const AItem: TListViewItem); var intent:JIntent; ext:String; //拡張子 mime:JString; //MIMEタイプ fullpath:String; javafile:JFile; Auth:JString; Provider:Jnet_Uri; begin //ファイルが選択された場合 if AItem.ImageIndex=1 then begin //フルパスの取得 if LPath.Text=TPath.DirectorySeparatorChar then fullpath:=rPath[CB_RootPath.ItemIndex]+LPath.Text+AItem.Text else fullpath:=rPath[CB_RootPath.ItemIndex]+LPath.Text+ TPath.DirectorySeparatorChar+AItem.Text; Intent:=TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_VIEW); //拡張子を取得する ext:=LowerCase(StringReplace(TPath.GetExtension(AItem.Text),'.','',[])); //MIMEタイプを取得する mime:=TJMimeTypeMap.JavaClass.getSingleton.getMimeTypeFromExtension( StringToJString(ext) ); if mime=nil then mime := StringToJString('*/*'); //インテントにフラグを設定する if ext='apk' then intent.addFlags( TJIntent.JavaClass.FLAG_GRANT_WRITE_URI_PERMISSION or TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION or TJIntent.JavaClass.FLAG_ACTIVITY_NEW_TASK ) else intent.addFlags( TJIntent.JavaClass.FLAG_GRANT_WRITE_URI_PERMISSION or TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION ); javafile:=TJFile.JavaClass.init(StringToJString(fullpath)); Auth := StringToJString( JStringToString( TAndroidHelper.Context.getApplicationContext.getPackageName ) + '.fileprovider' ); Provider:=TJFileProvider.JavaClass.getUriForFile( TAndroidHelper.Context, Auth, javafile ); intent.setDataAndType(Provider, mime); TAndroidHelper.Activity.startActivity(Intent); end else begin if LPath.Text=TPath.DirectorySeparatorChar then LPath.Text:=AItem.Text else LPath.Text:=LPath.Text+AItem.Text; ShowDirFile(LPath.Text); end; end; procedure TF_MamExplorer.PermissionRequestResult(Sender: TObject; const APermissions: TArray<string>; const AGrantResults: TArray<TPermissionStatus>); begin //ストレージ書き込みの権限があるか if (AGrantResults[0]<>TPermissionStatus.Granted) then begin //権限がない場合 //「□今後は表示しない」チェックボックスにチェックが入っているか if (TJActivityCompat.JavaClass.shouldShowRequestPermissionRationale( TAndroidHelper.Activity, TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE)) then begin //「□今後は表示しない」チェックボックスにチェックが入っていない場合 // 非同期でダイアログを表示して説明と許可を要求 TDialogService.MessageDialog( '許可しないとアプリが動作しません。',TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK],TMsgDlgBtn.mbOk,0, procedure (const AResult: TModalResult) begin //2回目以降は「□今後は表示しない」チェックボックスが表示される RequestPermissions(); end); end else begin //「□今後は表示しない」チェックボックスにチェックが入っている TDialogService.MessageDialog( '権限が無いため終了します。「設定⇒アプリと通知」から権限を設定してください。', TMsgDlgType.mtError,[TMsgDlgBtn.mbOK],TMsgDlgBtn.mbOK,0, procedure(const AResult:TModalResult) begin Application.Terminate; end ); end; end else begin //パスの初期化 ShowDirFile(TPath.DirectorySeparatorChar); end; end; procedure TF_MamExplorer.RequestPermissions; var FPmsWriteExternalStorage:string; begin FPmsWriteExternalStorage := JStringToString(TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE); PermissionsService.RequestPermissions( [FPmsWriteExternalStorage],PermissionRequestResult); end; procedure TF_MamExplorer.ShowDirFile(path: String); var pathList,filesList:TStringDynArray; strPath,strFile:string; lvi:TListViewItem; RootPath:String; DateTime : TDateTime; Size:Int64; begin ListView1.Items.Clear; RootPath:=rPath[CB_RootPath.ItemIndex]; LPath.Text:=path; //ディレクトリの探索 try pathList:=TDirectory.GetDirectories(RootPath+path); for strPath in pathList do begin lvi:=ListView1.Items.add; lvi.Text:=TPath.DirectorySeparatorChar+getDirName(strPath); lvi.ImageIndex:=0; end; except end; //ファイルの探索 try filesList:=TDirectory.GetFiles(RootPath+path,'*'); for strFile in filesList do begin lvi:=ListView1.Items.Add; lvi.Text:=getDirName(strFile); lvi.ImageIndex:=1; //ファイルの更新日時を取得 DateTime:=TFile.GetLastWriteTime(strFile); //ファイルのサイズを取得 Size:=GetFileSize(strFile); lvi.Detail:=FormatDateTime('yyyy/mm/dd hh:nn:ss',DateTime)+ ' '+Inttostr(Size div 1024)+'KB'; end; except end; end; end.