From 06972ec95c77194d42b593b3ee25685ce6f3f278 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 15 Sep 2018 19:38:45 +0200 Subject: [PATCH] show conversation media in contact/conference details --- .../persistance/DatabaseBackend.java | 23 ++ .../persistance/FileBackend.java | 24 +- .../services/XmppConnectionService.java | 12 + .../ui/ConferenceDetailsActivity.java | 26 +- .../ui/ContactDetailsActivity.java | 34 ++- .../ui/adapter/MediaAdapter.java | 225 ++++++++++++++++++ .../ui/adapter/MediaPreviewAdapter.java | 38 +-- .../ui/adapter/MessageAdapter.java | 23 +- .../ui/interfaces/OnMediaLoaded.java | 10 + .../conversations/ui/util/Attachment.java | 13 +- .../conversations/ui/util/GridManager.java | 71 ++++++ .../siacs/conversations/ui/util/ViewUtil.java | 51 ++++ .../ui/widget/SquareFrameLayout.java | 25 ++ .../res/layout/activity_contact_details.xml | 77 ++++-- src/main/res/layout/activity_muc_details.xml | 49 +++- src/main/res/layout/media.xml | 14 ++ src/main/res/values/dimens.xml | 1 + src/main/res/values/strings.xml | 1 + 18 files changed, 633 insertions(+), 84 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java create mode 100644 src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/GridManager.java create mode 100644 src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java create mode 100644 src/main/res/layout/media.xml diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 5daafea1d..92bcb97ee 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -34,6 +34,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import org.json.JSONException; @@ -774,6 +775,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { }; } + public List getRelativeFilePaths(Account account, Jid jid, int limit) { + SQLiteDatabase db = this.getReadableDatabase(); + final String SQL = "select uuid,relativeFilePath from messages where type in (1,2) and conversationUuid=(select uuid from conversations where accountUuid=? and (contactJid=? or contactJid like ?)) order by timeSent desc"; + final String[] args = {account.getUuid(), jid.asBareJid().toEscapedString(), jid.asBareJid().toEscapedString()+"/%"}; + Cursor cursor = db.rawQuery(SQL+(limit > 0 ? " limit "+String.valueOf(limit) : ""), args); + List filesPaths = new ArrayList<>(); + while(cursor.moveToNext()) { + filesPaths.add(new FilePath(cursor.getString(0),cursor.getString(1))); + } + cursor.close(); + return filesPaths; + } + + public static class FilePath { + public final UUID uuid; + public final String path; + private FilePath(String uuid, String path) { + this.uuid = UUID.fromString(uuid); + this.path = path; + } + } + public Conversation findConversation(final Account account, final Jid contactJid) { SQLiteDatabase db = this.getReadableDatabase(); String[] selectionArgs = {account.getUuid(), diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index f74d0a1ac..501dd0bab 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -43,6 +43,7 @@ import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; @@ -240,12 +241,12 @@ public class FileBackend { } public Bitmap getPreviewForUri(Attachment attachment, int size, boolean cacheOnly) { + final String key = "attachment_"+attachment.getUuid().toString()+"_"+String.valueOf(size); final LruCache cache = mXmppConnectionService.getBitmapCache(); - Bitmap bitmap = cache.get(attachment.getUuid().toString()); + Bitmap bitmap = cache.get(key); if (bitmap != null || cacheOnly) { return bitmap; } - Log.d(Config.LOGTAG,"attachment mime="+attachment.getMime()); if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) { bitmap = cropCenterSquareVideo(attachment.getUri(), size); drawOverlay(bitmap, paintOverlayBlack(bitmap) ? R.drawable.play_video_black : R.drawable.play_video_white, 0.75f); @@ -258,7 +259,7 @@ public class FileBackend { bitmap = withGifOverlay; } } - cache.put(attachment.getUuid().toString(), bitmap); + cache.put(key, bitmap); return bitmap; } @@ -452,7 +453,22 @@ public class FileBackend { } } - public String getConversationsDirectory(final String type) { + public List convertToAttachments(List relativeFilePaths) { + List attachments = new ArrayList<>(); + for(DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) { + final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path)); + Log.d(Config.LOGTAG,"mime="+mime); + File file = getFileForPath(relativeFilePath.path, mime); + if (file.exists()) { + attachments.add(Attachment.of(relativeFilePath.uuid, file,mime)); + } else { + Log.d(Config.LOGTAG,"file "+file.getAbsolutePath()+" doesnt exist"); + } + } + return attachments; + } + + private String getConversationsDirectory(final String type) { return getConversationsDirectory(mXmppConnectionService, type); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 83ced6879..61a67dcde 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -106,7 +106,9 @@ import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.ui.SettingsActivity; import eu.siacs.conversations.ui.UiCallback; import eu.siacs.conversations.ui.interfaces.OnAvatarPublication; +import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable; +import eu.siacs.conversations.ui.util.Attachment; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.ConversationsFileObserver; import eu.siacs.conversations.utils.CryptoHelper; @@ -2418,6 +2420,16 @@ public class XmppConnectionService extends Service { return false; } + + public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) { + getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded); + } + + + public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) { + new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start(); + } + public void persistSelfNick(MucOptions.User self) { final Conversation conversation = self.getConversation(); Jid full = self.getFullJid(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index b22e715f1..230414adb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -11,13 +11,12 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.os.Bundle; -import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; import android.support.v7.widget.Toolbar; import android.text.Editable; import android.text.SpannableStringBuilder; import android.text.TextWatcher; -import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; @@ -32,6 +31,7 @@ import org.openintents.openpgp.util.OpenPgpUtils; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; @@ -49,6 +49,10 @@ import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate; +import eu.siacs.conversations.ui.adapter.MediaAdapter; +import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.ui.util.GridManager; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.ui.util.MucDetailsContextMenuHelper; import eu.siacs.conversations.ui.util.MyLinkify; @@ -63,7 +67,7 @@ import rocks.xmpp.addr.Jid; import static eu.siacs.conversations.entities.Bookmark.printableValue; import static eu.siacs.conversations.utils.StringUtils.changed; -public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher { +public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConfigurationPushed, TextWatcher, OnMediaLoaded { public static final String ACTION_VIEW_MUC = "view_muc"; private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme @@ -77,6 +81,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } }; private ActivityMucDetailsBinding binding; + private MediaAdapter mMediaAdapter; private String uuid = null; private User mSelectedUser = null; @@ -273,6 +278,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers this.binding.mucEditTitle.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(this); this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject)); + mMediaAdapter = new MediaAdapter(this,R.dimen.media_size); + this.binding.media.setAdapter(mMediaAdapter); + GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); } @Override @@ -442,6 +450,16 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers return true; } + @Override + public void onMediaLoaded(List attachments) { + runOnUiThread(() -> { + int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size()))); + binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); + }); + + } + protected void saveAsBookmark() { xmppConnectionService.saveConversationAsBookmark(mConversation, mConversation.getMucOptions().getName()); @@ -468,6 +486,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers if (uuid != null) { this.mConversation = xmppConnectionService.findConversationByUuid(uuid); if (this.mConversation != null) { + final int limit = GridManager.getCurrentColumnCount(this.binding.media); + xmppConnectionService.getAttachments(this.mConversation, limit, this); updateView(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 3c4e257d6..98c9c4ca9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.ui; import android.content.ActivityNotFoundException; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -11,13 +12,18 @@ import android.preference.PreferenceManager; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; +import android.support.v4.content.ContextCompat; import android.support.v7.app.AlertDialog; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; import android.support.v7.widget.Toolbar; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewTreeObserver; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.TextView; @@ -25,6 +31,7 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpUtils; +import java.util.Collections; import java.util.List; import eu.siacs.conversations.Config; @@ -38,6 +45,10 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.ui.adapter.MediaAdapter; +import eu.siacs.conversations.ui.interfaces.OnMediaLoaded; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.ui.util.GridManager; import eu.siacs.conversations.ui.util.MenuDoubleTabUtil; import eu.siacs.conversations.utils.IrregularUnicodeDetector; import eu.siacs.conversations.utils.UIHelper; @@ -48,9 +59,12 @@ import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; import rocks.xmpp.addr.Jid; -public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated { +public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated, OnMediaLoaded { public static final String ACTION_VIEW_CONTACT = "view_contact"; ActivityContactDetailsBinding binding; + + private MediaAdapter mMediaAdapter; + private Contact contact; private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() { @@ -185,6 +199,10 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp populateView(); }); binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact)); + + mMediaAdapter = new MediaAdapter(this,R.dimen.media_size); + this.binding.media.setAdapter(mMediaAdapter); + GridManager.setupLayoutManager(this, this.binding.media, R.dimen.media_size); } @Override @@ -204,6 +222,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false); this.showLastSeen = preferences.getBoolean("last_activity", false); } + mMediaAdapter.setAttachments(Collections.emptyList()); } @Override @@ -466,6 +485,9 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp processFingerprintVerification(mPendingFingerprintVerificationUri); mPendingFingerprintVerificationUri = null; } + + final int limit = GridManager.getCurrentColumnCount(this.binding.media); + xmppConnectionService.getAttachments(account, contact.getJid().asBareJid(), limit, this); populateView(); } } @@ -485,4 +507,14 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show(); } } + + @Override + public void onMediaLoaded(List attachments) { + runOnUiThread(() -> { + int limit = GridManager.getCurrentColumnCount(binding.media); + mMediaAdapter.setAttachments(attachments.subList(0, Math.min(limit,attachments.size()))); + binding.mediaWrapper.setVisibility(attachments.size() > 0 ? View.VISIBLE : View.GONE); + }); + + } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java new file mode 100644 index 000000000..8adb55415 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaAdapter.java @@ -0,0 +1,225 @@ +package eu.siacs.conversations.ui.adapter; + +import android.content.Context; +import android.content.res.Resources; +import android.databinding.DataBindingUtil; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.support.annotation.AttrRes; +import android.support.annotation.DimenRes; +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.RejectedExecutionException; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.databinding.MediaBinding; +import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.ui.util.Attachment; +import eu.siacs.conversations.ui.util.StyledAttributes; +import eu.siacs.conversations.ui.util.ViewUtil; + +public class MediaAdapter extends RecyclerView.Adapter { + + private static final List DOCUMENT_MIMES = Arrays.asList( + "application/pdf", + "application/vnd.oasis.opendocument.text", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/x-tex", + "text/plain" + ); + + private final ArrayList attachments = new ArrayList<>(); + + private final XmppActivity activity; + + private int mediaSize = 0; + + public MediaAdapter(XmppActivity activity, @DimenRes int mediaSize) { + this.activity = activity; + this.mediaSize = Math.round(activity.getResources().getDimension(mediaSize)); + } + + public static void setMediaSize(RecyclerView recyclerView, int mediaSize) { + RecyclerView.Adapter adapter = recyclerView.getAdapter(); + if (adapter instanceof MediaAdapter) { + ((MediaAdapter) adapter).setMediaSize(mediaSize); + } + } + + private static @AttrRes int getImageAttr(Attachment attachment) { + final @AttrRes int attr; + if (attachment.getType() == Attachment.Type.LOCATION) { + attr = R.attr.media_preview_location; + } else if (attachment.getType() == Attachment.Type.RECORDING) { + attr = R.attr.media_preview_recording; + } else { + final String mime = attachment.getMime(); + if (mime == null) { + attr = R.attr.media_preview_unknown; + } else if (mime.startsWith("audio/")) { + attr = R.attr.media_preview_audio; + } else if (mime.equals("text/calendar") || (mime.equals("text/x-vcalendar"))) { + attr = R.attr.media_preview_calendar; + } else if (mime.equals("text/x-vcard")) { + attr = R.attr.media_preview_contact; + } else if (mime.equals("application/vnd.android.package-archive")) { + attr = R.attr.media_preview_app; + } else if (mime.equals("application/zip") || mime.equals("application/rar")) { + attr = R.attr.media_preview_archive; + } else if (DOCUMENT_MIMES.contains(mime)) { + attr = R.attr.media_preview_document; + } else { + attr = R.attr.media_preview_unknown; + } + } + return attr; + } + + public static void renderPreview(Context context, Attachment attachment, ImageView imageView) { + imageView.setBackgroundColor(StyledAttributes.getColor(context, R.attr.color_background_tertiary)); + imageView.setImageAlpha(Math.round(StyledAttributes.getFloat(context, R.attr.icon_alpha) * 255)); + imageView.setImageDrawable(StyledAttributes.getDrawable(context, getImageAttr(attachment))); + } + + private static boolean cancelPotentialWork(Attachment attachment, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Attachment oldAttachment = bitmapWorkerTask.attachment; + if (oldAttachment == null || !oldAttachment.equals(attachment)) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + @NonNull + @Override + public MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + MediaBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.media, parent, false); + return new MediaViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) { + final Attachment attachment = attachments.get(position); + if (attachment.renderThumbnail()) { + holder.binding.media.setImageAlpha(255); + loadPreview(attachment, holder.binding.media); + } else { + cancelPotentialWork(attachment, holder.binding.media); + renderPreview(activity, attachment, holder.binding.media); + } + holder.binding.media.setOnClickListener(v -> ViewUtil.view(activity, attachment)); + } + + public void setAttachments(List attachments) { + this.attachments.clear(); + this.attachments.addAll(attachments); + notifyDataSetChanged(); + } + + private void setMediaSize(int mediaSize) { + this.mediaSize = mediaSize; + } + + private void loadPreview(Attachment attachment, ImageView imageView) { + if (cancelPotentialWork(attachment, imageView)) { + final Bitmap bm = activity.xmppConnectionService.getFileBackend().getPreviewForUri(attachment,mediaSize,true); + if (bm != null) { + cancelPotentialWork(attachment, imageView); + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(0xff333333); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(attachment); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + @Override + public int getItemCount() { + return attachments.size(); + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + class MediaViewHolder extends RecyclerView.ViewHolder { + + private final MediaBinding binding; + + MediaViewHolder(MediaBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + class BitmapWorkerTask extends AsyncTask { + private final WeakReference imageViewReference; + private Attachment attachment = null; + + BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Attachment... params) { + this.attachment = params[0]; + return activity.xmppConnectionService.getFileBackend().getPreviewForUri(this.attachment, mediaSize, false); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null && !isCancelled()) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java index ce0960592..dbf5f7ec7 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java @@ -30,15 +30,6 @@ import eu.siacs.conversations.ui.util.StyledAttributes; public class MediaPreviewAdapter extends RecyclerView.Adapter { - private static final List DOCUMENT_MIMES = Arrays.asList( - "application/pdf", - "application/vnd.oasis.opendocument.text", - "application/msword", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - "text/x-tex", - "text/plain" - ); - private final ArrayList mediaPreviews = new ArrayList<>(); private final ConversationFragment conversationFragment; @@ -64,34 +55,7 @@ public class MediaPreviewAdapter extends RecyclerView.Adapter { int pos = mediaPreviews.indexOf(attachment); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 488ebc800..ff580a005 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -71,6 +71,7 @@ import eu.siacs.conversations.ui.service.AudioPlayer; import eu.siacs.conversations.ui.text.DividerSpan; import eu.siacs.conversations.ui.text.QuoteSpan; import eu.siacs.conversations.ui.util.MyLinkify; +import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.ClickableMovementMethod; import eu.siacs.conversations.ui.widget.CopyTextView; import eu.siacs.conversations.ui.widget.ListSelectionManager; @@ -896,31 +897,11 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie Toast.makeText(activity, R.string.file_deleted, Toast.LENGTH_SHORT).show(); return; } - Intent openIntent = new Intent(Intent.ACTION_VIEW); String mime = file.getMimeType(); if (mime == null) { mime = "*/*"; } - Uri uri; - try { - uri = FileBackend.getUriForFile(activity, file); - } catch (SecurityException e) { - Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); - Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); - return; - } - openIntent.setDataAndType(uri, mime); - openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - PackageManager manager = activity.getPackageManager(); - List info = manager.queryIntentActivities(openIntent, 0); - if (info.size() == 0) { - openIntent.setDataAndType(uri, "*/*"); - } - try { - getContext().startActivity(openIntent); - } catch (ActivityNotFoundException e) { - Toast.makeText(activity, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); - } + ViewUtil.view(activity, file, mime); } public void showLocation(Message message) { diff --git a/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java b/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java new file mode 100644 index 000000000..daa6d4b38 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/interfaces/OnMediaLoaded.java @@ -0,0 +1,10 @@ +package eu.siacs.conversations.ui.interfaces; + +import java.util.List; + +import eu.siacs.conversations.ui.util.Attachment; + +public interface OnMediaLoaded { + + void onMediaLoaded(List attachments); +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java index b9726f70f..a4ed47fdb 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/Attachment.java +++ b/src/main/java/eu/siacs/conversations/ui/util/Attachment.java @@ -37,6 +37,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -44,7 +45,6 @@ import java.util.UUID; import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.MimeUtils; -import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class Attachment implements Parcelable { @@ -97,6 +97,13 @@ public class Attachment implements Parcelable { private final UUID uuid; private final String mime; + private Attachment(UUID uuid, Uri uri, Type type, String mime) { + this.uri = uri; + this.type = type; + this.mime = mime; + this.uuid = uuid; + } + private Attachment(Uri uri, Type type, String mime) { this.uri = uri; this.type = type; @@ -118,6 +125,10 @@ public class Attachment implements Parcelable { return attachments; } + public static Attachment of(UUID uuid, final File file, String mime) { + return new Attachment(uuid, Uri.fromFile(file),mime != null && (mime.startsWith("image/") || mime.startsWith("video/")) ? Type.IMAGE : Type.FILE, mime); + } + public static List extractAttachments(final Context context, final Intent intent, Type type) { List uris = new ArrayList<>(); if (intent == null) { diff --git a/src/main/java/eu/siacs/conversations/ui/util/GridManager.java b/src/main/java/eu/siacs/conversations/ui/util/GridManager.java new file mode 100644 index 000000000..467126f51 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/GridManager.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.ui.util; + +import android.content.Context; +import android.support.annotation.DimenRes; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.ViewTreeObserver; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.ui.adapter.MediaAdapter; + +public class GridManager { + + public static void setupLayoutManager(final Context context, RecyclerView recyclerView, @DimenRes int desiredSize) { + int maxWidth = context.getResources().getDisplayMetrics().widthPixels; + ColumnInfo columnInfo = calculateColumnCount(context, maxWidth, desiredSize); + Log.d(Config.LOGTAG, "preliminary count=" + columnInfo.count); + MediaAdapter.setMediaSize(recyclerView, columnInfo.width); + recyclerView.setLayoutManager(new GridLayoutManager(context, columnInfo.count)); + recyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + final ColumnInfo columnInfo = calculateColumnCount(context, recyclerView.getMeasuredWidth(), desiredSize); + Log.d(Config.LOGTAG, "final count " + columnInfo.count); + if (recyclerView.getAdapter().getItemCount() != 0) { + Log.e(Config.LOGTAG, "adapter already has items; just go with it now"); + return; + } + setupLayoutManagerInternal(recyclerView, columnInfo); + MediaAdapter.setMediaSize(recyclerView, columnInfo.width); + } + }); + } + + private static void setupLayoutManagerInternal(RecyclerView recyclerView, final ColumnInfo columnInfo) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + ((GridLayoutManager) layoutManager).setSpanCount(columnInfo.count); + } + } + + private static ColumnInfo calculateColumnCount(Context context, int availableWidth, @DimenRes int desiredSize) { + final float desiredWidth = context.getResources().getDimension(desiredSize); + final int columns = Math.round(availableWidth / desiredWidth); + final int realWidth = availableWidth / columns; + Log.d(Config.LOGTAG, "desired=" + desiredWidth + " real=" + realWidth); + return new ColumnInfo(columns, realWidth); + } + + public static int getCurrentColumnCount(RecyclerView recyclerView) { + RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + return ((GridLayoutManager) layoutManager).getSpanCount(); + } else { + return 0; + } + } + + public static class ColumnInfo { + private final int count; + private final int width; + + private ColumnInfo(int count, int width) { + this.count = count; + this.width = width; + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java new file mode 100644 index 000000000..c2f89cba3 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/util/ViewUtil.java @@ -0,0 +1,51 @@ +package eu.siacs.conversations.ui.util; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.util.Log; +import android.widget.Toast; + +import java.io.File; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.persistance.FileBackend; + +public class ViewUtil { + + public static void view(Context context, Attachment attachment) { + File file = new File(attachment.getUri().getPath()); + final String mime = attachment.getMime() == null ? "*/*" : attachment.getMime(); + view(context, file, mime); + } + + public static void view(Context context, File file, String mime) { + Intent openIntent = new Intent(Intent.ACTION_VIEW); + Uri uri; + try { + uri = FileBackend.getUriForFile(context, file); + } catch (SecurityException e) { + Log.d(Config.LOGTAG, "No permission to access " + file.getAbsolutePath(), e); + Toast.makeText(context, context.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show(); + return; + } + openIntent.setDataAndType(uri, mime); + openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + PackageManager manager = context.getPackageManager(); + List info = manager.queryIntentActivities(openIntent, 0); + if (info.size() == 0) { + openIntent.setDataAndType(uri, "*/*"); + } + try { + context.startActivity(openIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show(); + } + } + +} diff --git a/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java b/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java new file mode 100644 index 000000000..29e85b1d9 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/SquareFrameLayout.java @@ -0,0 +1,25 @@ +package eu.siacs.conversations.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +public class SquareFrameLayout extends FrameLayout { + public SquareFrameLayout(Context context) { + super(context); + } + + public SquareFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + //noinspection SuspiciousNameCombination + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } +} diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index b93a8afac..5337c46c1 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + layout="@layout/toolbar" /> + android:scaleType="centerCrop" /> + android:textAppearance="@style/TextAppearance.Conversations.Title" /> - + android:orientation="horizontal"> + android:textAppearance="@style/TextAppearance.Conversations.Subhead" /> + android:textAppearance="@style/TextAppearance.Conversations.Body1" />