Android-Freigabeabsicht für eine Bitmap - Kann sie vor dem Freigeben nicht gespeichert werden?

71

Ich versuche, eine Bitmap mit Share Intent aus meiner App zu exportieren, ohne eine Datei für einen zeitlichen Speicherort zu speichern. Alle Beispiele, die ich gefunden habe, sind zweistufig 1) Speichern auf SD-Karte und Erstellen von Uri für diese Datei 2) Starten der Absicht mit diesem Uri

Ist es möglich, es zu erstellen, ohne die Berechtigung WRITE_EXTERNAL_STORAGE zu benötigen, die Datei zu speichern [und sie anschließend zu entfernen]? Wie adressiere ich Geräte ohne ExternalStorage?

Piotr
quelle

Antworten:

256

Ich hatte das gleiche Problem. Ich wollte nicht nach den Lese- und Schreibberechtigungen für externen Speicher fragen müssen. Manchmal gibt es auch Probleme, wenn Telefone keine SD-Karten haben oder die Karten nicht mehr montiert werden.

Die folgende Methode verwendet einen ContentProvider namens FileProvider . Technisch gesehen speichern Sie die Bitmap (im internen Speicher) noch vor der Freigabe, müssen jedoch keine Berechtigungen anfordern. Außerdem wird die Bilddatei jedes Mal, wenn Sie die Bitmap freigeben, überschrieben. Und da es sich im internen Cache befindet, wird es gelöscht, wenn der Benutzer die App deinstalliert. Meiner Meinung nach ist es genauso gut, das Bild nicht zu speichern. Diese Methode ist auch sicherer als das Speichern im externen Speicher.

Die Dokumentation ist ziemlich gut (siehe weiterführende Literatur unten), aber einige Teile sind etwas knifflig. Hier ist eine Zusammenfassung, die für mich funktioniert hat.

Richten Sie den FileProvider im Manifest ein

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        ...
    </application>
</manifest>

Ersetzen Sie com.example.myappdurch Ihren App-Paketnamen.

Erstellen Sie res / xml / filepaths.xml

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="shared_images" path="images/"/>
</paths>

Dies teilt dem FileProvider mit, wo die Dateien freigegeben werden sollen (in diesem Fall über das Cache-Verzeichnis).

Speichern Sie das Bild im internen Speicher

// save bitmap to cache directory
try {

    File cachePath = new File(context.getCacheDir(), "images");
    cachePath.mkdirs(); // don't forget to make the directory
    FileOutputStream stream = new FileOutputStream(cachePath + "/image.png"); // overwrites this image every time
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
    stream.close();

} catch (IOException e) {
    e.printStackTrace();
}

Teile das Bild

File imagePath = new File(context.getCacheDir(), "images");
File newFile = new File(imagePath, "image.png");
Uri contentUri = FileProvider.getUriForFile(context, "com.example.myapp.fileprovider", newFile);

if (contentUri != null) {
    Intent shareIntent = new Intent();
    shareIntent.setAction(Intent.ACTION_SEND);
    shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // temp permission for receiving app to read this file
    shareIntent.setDataAndType(contentUri, getContentResolver().getType(contentUri));
    shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
    startActivity(Intent.createChooser(shareIntent, "Choose an app"));
}

Weiterführende Literatur

Suragch
quelle
1
Tolle Lösung! Gibt es einen Grund, warum Sie getCacheDir () anstelle von getFilesDir () oder sogar getExternalCacheDir () verwenden ?
Alex Vang
Dies würde auch eine AsyncTask oder IntentService erfordern? Was funktioniert Ihrer Meinung nach am besten für diesen Fall? Haben Sie (oder kennen) Sie vollständigen Code, der Ihre Lösung in eine AsyncTask oder einen IntentService einbindet?
Alex Vang
3
@ Jamrelian, da der Zweck nur darin besteht, das Bild freizugeben und nicht zu speichern, habe ich das Cache-Verzeichnis ausgewählt. Dinge im Cache-Verzeichnis sind nicht von langer Dauer. Ich stelle mir jedoch vor, dass es gut funktionieren würde, ein anderes Verzeichnis zu verwenden.
Suragch
3
Gute Antwort! Für zukünftige Leser - Die meisten Apps speichern einen Miniaturbild-Cache relativ zum Namen der Bilddatei. Der Cache wird nicht aktualisiert, wenn der Dateiname gleich bleibt. Ich habe dieses Problem gelöst, indem ich den Inhalt des Verzeichnisses gelöscht und den Dateinamen in Millisekunden auf das aktuelle Datum gesetzt habe.
Gurupad Mamadapur
1
@ GurupadMamadapur Sie haben Recht. Sie müssen den Cache löschen und mit einem anderen Dateinamen speichern ...
Hoang Nguyen
10

Ich versuche, eine Bitmap mit Share Intent aus meiner App zu exportieren, ohne eine Datei für einen zeitlichen Speicherort zu speichern.

Theoretisch ist dies möglich. In der Praxis ist dies wahrscheinlich nicht möglich.

Theoretisch müssen Sie nur eine Urifreigeben, die in die Bitmap aufgelöst wird. Der einfachste Ansatz ist, wenn es sich um eine Datei handelt, auf die die andere Anwendung direkt zugreifen kann, z. B. auf einem externen Speicher.

Um es überhaupt nicht in Flash zu schreiben, müssten Sie Ihre eigene implementieren ContentProvider, herausfinden, wie openFile()Sie die speicherinterne Bitmap zurückgeben, und dann eine UriDarstellung dieser Bitmap in der übergeben ACTION_SEND Intent. Da a zurückgegeben werden openFile()muss, ParcelFileDescriptorweiß ich nicht, wie Sie dies ohne eine Darstellung auf der Festplatte tun würden, aber ich habe nicht viel Zeit mit der Suche verbracht.

Ist es möglich, es zu erstellen, ohne die Berechtigung WRITE_EXTERNAL_STORAGE zu benötigen, die Datei zu speichern [und sie anschließend zu entfernen]?

Wenn Sie es einfach nicht im externen Speicher haben möchten, können Sie den ContentProviderWeg mithilfe einer Datei im internen Speicher gehen. Dieses Beispielprojekt zeigt ein ProjektContentProvider , das eine PDF-Datei über ACTION_VIEWeinen PDF-Viewer auf einem Gerät bereitstellt. der gleiche Ansatz könnte für verwendet werden ACTION_SEND.

CommonsWare
quelle
Könnte man nicht einfach so verwenden, getCacheDir()wie es in dieser SO-Antwort vorgeschlagen wird ?
Suragch
1
@Suragch: Das Erstellen von weltweit lesbaren Dateien im internen Speicher Ihrer App ist keine gute Idee.
CommonsWare
1
Das macht Sinn. Nachdem ich diesen Kommentar abgegeben habe, habe ich eine weitere Ihrer Antworten gelesen, in der Sie die Verwendung von FileProvider empfehlen. Darauf arbeite ich jetzt hin.
Suragch
Die Größe des Cache-Speichers nimmt jedes Mal zu, wenn ich den Screenshot teile. Wie aus der anderen Antwort hervorgeht, wird das vorherige Bild ersetzt, sodass die Größe des Cache-Speichers nicht erhöht werden sollte. Bitte erklären Sie dies.
GvSharma
5

Wenn jemand noch nach einer einfachen und kurzen Lösung ohne Speichererlaubnis sucht (unterstützt auch Nougat 7.0). Hier ist es.

Fügen Sie dies im Manifest hinzu

<provider
     android:name="android.support.v4.content.FileProvider"
     android:authorities="${applicationId}.provider"
     android:exported="false"
     android:grantUriPermissions="true">
     <meta-data
          android:name="android.support.FILE_PROVIDER_PATHS"
          android:resource="@xml/provider_paths" />
 </provider>

Erstellen Sie nun provider_paths.xml

<paths>
    <external-path name="external_files" path="."/>
</paths>

Fügen Sie diese Methode schließlich zu Ihrer Aktivität / Ihrem Fragment hinzu (rootView ist die Ansicht, die Sie freigeben möchten).

 private void ShareIt(View rootView){
        if (rootView != null && context != null && !context.isFinishing()) {
            rootView.setDrawingCacheEnabled(true);
            Bitmap bitmap = Bitmap.createBitmap(rootView.getDrawingCache());
            if (bitmap != null ) {
                 //Save the image inside the APPLICTION folder
                File mediaStorageDir = new File(AppContext.getInstance().getExternalCacheDir() + "Image.png");

                try {
                    FileOutputStream outputStream = new FileOutputStream(String.valueOf(mediaStorageDir));
                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
                    outputStream.close();

                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                if (ObjectUtils.isNotNull(mediaStorageDir)) {

                    Uri imageUri = FileProvider.getUriForFile(getActivity(), getActivity().getApplicationContext().getPackageName() + ".provider", mediaStorageDir);

                    if (ObjectUtils.isNotNull(imageUri)) {
                        Intent waIntent = new Intent(Intent.ACTION_SEND);
                        waIntent.setType("image/*");
                        waIntent.putExtra(Intent.EXTRA_STREAM, imageUri);
                        startActivity(Intent.createChooser(waIntent, "Share with"));
                    }
                }
            }
        }
    }

Aktualisieren:

Wie @Kathir in den Kommentaren erwähnt hat,

DrawingCacheist von API 28+ veraltet. Verwenden Sie Canvasstattdessen den folgenden Code .

 Bitmap bitmap = Bitmap.createBitmap(rootView.getWidth(), rootView.getHeight(), quality);
    Canvas canvas = new Canvas(bitmap);

    Drawable backgroundDrawable = view.getBackground();
    if (backgroundDrawable != null) {
        backgroundDrawable.draw(canvas);
    } else {
        canvas.drawColor(Color.WHITE);
    }
    view.draw(canvas);

    return bitmap;
Fajar Khan
quelle
1
DrawingCacheist von API 28+ veraltet. Verwenden Sie dazu Canvas oder PixelCopy . Beispiel für die Verwendung von Canvas . Und vergessen Sie nicht, die Hintergrundfarbe (entweder auf Ihre viewoder Bitmap) einzustellen , sonst erhalten Sie einen schwarzen Hintergrund
Kathir
3

Dies dient zum Freigeben von CardView als Image und zum Speichern im Cache-Unterverzeichnis des internen Speicherbereichs der App. hoffe es wird hilfreich sein.

        @Override
        public void onClick(View view) {

            CardView.setDrawingCacheEnabled(true);
            CardView.buildDrawingCache();
            Bitmap bitmap = CardView.getDrawingCache();

            try{
                File file = new File(getContext().getCacheDir()+"/Image.png");
                bitmap.compress(Bitmap.CompressFormat.PNG,100,new FileOutputStream(file));
                Uri uri = FileProvider.getUriForFile(getContext(),"com.mydomain.app", file);

                Intent shareIntent = new Intent();
                shareIntent.setAction(Intent.ACTION_SEND);
                shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
                shareIntent.setType("image/jpeg");
                getContext().startActivity(Intent.createChooser(shareIntent, "Share"));

            }catch (FileNotFoundException e) {e.printStackTrace();}

        }
    });
ahmed aldawody
quelle
1

Hier Verfahren arbeitet , um einen Screenshot von eigenen App zu machen und teilen Sie es als Bild über jeden Messenger oder E - Mail - Client.

Um dies zu beheben , die Bitmap nicht aktualisieren Problem , das ich verbessern Suragch ‚s Antwort, mit Gurupad Mamadapur ‘ s Kommentar und hinzugefügt eigene Modifikationen.


Hier ist Code in Kotlin- Sprache:

private lateinit var myRootView:View // root view of activity
@SuppressLint("SimpleDateFormat")
private fun shareScreenshot() {
    // We need date and time to be added to image name to make it unique every time, otherwise bitmap will not update
    val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
    val currentDateandTime = sdf.format(Date())
    val imageName = "/image_$currentDateandTime.jpg"       

    // CREATE
    myRootView = window.decorView.rootView
    myRootView.isDrawingCacheEnabled = true
    myRootView.buildDrawingCache(true) // maybe You dont need this
    val bitmap = Bitmap.createBitmap(myRootView.drawingCache)
    myRootView.isDrawingCacheEnabled = false

    // SAVE
    try {
        File(this.cacheDir, "images").deleteRecursively() // delete old images
        val cachePath = File(this.cacheDir, "images")
        cachePath.mkdirs() // don't forget to make the directory
        val stream = FileOutputStream("$cachePath$imageName")
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) // can be png and any quality level
        stream.close()
    } catch (ex: Exception) {
        Toast.makeText(this, ex.javaClass.canonicalName, Toast.LENGTH_LONG).show() // You can replace this with Log.e(...)
    }

    // SHARE
    val imagePath = File(this.cacheDir, "images")
    val newFile = File(imagePath, imageName)
    val contentUri = FileProvider.getUriForFile(this, "com.example.myapp.fileprovider", newFile)
    if (contentUri != null) {
        val shareIntent = Intent()
        shareIntent.action = Intent.ACTION_SEND
        shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // temp permission for receiving app to read this file
        shareIntent.type = "image/jpeg" // just assign type. we don't need to set data, otherwise intent will not work properly
        shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri)
        startActivity(Intent.createChooser(shareIntent, "Choose app"))
    } 
}
Zakir
quelle
1
Zakir, bitte erläutern Sie Ihren Code, damit andere ihn verwenden können. Übrigens ist es gut zu sagen, dass Kotlin- und Java-Codes im selben Android Studio-Projekt verwendet werden können. Einige Leute ziehen Ihre Lösung möglicherweise nicht in Betracht, weil sie das nicht wissen. Vielen Dank!
Will
Hallo Zakir, kannst du bitte Java-Code angeben? Ich muss Scrollview-Daten erfassen. Erfolgreiches Erfassen von Bildern und Speichern in / storage, jedoch ohne gemeinsame Nutzung des Dateipfads. Würdest du mir bitte helfen
Tara