Adds saving media to drafts.

This commit is contained in:
Vavassor 2017-07-11 21:49:46 -04:00
parent f68f6d7473
commit a1e007eb2a
3 changed files with 236 additions and 42 deletions

View File

@ -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<ArrayList<String>>() {
}.getType());
new TypeToken<ArrayList<String>>() {}.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
}
}
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;
}
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;
}
to.write(chunk, 0, bytes);
}
} catch (IOException e) {
return false;
}
IOUtils.closeQuietly(from);
IOUtils.closeQuietly(to);
return true;
}
@Nullable
private List<String> saveMedia(@Nullable ArrayList<String> 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<File> filesSoFar = new ArrayList<>();
ArrayList<String> 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;
}
Uri uri = FileProvider.getUriForFile(this, "com.keylesspalace.tusky.fileprovider",
file);
results.add(uri.toString());
}
return results;
}
private void deleteMedia(List<String> 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<String> setDifference(List<String> a, List<String> b) {
List<String> 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;
} else {
final TootEntity toot = new TootEntity();
toot.setText(s);
toot.setContentWarning(contentWarning);
if (mediaQueued != null && mediaQueued.size() > 0) {
List<String> list = new ArrayList<>();
for (QueuedMedia q : mediaQueued) {
list.add(q.uri.toString());
}
String json = new Gson().toJson(list);
toot.setUrls(json);
}
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (savedTootUid != 0) {
toot.setUid(savedTootUid);
tootDao.updateToot(toot);
} else {
tootDao.insert(toot);
}
return null;
}
}.execute();
return true;
}
// Get any existing file's URIs.
ArrayList<String> existingUris = null;
String savedJsonUrls = getIntent().getStringExtra("saved_json_urls");
if (!TextUtils.isEmpty(savedJsonUrls)) {
existingUris = new Gson().fromJson(savedJsonUrls,
new TypeToken<ArrayList<String>>() {}.getType());
}
final TootEntity toot = new TootEntity();
toot.setText(s);
toot.setContentWarning(contentWarning);
if (!ListUtils.isEmpty(mediaQueued)) {
List<String> 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<Void, Void, Void>() {
@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;

View File

@ -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<String> uris = new Gson().fromJson(item.getUrls(),
new TypeToken<ArrayList<String>>() {}.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

View File

@ -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 <http://www.gnu.org/licenses>. */
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();
}
}
}