Delphiでインテントからファイルを受け取ってみる

#はじめに
[前回の記事](https://qiita.com/himajin_sekky/items/154896605afe5af4c3d3)と対になるものです。 Delphi 10.3 CommunityEditionにて確認しています。

##暗黙的インテントを受け取る
参考：
[Delphiサンプル FMX.Android Intents Sample](http://docwiki.embarcadero.com/CodeExamples/Rio/en/FMX.Android_Intents_Sample)
[Delphi 10 SeattleのAndroidアプリケーションでIntentの送受信をするには](https://www.gesource.jp/weblog/?p=7140)

###インテントフィルタ
最初に<code>AndroidManifest.template.xml</code>を編集し、受け取りたいインテントの種類に応じた内容を追加します。
例えば、Googleドライブでファイルを選択し自アプリで開く場合は、VIEWというアクションになります。（拡張子がTxtなどのドキュメント形式の場合、EDITというアクションになるので注意）

```xml
<activity android:name="com.embarcadero.firemonkey.FMXNativeActivity" （省略）>
（中略）
  <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="application/octet-stream" />
  </intent-filter>  
</activity>
```

###コード
インテントの受信の処理は、アプリケーション動作前（停止中）と、アプリケーション動作中でインテントの取得方法が違うため、コードが分かれます。先に、アプリケーション動作中について書きます。

####アプリケーション動作中のインテント処理
interface節のusesに次を加えます。

```
System.Messaging, Androidapi.JNI.GraphicsContentViewText
```

メインフォーム（TForm1）のメソッドに次を追加します。

```pascal
procedure HandleActivityMessage(const Sender: TObject; const AMessage: TMessage);
procedure HandleIntentAction(const intent: JIntent);
```

implementation節のusesに次を加えます。

```
FMX.Platform.Android, Androidapi.Helpers
```

Formの<code>OnCreate</code>イベントに以下を記述します。

```pascal
procedure TForm1.FormCreate(Sender: TObject);
begin
  MainActivity.registerIntentAction(TJIntent.JavaClass.ACTION_VIEW);
  TMessageManager.DefaultManager.SubscribeToMessage
    (TMessageReceivedNotification, HandleActivityMessage);
end;
```
<code>MainActivity.registerIntentAction</code>の引数は、インテントフィルタに設定したものと合わせます。
複数種類のインテントを受ける場合、複数行に分けて書きます。


次にHandleActivityMessageを実装します。

```pascal
procedure TForm1.HandleActivityMessage(const Sender: TObject; const AMessage: TMessage);
var
  intent: JIntent;
begin
  if AMessage is TMessageReceivedNotification then
  begin
    intent := TMessageReceivedNotification(AMessage).Value;
    if intent <> nil then
      HandleIntentAction(intent);
  end;
end;
```

HandleIntentActionを実装します。（ここでは動作確認のみ。ファイル処理は後半で扱います。）

```pascal
procedure TForm1.HandleIntentAction(const intent: JIntent);
begin
  ShowMessage(JStringToString(intent.toString));
end;
```

一旦動作確認してみましょう。（GoogleドライブにTest.datというファイルを置いて開いてみました）
![intent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33684/7a41d130-5e50-3bd7-26b0-c3bfd0addeaf.png)
AndroidManifest.xml が古い場合や、内容が違う場合、ファイルを選択した際に候補として出てこないことがあります。その場合はプロジェクトのクリーンアップ後に再度実行してみてください。


####アプリケーション起動時のインテント処理

メインフォームに次のメソッドを追加します。

```pascal
procedure HandleAppEvent(AAppEvent: TApplicationEvent; AContext: TObject); 
```

FormのOnCreateイベントに以下を追加します。

```pascal
procedure TForm1.FormCreate(Sender: TObject);
var
  AppEventService: IFMXApplicationEventService;
begin
  if TPlatformServices.Current.SupportsPlatformService
    (IFMXApplicationEventService, AppEventService) then
    AppEventService.SetApplicationEventHandler(HandleAppEvent);

  (上で書いた処理)
end;
```

HandleAppEventを実装します。

```pascal
function TForm1.HandleAppEvent(AAppEvent: TApplicationEvent; AContext: TObject): Boolean;
var
  StartupIntent: JIntent;
begin
  result := False; //（ここちょっとよく理解できていません）

  if AAppEvent = TApplicationEvent.FinishedLaunching then //起動時イベントの場合
  begin
    StartupIntent := MainActivity.getIntent;
    if StartupIntent <> nil then
    begin
      HandleIntentAction(StartupIntent);
    end;
  end;
end;
```

デバッグ実行し、一度アプリを終了してからインテントを受け取れるか確認してみましょう！


##インテント内のファイルの抽出

先ほどのHandleIntentAction を書き変えていきますが、まずimplementation節のusesに以下を追加します。

```pascal
  Androidapi.JNI.Net,        //Jnet_Url
  Androidapi.JNI.JavaTypes,  //JInputStream
  Androidapi.JNIBridge,       //TJavaArray
  Androidapi.JNI.Provider;   //TJOpenableColumns
```

###ファイルの有無
インテントにファイルが含まれているかどうかは、<code>intent.getData</code>が<code>nil</code>かどうかで判定できます。

```pascal
var
  uri:JNet_Uri;
begin
  uri := intent.getData;
  if uri <> nil then
  begin
  （ファイルがある場合の処理）
  end;
end;
```
（ファイル以外の情報をとる場合、<code>intent.getExtras(StrintgToJString('キー名'))</code>を使用します。今回は割愛します。）

###ファイルデータの取得
URIが取得できれば、ファイルの中身は<code>JInputStream</code>にてとることができます。それを<code>TMemoryStream</code>に転写します。

```pascal
var
  inputStream: JInputStream;
  jbytes:TJavaArray<Byte>;
  memoryStream:TMemoryStream;
begin
  inputStream := TAndroidHelper.ContentResolver.openInputStream(uri);
  jbytes := TJavaArray<Byte>.Create(inputStream.available);
  inputStream.read(jbytes);

  memoryStream := TMemoryStream.Create;
  memoryStream.Write(jbytes.Data^,jbytes.Length);

  inputStream.close;
end;
```
そこから先は普段のDelphiと変わりません。

###ファイル名の取得
URI の内容が file://～ から始まっている場合 ファイル名はURI内に含まれていますが、content:// から始まっている場合は Android OSの仕組みである、ContentResolverを使用する必要があります。
参考：[【Android】「Uri」から「ファイル名」を取得する](https://qiita.com/CUTBOSS/items/3476e164b86a63b02b2e)

```pascal
var
  cursor:JCursor;
  filename:String;
begin
  if uri.getScheme.equals(TJContentResolver.JavaClass.SCHEME_FILE) then
  begin
    filename := JStringToString( Uri.getLastPathSegment );
  end
  else if uri.getScheme.equals(TJContentResolver.JavaClass.SCHEME_CONTENT) then
  begin
    cursor := TAndroidHelper.ContentResolver.query(uri, nil, nil, nil, nil);
    try
      if (cursor <> nil) and (cursor.moveToFirst) then
      begin
        filename := JStringToString(
          cursor.getString(cursor.getColumnIndex(TJOpenableColumns.JavaClass.DISPLAY_NAME) ));
      end;
    finally
      cursor.close;
    end;
  end;
  //filenameが空だった時はデフォルト名か何かをつける
end;
```
（projectionを指定しなくてもDisplayNameとSizeはとれるからいいのかなーという雑な認識）


###アプリケーションローカルにファイルのコピーを保存する場合
上記を通しで書くとこんな感じです

```pascal
procedure TForm1.HandleIntentAction(const intent: JIntent);
var
  uri:JNet_Uri;  
  cursor:JCursor;
  filename:String;
  inputStream: JInputStream;
  jbytes:TJavaArray<Byte>;
  memoryStream:TMemoryStream;
begin
  uri := intent.getData;
  if uri <> nil then
  begin
    if uri.getScheme.equals(TJContentResolver.JavaClass.SCHEME_FILE) then
    begin
      filename := JStringToString( Uri.getLastPathSegment );
    end
    else if uri.getScheme.equals(TJContentResolver.JavaClass.SCHEME_CONTENT) then
    begin
      cursor := TAndroidHelper.ContentResolver.query(uri, nil, nil, nil, nil);
      try
        if (cursor <> nil) and (cursor.moveToFirst) then
        begin
          filename := JStringToString(
            cursor.getString(cursor.getColumnIndex(TJOpenableColumns.JavaClass.DISPLAY_NAME) ));
        end;
      finally
        cursor.close;
      end;
    end;
    if filename.isEmpty then fileName := TPath.GetTempFileName;  

    inputStream := TAndroidHelper.ContentResolver.openInputStream(uri);
    jbytes := TJavaArray<Byte>.Create(inputStream.available);
    inputStream.read(jbytes);

    memoryStream := TMemoryStream.Create;
    memoryStream.Write(jbytes.Data^,jbytes.Length);

    inputStream.close;
    
    memoryStream.SaveToFile(TPath.Combine(TPath.GetDocumentPath, filename));
    memoryStream.DisposeOf;
  end;
end;
```

以上です。