Adds saving media to drafts.
This commit is contained in:
parent
f68f6d7473
commit
a1e007eb2a
3 changed files with 236 additions and 42 deletions
|
@ -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);
|
||||
|
@ -547,20 +555,143 @@ 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 {
|
||||
}
|
||||
|
||||
// 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 (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);
|
||||
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>() {
|
||||
|
@ -577,7 +708,6 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFragm
|
|||
}.execute();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatusVisibility(String visibility) {
|
||||
statusVisibility = 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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue