package info.guardianproject.keanuapp.ui.conversation; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.FloatingActionButton; import android.support.v4.app.ActivityCompat; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.PagerSnapHelper; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.SnapHelper; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.ProgressBar; import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.request.RequestOptions; import com.github.barteksc.pdfviewer.PDFView; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import info.guardianproject.keanu.core.provider.Imps; import info.guardianproject.keanu.core.util.SecureMediaStore; import info.guardianproject.keanuapp.R; import info.guardianproject.keanuapp.ui.widgets.AudioRecorder; import info.guardianproject.keanuapp.ui.widgets.CircularPulseImageButton; import info.guardianproject.keanuapp.ui.widgets.MediaInfo; import info.guardianproject.keanuapp.ui.widgets.MessageViewHolder; import info.guardianproject.keanuapp.ui.widgets.PZSImageView; import info.guardianproject.keanuapp.ui.widgets.PdfViewActivity; import info.guardianproject.keanuapp.ui.widgets.StoryAudioPlayer; import info.guardianproject.keanuapp.ui.widgets.StoryExoPlayerManager; import info.guardianproject.keanuapp.ui.widgets.VideoViewActivity; import static info.guardianproject.keanu.core.KeanuConstants.LOG_TAG; /** * Created by N-Pex on 2019-03-28. */ public class StoryView extends ConversationView implements AudioRecorder.AudioRecorderListener { private final ProgressBar progressBar; private final SnapHelper snapHelper; private final SimpleExoPlayerView previewAudio; private final StoryAudioPlayer storyAudioPlayer; private final PlayerControlView storyAudioPlayerView; private AudioRecorder audioRecorder; private int currentPage = -1; private RecyclerView.ViewHolder currentPageViewHolder = null; private static final int AUTO_ADVANCE_TIMEOUT_IMAGE = 5000; // Milliseconds private static final int AUTO_ADVANCE_TIMEOUT_PDF = 5000; // Milliseconds /** * Set to true to automatically advance to next media item. For images this is after a set time, for video and audio when they are played. */ private boolean autoAdvance = true; private boolean waitingForMoreData = false; // Set to true if we get an auto advance event while on the last item // If this is set, we are in "preview audio" mode. private MediaInfo recordedAudio; private int audioLoaderId = -1; public StoryView(ConversationDetailActivity activity) { super(activity); final LinearLayoutManager llm = new LinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false); llm.setStackFromEnd(false); mHistory.setLayoutManager(llm); snapHelper = new PagerSnapHelper(); snapHelper.attachToRecyclerView(mHistory); progressBar = activity.findViewById(R.id.progress_horizontal); mHistory.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE) { if (currentPage != getCurrentPagePosition()) { // Only react on change setCurrentPage(); updateProgressCurrentPage(); autoAdvance = true; } } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { getHistoryView().removeCallbacks(advanceToNextRunnable); autoAdvance = false; } } @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); } }); mMicButton.setOnClickListener(null); mMicButton.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { captureAudioStart(); return true; } }); mMicButton.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (audioRecorder != null && audioRecorder.isAudioRecording() && (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL)) { captureAudioStop(); // Stop! } return false; } }); previewAudio = activity.findViewById(R.id.previewAudio); previewAudio.setVisibility(View.GONE); storyAudioPlayer = new StoryAudioPlayer(activity); storyAudioPlayerView = activity.findViewById(R.id.audioPlayerView); storyAudioPlayerView.setShowShuffleButton(false); storyAudioPlayerView.setShowMultiWindowTimeBar(true); storyAudioPlayerView.hide(); storyAudioPlayerView.setPlayer(storyAudioPlayer.getPlayer()); FloatingActionButton fabShowAudioPlayer = activity.findViewById(R.id.fabShowAudioPlayer); fabShowAudioPlayer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) fabShowAudioPlayer.getLayoutParams(); if (storyAudioPlayerView.getVisibility() == View.VISIBLE) { storyAudioPlayerView.hide(); lp.dodgeInsetEdges = Gravity.BOTTOM; fabShowAudioPlayer.setImageResource(R.drawable.ic_audio_24dp); } else { storyAudioPlayerView.show(); lp.dodgeInsetEdges = 0; fabShowAudioPlayer.setImageResource(R.drawable.ic_close_white_24dp); storyAudioPlayerView.getPlayer(); } fabShowAudioPlayer.setLayoutParams(lp); } }); mComposeMessage.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { autoAdvance = false; } @Override public void afterTextChanged(Editable s) { } }); activity.findViewById(R.id.composeMessage).clearFocus(); } @Override public boolean bindChat(long chatId, String address, String name) { // Destroy old audio loader if (audioLoaderId != -1) { mActivity.getSupportLoaderManager().destroyLoader(audioLoaderId); audioLoaderId = -1; } return super.bindChat(chatId, address, name); } @Override protected void onSendButtonClicked() { // If we have recorded audio, send that! if (recordedAudio != null) { // TODO Story - Send the audio! It's in recorderAudio.uri (not in VFS). Need to delete afterwards. ((StoryActivity)mActivity).sendMedia(recordedAudio.uri,"audio/m4a",true); setRecordedAudio(null); return; } super.onSendButtonClicked(); } @Override protected void sendMessage() { super.sendMessage(); View view = mActivity.getCurrentFocus(); if (view != null) { InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); } } private int getCurrentPagePosition() { View snapView = snapHelper.findSnapView(mHistory.getLayoutManager()); if (snapView != null) { return mHistory.getLayoutManager().getPosition(snapView); } return RecyclerView.NO_POSITION; } private void updateProgressCurrentPage() { if (currentPage >= 0) { progressBar.setProgress(currentPage + 1); } } private void setCurrentPage() { if (currentPageViewHolder != null) { if (currentPageViewHolder.itemView instanceof SimpleExoPlayerView) { Player player = ((SimpleExoPlayerView)currentPageViewHolder.itemView).getPlayer(); if (player != null) { player.setPlayWhenReady(false); } } } currentPage = getCurrentPagePosition(); currentPageViewHolder = (currentPage >= 0) ? getHistoryView().findViewHolderForAdapterPosition(currentPage) : null; if (currentPageViewHolder != null) { if (currentPageViewHolder.itemView instanceof SimpleExoPlayerView) { SimpleExoPlayerView playerView = (SimpleExoPlayerView) currentPageViewHolder.itemView; playerView.getPlayer().setPlayWhenReady(true); } else if (currentPageViewHolder.itemView instanceof PZSImageView) { getHistoryView().removeCallbacks(advanceToNextRunnable); getHistoryView().postDelayed(advanceToNextRunnable, AUTO_ADVANCE_TIMEOUT_IMAGE); } else if (currentPageViewHolder.itemView instanceof PDFView) { getHistoryView().removeCallbacks(advanceToNextRunnable); getHistoryView().postDelayed(advanceToNextRunnable, AUTO_ADVANCE_TIMEOUT_PDF); } } } @Override protected Loader<Cursor> createLoader() { String selection = "mime_type LIKE 'image/%' OR mime_type LIKE 'NOTaudio/%' OR mime_type LIKE 'video/%' OR mime_type LIKE 'application/pdf'"; CursorLoader loader = new CursorLoader(mActivity, mUri, null, selection, null, Imps.Messages.DEFAULT_SORT_ORDER); return loader; } @Override protected void loaderFinished() { // Dont call super, we don't want to scroll to last message //TODO - find last read message and scroll to that // If we are on the previously last message, advance? int n = getHistoryView().getAdapter().getItemCount(); if (currentPage == n - 1 - 1 && autoAdvance && waitingForMoreData) { waitingForMoreData = false; getHistoryView().post(new Runnable() { @Override public void run() { advanceToNext(); } }); } // Update audio cursor as well if (audioLoaderId == -1) { audioLoaderId = loaderId++; } mActivity.getSupportLoaderManager().restartLoader(audioLoaderId, null, new LoaderManager.LoaderCallbacks<Cursor>() { @NonNull @Override public Loader<Cursor> onCreateLoader(int i, @Nullable Bundle bundle) { String selection = "mime_type LIKE 'audio/%'"; return new CursorLoader(mActivity, mUri, null, selection, null, Imps.Messages.DEFAULT_SORT_ORDER); } @Override public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) { int uriColumn = cursor.getColumnIndexOrThrow(Imps.Messages.BODY); int mimeTypeColumn = cursor.getColumnIndexOrThrow(Imps.Messages.MIME_TYPE); storyAudioPlayer.updateCursor(cursor, mimeTypeColumn, uriColumn); } @Override public void onLoaderReset(@NonNull Loader<Cursor> loader) { } }); } @Override protected ConversationRecyclerViewAdapter createRecyclerViewAdapter() { return new StoryRecyclerViewAdapter(mActivity, null); } private void captureAudioStart() { mComposeMessage.setVisibility(View.INVISIBLE); // Start recording! if (audioRecorder == null) { audioRecorder = new AudioRecorder(previewAudio.getContext(), this); } else if (audioRecorder.isAudioRecording()) { audioRecorder.stopAudioRecording(true); } StoryExoPlayerManager.recordAudio(audioRecorder, previewAudio); audioRecorder.startAudioRecording(); ((CircularPulseImageButton)mMicButton).setAnimating(true); } private void captureAudioStop() { ((CircularPulseImageButton)mMicButton).setAnimating(false); if (audioRecorder != null && audioRecorder.isAudioRecording()) { audioRecorder.stopAudioRecording(false); } } private void setRecordedAudio(MediaInfo recordedAudio) { this.recordedAudio = recordedAudio; if (this.recordedAudio != null) { mMicButton.setVisibility(View.GONE); mSendButton.setVisibility(View.VISIBLE); Drawable d = ActivityCompat.getDrawable(mActivity, R.drawable.ic_close_white_24dp).mutate(); DrawableCompat.setTint(d, Color.GRAY); mActivity.getSupportActionBar().setHomeAsUpIndicator(d); mActivity.setBackButtonHandler(new View.OnClickListener() { @Override public void onClick(View v) { StoryExoPlayerManager.stop(previewAudio); setRecordedAudio(null); } }); } else { mActivity.getSupportActionBar().setHomeAsUpIndicator(null); mActivity.setBackButtonHandler(null); previewAudio.setVisibility(View.GONE); mComposeMessage.setVisibility(View.VISIBLE); mComposeMessage.setText(""); mMicButton.setVisibility(View.VISIBLE); } } @Override public void onAudioRecorded(Uri uri) { setRecordedAudio(new MediaInfo(uri, "audio/mp4")); StoryExoPlayerManager.load(recordedAudio, previewAudio, true); } class StoryRecyclerViewAdapter extends ConversationRecyclerViewAdapter implements PZSImageView.PSZImageViewImageMatrixListener { private final RequestOptions imageRequestOptions; public StoryRecyclerViewAdapter(Activity context, Cursor c) { super(context, c); imageRequestOptions = new RequestOptions().centerInside().diskCacheStrategy(DiskCacheStrategy.NONE).error(R.drawable.broken_image_large); } @Override public int getItemCount() { int count = super.getItemCount(); if (progressBar != null) { progressBar.setMax(count); updateProgressCurrentPage(); } return count; } @Override public int getItemViewType(int position) { try { Cursor c = getCursor(); c.moveToPosition(position); String mime = c.getString(mMimeTypeColumn); if (!TextUtils.isEmpty(mime)) { if (mime.startsWith("audio/") || mime.startsWith("video/")) { return 1; } else if (mime.contentEquals("application/pdf")) { return 2; } } } catch (Exception ignored) { } return 0; // Image } @Override public MessageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View mediaView = null; Context context = parent.getContext(); switch (viewType) { case 2: mediaView = LayoutInflater.from(context).inflate(R.layout.story_viewer_file_info, parent, false); break; case 1: SimpleExoPlayerView playerView = (SimpleExoPlayerView) LayoutInflater.from(context).inflate(R.layout.story_viewer_exo_player, parent, false); mediaView = playerView; mediaView.setBackgroundColor(0xff333333); break; case 0: default: PZSImageView imageView = new PZSImageView(context); mediaView = imageView; imageView.setBackgroundColor(0xff333333); break; } RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mediaView.setLayoutParams(lp); MessageViewHolder mvh = new MessageViewHolder(mediaView); mvh.setLayoutInflater(LayoutInflater.from(parent.getContext())); mvh.setOnImageClickedListener(this); return mvh; } @Override public void onBindViewHolder(MessageViewHolder viewHolder, Cursor cursor) { int viewType = getItemViewType(cursor.getPosition()); Context context = viewHolder.itemView.getContext(); try { String mime = cursor.getString(mMimeTypeColumn); Uri uri = Uri.parse(cursor.getString(mBodyColumn)); switch (viewType) { case 2: TextView infoView = (TextView)viewHolder.itemView.findViewById(R.id.text); infoView.setText(uri.getLastPathSegment()); viewHolder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(context, PdfViewActivity.class); intent.setDataAndType(uri,mime); context.startActivity(intent); } }); break; case 1: SimpleExoPlayerView playerView = (SimpleExoPlayerView)viewHolder.itemView; MediaInfo mediaInfo = new MediaInfo(uri, mime); StoryExoPlayerManager.load(mediaInfo, playerView, false); playerView.getPlayer().addListener(new Player.EventListener() { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED) { advanceToNext(); } } }); break; case 0: default: PZSImageView imageView = (PZSImageView)viewHolder.itemView; try { imageView.setMatrixListener(this); if (SecureMediaStore.isVfsUri(uri)) { info.guardianproject.iocipher.File fileMedia = new info.guardianproject.iocipher.File(uri.getPath()); if (fileMedia.exists()) { Glide.with(context) .asBitmap() .apply(imageRequestOptions) .load(new info.guardianproject.iocipher.FileInputStream(fileMedia)) .into(imageView); } else { Glide.with(context) .asBitmap() .apply(imageRequestOptions) .load(R.drawable.broken_image_large) .into(imageView); } } else { Glide.with(context) .asBitmap() .apply(imageRequestOptions) .load(uri) .into(imageView); } } catch (Throwable t) { // may run Out Of Memory Log.w(LOG_TAG, "unable to load thumbnail: " + t); } break; } } catch (Exception e) { e.printStackTrace(); } if (currentPage == -1) { currentPage = 0; getHistoryView().post(new Runnable() { @Override public void run() { setCurrentPage(); } }); } } @Override public void onViewRecycled(@NonNull MessageViewHolder holder) { if (holder.itemView instanceof PDFView) { final PDFView pdfView = (PDFView)holder.itemView; pdfView.post(new Runnable() { @Override public void run() { pdfView.recycle(); } }); } else if (holder.itemView instanceof SimpleExoPlayerView) { ((SimpleExoPlayerView)holder.itemView).getPlayer().stop(true); ((SimpleExoPlayerView)holder.itemView).getPlayer().release(); ((SimpleExoPlayerView)holder.itemView).setPlayer(null); } super.onViewRecycled(holder); } @Override public void onImageMatrixSet(PZSImageView view, int imageWidth, int imageHeight, Matrix imageMatrix) { //TODO } } private Runnable advanceToNextRunnable = new Runnable() { @Override public void run() { advanceToNext(); } }; private void advanceToNext() { if (autoAdvance) { if (currentPage >= 0) { // At end of data? if ((currentPage + 1) < getHistoryView().getAdapter().getItemCount()) { waitingForMoreData = false; getHistoryView().smoothScrollToPosition(currentPage + 1); } else { waitingForMoreData = true; } } } } public void pause () { storyAudioPlayer.getPlayer().stop(); } }