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}