Support for "recent photos" thumbnailsabove attachment selector

// FREEBIE

Closes #5893
This commit is contained in:
Moxie Marlinspike 2016-11-25 22:37:23 -08:00
parent e8ae6d67b1
commit 8b342ee18b
10 changed files with 271 additions and 17 deletions

View file

@ -224,10 +224,6 @@ android {
proguardFiles = buildTypes.debug.proguardFiles
signingConfig signingConfigs.release
}
dev.initWith(buildTypes.debug)
dev {
buildConfigField "boolean", "DEV_BUILD", "true"
}
}
sourceSets {

View file

@ -5,12 +5,20 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attachment_type_selector_background"
android:elevation="4dp"
android:padding="16dp">
android:elevation="4dp">
<org.thoughtcrime.securesms.components.RecentPhotoViewRail
android:id="@+id/recent_photos"
android:layout_width="match_parent"
android:layout_height="90dp"
android:padding="4dp"/>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:weightSum="4">
<LinearLayout android:layout_width="match_parent"
@ -112,6 +120,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:weightSum="4">
<LinearLayout android:layout_width="match_parent"

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.widget.RecyclerView
android:id="@+id/photo_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"/>
</merge>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.SquareFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="2dp"
app:square_height="true">
<ImageView android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</org.thoughtcrime.securesms.components.SquareFrameLayout>

View file

@ -179,4 +179,8 @@
<attr name="offset" format="integer"/>
</declare-styleable>
<declare-styleable name="SquareFrameLayout">
<attr name="square_height" format="boolean"/>
</declare-styleable>
</resources>

View file

@ -985,7 +985,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeBubble.getBackground().setColorFilter(defaultColor, PorterDuff.Mode.MULTIPLY);
colors.recycle();
attachmentTypeSelector = new AttachmentTypeSelector(this, new AttachmentTypeListener());
attachmentTypeSelector = new AttachmentTypeSelector(this, getSupportLoaderManager(), new AttachmentTypeListener());
attachmentManager = new AttachmentManager(this, this);
audioRecorder = new AudioRecorder(this, masterSecret);
@ -1607,6 +1607,14 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onClick(int type) {
addAttachment(type);
}
@Override
public void onQuickAttachment(Uri uri) {
Intent intent = new Intent();
intent.setData(uri);
onActivityResult(PICK_IMAGE, RESULT_OK, intent);
}
}
private class QuickCameraToggleListener implements OnClickListener {

View file

@ -5,9 +5,11 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
@ -52,11 +54,12 @@ public class AttachmentTypeSelector extends PopupWindow {
private @Nullable View currentAnchor;
private @Nullable AttachmentClickedListener listener;
public AttachmentTypeSelector(@NonNull Context context, @Nullable AttachmentClickedListener listener) {
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) {
super(context);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
RecentPhotoViewRail recentPhotos = ViewUtil.findById(layout, R.id.recent_photos);
this.listener = listener;
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
@ -76,6 +79,7 @@ public class AttachmentTypeSelector extends PopupWindow {
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
this.closeButton.setOnClickListener(new CloseClickListener());
recentPhotos.setListener(new RecentPhotoSelectedListener());
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
ViewUtil.findById(layout, R.id.location_linear_layout).setVisibility(View.INVISIBLE);
@ -89,6 +93,8 @@ public class AttachmentTypeSelector extends PopupWindow {
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
setFocusable(true);
setTouchable(true);
loaderManager.initLoader(1, null, recentPhotos);
}
public void show(@NonNull Activity activity, final @NonNull View anchor) {
@ -236,6 +242,15 @@ public class AttachmentTypeSelector extends PopupWindow {
return new Pair<>(x, y);
}
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
@Override
public void onItemClicked(Uri uri) {
animateWindowOutTranslate(getContentView());
if (listener != null) listener.onQuickAttachment(uri);
}
}
private class PropagatingClickListener implements View.OnClickListener {
private final int type;
@ -262,6 +277,7 @@ public class AttachmentTypeSelector extends PopupWindow {
public interface AttachmentClickedListener {
public void onClick(int type);
public void onQuickAttachment(Uri uri);
}
}

View file

@ -0,0 +1,146 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.signature.MediaStoreSignature;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
@NonNull private final RecyclerView recyclerView;
@Nullable private OnItemClickedListener listener;
public RecentPhotoViewRail(Context context) {
this(context, null);
}
public RecentPhotoViewRail(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RecentPhotoViewRail(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.recent_photo_view, this);
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
}
public void setListener(@Nullable OnItemClickedListener listener) {
this.listener = listener;
if (this.recyclerView.getAdapter() != null) {
((RecentPhotoAdapter)this.recyclerView.getAdapter()).setListener(listener);
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new RecentPhotosLoader(getContext());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
this.recyclerView.setAdapter(new RecentPhotoAdapter(getContext(), data, RecentPhotosLoader.BASE_URL, listener));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(null);
}
private static class RecentPhotoAdapter extends CursorRecyclerViewAdapter<RecentPhotoAdapter.RecentPhotoViewHolder> {
@NonNull private final Uri baseUri;
@Nullable private OnItemClickedListener clickedListener;
private RecentPhotoAdapter(@NonNull Context context, @NonNull Cursor cursor, @NonNull Uri baseUri, @Nullable OnItemClickedListener listener) {
super(context, cursor);
this.baseUri = baseUri;
this.clickedListener = listener;
}
@Override
public RecentPhotoViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.recent_photo_view_item, parent, false);
return new RecentPhotoViewHolder(itemView);
}
@Override
public void onBindItemViewHolder(RecentPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
viewHolder.imageView.setImageDrawable(null);
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns._ID));
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION));
final Uri uri = Uri.withAppendedPath(baseUri, Long.toString(id));
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
Glide.with(getContext())
.fromMediaStore()
.load(uri)
.signature(signature)
.centerCrop()
.into(viewHolder.imageView);
viewHolder.imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickedListener != null) clickedListener.onItemClicked(uri);
}
});
}
public void setListener(@Nullable OnItemClickedListener listener) {
this.clickedListener = listener;
}
static class RecentPhotoViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
RecentPhotoViewHolder(View itemView) {
super(itemView);
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
}
}
}
public interface OnItemClickedListener {
public void onItemClicked(Uri uri);
}
}

View file

@ -2,34 +2,45 @@ package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.BitmapFactory;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import org.thoughtcrime.securesms.R;
public class SquareFrameLayout extends FrameLayout {
private final boolean squareHeight;
@SuppressWarnings("unused")
public SquareFrameLayout(Context context) {
super(context);
this(context, null);
}
@SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
this(context, attrs, 0);
}
@TargetApi(VERSION_CODES.HONEYCOMB) @SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(VERSION_CODES.LOLLIPOP) @SuppressWarnings("unused")
public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SquareFrameLayout, 0, 0);
this.squareHeight = typedArray.getBoolean(R.styleable.SquareFrameLayout_square_height, false);
typedArray.recycle();
} else {
this.squareHeight = false;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//noinspection SuspiciousNameCombination
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
if (squareHeight) super.onMeasure(heightMeasureSpec, heightMeasureSpec);
else super.onMeasure(widthMeasureSpec, widthMeasureSpec);
}
}

View file

@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.database.loaders;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.CursorLoader;
import org.thoughtcrime.securesms.database.DatabaseFactory;
public class RecentPhotosLoader extends CursorLoader {
public static Uri BASE_URL = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
private static final String[] PROJECTION = new String[] {
MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATE_MODIFIED,
MediaStore.Images.ImageColumns.ORIENTATION,
MediaStore.Images.ImageColumns.MIME_TYPE
};
private final Context context;
public RecentPhotosLoader(Context context) {
super(context);
this.context = context.getApplicationContext();
}
@Override
public Cursor loadInBackground() {
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, null, null,
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
}
}