001// SPDX-License-Identifier: GPL-3.0-or-later 002// Copyright © 2026 Aurélien Pierre 003 004package com.aurelienpierre.sioul; 005 006import android.app.Activity; 007import android.content.ActivityNotFoundException; 008import android.content.Context; 009import android.content.Intent; 010import android.net.Uri; 011import android.os.Bundle; 012import android.util.Log; 013 014import org.json.JSONObject; 015 016/** 017 * "Save…" beside a mail attachment, on a phone (docs/client.md, "Antivirus"; 018 * Attachments): Android's own question where to save it 019 * ({@code ACTION_CREATE_DOCUMENT}: Downloads, a folder, a cloud's app), then 020 * the copy there, on a thread of its own, and the copy in Sioul's cache taken 021 * out, saved or not. Android answers only an activity that asks for a result, 022 * so this one, never seen, asks; it stays behind Sioul's window while the 023 * file is copied, since Android's leave to write there goes with it. Rust is 024 * told what became of it (nativeSaved, android/main.cpp), and says it in the 025 * status line. 026 */ 027public final class AttachmentSave extends Activity 028{ 029 /** The attachment's copy in Sioul's cache (a canonical path). */ 030 static final String PATH = "com.aurelienpierre.sioul.extra.PATH"; 031 /** The name offered: the attachment's. */ 032 static final String NAME = "com.aurelienpierre.sioul.extra.NAME"; 033 /** Its type, from its name's extension. */ 034 static final String TYPE = "com.aurelienpierre.sioul.extra.TYPE"; 035 036 private static final int WHERE = 0x5135; 037 038 /** Rust told: {path, name, saved, shown} or {path, name, saved, problem} (crates/sioul-app/src/attachments.rs). */ 039 static native void nativeSaved(String json); 040 041 @Override 042 protected void onCreate(Bundle saved) 043 { 044 super.onCreate(saved); 045 // Made again (the screen turned): Android's question is open already. 046 if (saved != null) 047 return; 048 Attachments.SaveAs ask = Attachments.saveAs(getIntent().getStringExtra(NAME), getIntent().getStringExtra(TYPE)); 049 Intent where = new Intent(ask.action).addCategory(ask.category).setType(ask.type).putExtra(Intent.EXTRA_TITLE, ask.title); 050 try { 051 startActivityForResult(where, WHERE); 052 } catch (ActivityNotFoundException | SecurityException e) { 053 Log.w(Attachments.TAG, "Attachments: Android's question where to save could not be asked: " + e.getClass().getSimpleName()); 054 done(null); 055 } 056 } 057 058 @Override 059 protected void onActivityResult(int request, int result, Intent data) 060 { 061 super.onActivityResult(request, result, data); 062 if (request == WHERE) 063 done(result == RESULT_OK && data != null ? data.getData() : null); 064 } 065 066 /** The copy made (or not) on a thread of its own, Rust told, then this activity ends. */ 067 private void done(Uri target) 068 { 069 final Context app = getApplicationContext(); 070 final String path = getIntent().getStringExtra(PATH); 071 final String name = getIntent().getStringExtra(NAME); 072 new Thread(() -> { 073 tell(Attachments.write(app, path, target, name)); 074 runOnUiThread(this::finish); 075 }, "sioul-save").start(); 076 } 077 078 /** Rust told, when Sioul's library is loaded; else (its process made anew meanwhile) the copy is there all the same. */ 079 private static void tell(JSONObject said) 080 { 081 try { 082 nativeSaved(said.toString()); 083 } catch (UnsatisfiedLinkError e) { 084 Log.i(Attachments.TAG, "Attachments: Sioul's window closed meanwhile; nothing said."); 085 } 086 } 087}