diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index d5e7f59a..92a28015 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -88,6 +88,7 @@ import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; import com.keylesspalace.tusky.util.CountUpDownLatch; import com.keylesspalace.tusky.util.DownsizeImageTask; import com.keylesspalace.tusky.util.IOUtils; +import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.MediaUtils; import com.keylesspalace.tusky.util.MentionTokenizer; import com.keylesspalace.tusky.util.ParserUtils; @@ -101,6 +102,7 @@ import com.squareup.picasso.Target; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; @@ -205,6 +207,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm if (statusHideText) { contentWarning = contentWarningEditor.getText().toString(); } + /* Discard any upload URLs embedded in the text because they'll be re-uploaded when + * the draft is loaded and replaced with new URLs. */ + if (mediaQueued != null) { + for (QueuedMedia item : mediaQueued) { + removeUrlFromEditable(textEditor.getEditableText(), item.uploadUrl); + } + } boolean b = saveTheToot(textEditor.getText().toString(), contentWarning); if (b) { Toast.makeText(ComposeActivity.this, R.string.action_save_one_toot, Toast.LENGTH_SHORT).show(); @@ -317,8 +326,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm if (!TextUtils.isEmpty(savedJsonUrls)) { // try to redo a list of media loadedDraftMediaUris = new Gson().fromJson(savedJsonUrls, - new TypeToken>() { - }.getType()); + new TypeToken>() {}.getType()); } int savedTootUid = intent.getIntExtra("saved_toot_uid", 0); @@ -408,14 +416,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm statusAlreadyInFlight = false; // These can only be added after everything affected by the media queue is initialized. - /* if (loadedDraftMediaUris != null && !loadedDraftMediaUris.isEmpty()) { + if (!ListUtils.isEmpty(loadedDraftMediaUris)) { for (String uriString : loadedDraftMediaUris) { Uri uri = Uri.parse(uriString); long mediaSize = MediaUtils.getMediaSize(getContentResolver(), uri); pickMedia(uri, mediaSize); } - } else */ - if (savedMediaQueued != null) { + } else if (savedMediaQueued != null) { for (SavedQueuedMedia item : savedMediaQueued) { addMediaToQueue(item.type, item.preview, item.uri, item.mediaSize); } @@ -478,6 +485,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm for (QueuedMedia item : mediaQueued) { savedMediaQueued.add(new SavedQueuedMedia(item.type, item.uri, item.preview, item.mediaSize)); + removeUrlFromEditable(textEditor.getEditableText(), item.uploadUrl); } outState.putParcelableArrayList("savedMediaQueued", savedMediaQueued); outState.putBoolean("showMarkSensitive", showMarkSensitive); @@ -496,7 +504,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm } private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId, - View.OnClickListener listener) { + View.OnClickListener listener) { Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(descriptionId), Snackbar.LENGTH_SHORT); bar.setAction(actionId, listener); @@ -547,36 +555,158 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm } } - public boolean saveTheToot(String s, @Nullable String contentWarning) { - if (TextUtils.isEmpty(s)) { + private static boolean copyToFile(ContentResolver contentResolver, Uri uri, File file) { + InputStream from; + FileOutputStream to; + try { + from = contentResolver.openInputStream(uri); + to = new FileOutputStream(file); + } catch (FileNotFoundException e) { return false; - } else { - final TootEntity toot = new TootEntity(); - toot.setText(s); - toot.setContentWarning(contentWarning); - if (mediaQueued != null && mediaQueued.size() > 0) { - List list = new ArrayList<>(); - for (QueuedMedia q : mediaQueued) { - list.add(q.uri.toString()); + } + if (from == null) { + return false; + } + byte[] chunk = new byte[16384]; + try { + while (true) { + int bytes = from.read(chunk, 0, chunk.length); + if (bytes < 0) { + break; } - String json = new Gson().toJson(list); - toot.setUrls(json); + to.write(chunk, 0, bytes); } + } catch (IOException e) { + return false; + } + IOUtils.closeQuietly(from); + IOUtils.closeQuietly(to); + return true; + } - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - if (savedTootUid != 0) { - toot.setUid(savedTootUid); - tootDao.updateToot(toot); - } else { - tootDao.insert(toot); + @Nullable + private List saveMedia(@Nullable ArrayList existingUris) { + File imageDirectory = getExternalFilesDir(Environment.DIRECTORY_PICTURES); + File videoDirectory = getExternalFilesDir(Environment.DIRECTORY_MOVIES); + if (imageDirectory == null || !(imageDirectory.exists() || imageDirectory.mkdirs())) { + Log.e(TAG, "Image directory is not created."); + return null; + } + if (videoDirectory == null || !(videoDirectory.exists() || videoDirectory.mkdirs())) { + Log.e(TAG, "Video directory is not created."); + return null; + } + ContentResolver contentResolver = getContentResolver(); + ArrayList filesSoFar = new ArrayList<>(); + ArrayList results = new ArrayList<>(); + for (QueuedMedia item : mediaQueued) { + /* If the media was already saved in a previous draft, there's no need to save another + * copy, just add the existing URI to the results. */ + if (existingUris != null) { + String uri = item.uri.toString(); + int index = existingUris.indexOf(uri); + if (index != -1) { + results.add(uri); + continue; + } + } + // Otherwise, save the media. + File directory; + switch (item.type) { + default: + case IMAGE: directory = imageDirectory; break; + case VIDEO: directory = videoDirectory; break; + } + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) + .format(new Date()); + String mimeType = contentResolver.getType(item.uri); + MimeTypeMap map = MimeTypeMap.getSingleton(); + String fileExtension = map.getExtensionFromMimeType(mimeType); + String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension); + File file = new File(directory, filename); + filesSoFar.add(file); + boolean copied = copyToFile(contentResolver, item.uri, file); + if (!copied) { + /* If any media files were created in prior iterations, delete those before + * returning. */ + for (File earlierFile : filesSoFar) { + boolean deleted = earlierFile.delete(); + if (!deleted) { + Log.i(TAG, "Could not delete the file " + earlierFile.toString()); } - return null; } - }.execute(); - return true; + return null; + } + Uri uri = FileProvider.getUriForFile(this, "com.keylesspalace.tusky.fileprovider", + file); + results.add(uri.toString()); + } + return results; + } + + private void deleteMedia(List mediaUris) { + for (String uriString : mediaUris) { + Uri uri = Uri.parse(uriString); + if (getContentResolver().delete(uri, null, null) == 0) { + Log.e(TAG, String.format("Did not delete file %s.", uriString)); + } + } + } + + private static List setDifference(List a, List b) { + List c = new ArrayList<>(); + for (String s : a) { + if (!b.contains(s)) { + c.add(s); + } } + return c; + } + + public boolean saveTheToot(String s, @Nullable String contentWarning) { + if (TextUtils.isEmpty(s)) { + return false; + } + + // Get any existing file's URIs. + ArrayList existingUris = null; + String savedJsonUrls = getIntent().getStringExtra("saved_json_urls"); + if (!TextUtils.isEmpty(savedJsonUrls)) { + existingUris = new Gson().fromJson(savedJsonUrls, + new TypeToken>() {}.getType()); + } + + final TootEntity toot = new TootEntity(); + toot.setText(s); + toot.setContentWarning(contentWarning); + if (!ListUtils.isEmpty(mediaQueued)) { + List savedList = saveMedia(existingUris); + if (!ListUtils.isEmpty(savedList)) { + String json = new Gson().toJson(savedList); + toot.setUrls(json); + deleteMedia(setDifference(existingUris, savedList)); + } else { + return false; + } + } else if (!ListUtils.isEmpty(existingUris)) { + /* If there were URIs in the previous draft, but they've now been removed, those files + * can be deleted. */ + deleteMedia(existingUris); + } + + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + if (savedTootUid != 0) { + toot.setUid(savedTootUid); + tootDao.updateToot(toot); + } else { + tootDao.insert(toot); + } + return null; + } + }.execute(); + return true; } private void setStatusVisibility(String visibility) { @@ -785,6 +915,17 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm } private void onSendSuccess() { + // If the status was loaded from a draft, delete the draft and associated media files. + if (savedTootUid != 0) { + TootEntity status = new TootEntity(); + status.setUid(savedTootUid); + tootDao.delete(status); + for (QueuedMedia item : mediaQueued) { + if (getContentResolver().delete(item.uri, null, null) == 0) { + Log.e(TAG, String.format("Did not delete file %s.", item.uri.toString())); + } + } + } Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT); bar.show(); @@ -1036,15 +1177,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(), textEditor.getPaddingRight(), 0); } - // Remove the text URL associated with this media. - if (item.uploadUrl != null) { - Editable text = textEditor.getText(); - int start = text.getSpanStart(item.uploadUrl); - int end = text.getSpanEnd(item.uploadUrl); - if (start != -1 && end != -1) { - text.delete(start, end); - } - } + removeUrlFromEditable(textEditor.getEditableText(), item.uploadUrl); enableMediaButtons(); cancelReadyingMedia(item); } @@ -1057,6 +1190,17 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm } } + private static void removeUrlFromEditable(Editable editable, @Nullable URLSpan urlSpan) { + if (urlSpan == null) { + return; + } + int start = editable.getSpanStart(urlSpan); + int end = editable.getSpanEnd(urlSpan); + if (start != -1 && end != -1) { + editable.delete(start, end); + } + } + private void downsizeMedia(final QueuedMedia item) { item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING; diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 10507d68..d5551bf3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -17,6 +17,7 @@ package com.keylesspalace.tusky; import android.content.Intent; import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.support.v7.app.ActionBar; @@ -24,18 +25,23 @@ import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; import com.keylesspalace.tusky.adapter.SavedTootAdapter; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.util.ThemeUtils; +import java.util.ArrayList; import java.util.List; public class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction { + public static final String TAG = "SavedTootActivity"; // logging tag // dao private static TootDao tootDao = TuskyApplication.getDB().tootDao(); @@ -71,7 +77,6 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. recyclerView.addItemDecoration(divider); adapter = new SavedTootAdapter(this); recyclerView.setAdapter(adapter); - } @Override @@ -123,6 +128,15 @@ public class SavedTootActivity extends BaseActivity implements SavedTootAdapter. @Override public void delete(int position, TootEntity item) { + // Delete any media files associated with the status. + ArrayList uris = new Gson().fromJson(item.getUrls(), + new TypeToken>() {}.getType()); + for (String uriString : uris) { + Uri uri = Uri.parse(uriString); + if (getContentResolver().delete(uri, null, null) == 0) { + Log.e(TAG, String.format("Did not delete file %s.", uriString)); + } + } // update DB tootDao.delete(item); // update adapter diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.java new file mode 100644 index 00000000..ae269683 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.java @@ -0,0 +1,36 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util; + +import android.support.annotation.Nullable; + +import java.util.List; + +public class ListUtils { + /** @return true if list is null or else return list.isEmpty() */ + public static boolean isEmpty(@Nullable List list) { + return list == null || list.isEmpty(); + } + + /** @return 0 if list is null, or else return list.size() */ + public static int getSize(@Nullable List list) { + if (list == null) { + return 0; + } else { + return list.size(); + } + } +}