| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package io.v.syncslides; |
| |
| import android.content.Context; |
| import android.content.res.Configuration; |
| import android.graphics.Bitmap; |
| import android.os.Bundle; |
| import android.support.v4.app.Fragment; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.Menu; |
| import android.view.MenuInflater; |
| import android.view.MenuItem; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.inputmethod.InputMethodManager; |
| import android.widget.EditText; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import io.v.syncslides.db.DB; |
| import io.v.syncslides.model.DynamicList; |
| import io.v.syncslides.model.ListListener; |
| import io.v.syncslides.model.Session; |
| import io.v.syncslides.model.Slide; |
| import io.v.v23.verror.VException; |
| |
| /** |
| * Provides both the presenter and audience views for navigating through a presentation. |
| * Instantiated by the PresentationActivity along with other views/fragments of the presentation |
| * to make transitions between them seamless. |
| */ |
| public class NavigateFragment extends Fragment { |
| private static final String TAG = "NavigateFragment"; |
| private static final String SESSION_ID_KEY = "session_id_key"; |
| |
| private Session mSession; |
| private int mSlideNum = 0; |
| private SlideNumberListener mSlideNumberListener = new SlideNumberListener(); |
| private ListListener mSlideListListener = new SlideListListener(); |
| private ImageView mPrevThumb; |
| private ImageView mNextThumb; |
| private ImageView mCurrentSlide; |
| private TextView mSlideNumText; |
| private EditText mNotes; |
| private boolean mEditing; |
| private DynamicList<Slide> mSlides; |
| |
| public static NavigateFragment newInstance(String sessionId) { |
| NavigateFragment fragment = new NavigateFragment(); |
| Bundle args = new Bundle(); |
| args.putString(SESSION_ID_KEY, sessionId); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| // When editing notes, display a menu with "Save". |
| setHasOptionsMenu(true); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| Bundle bundle = savedInstanceState; |
| if (bundle == null) { |
| bundle = getArguments(); |
| } |
| String sessionId = bundle.getString(SESSION_ID_KEY); |
| try { |
| mSession = DB.Singleton.get().getSession(sessionId); |
| } catch (VException e) { |
| handleFatalError("Failed to fetch Session", e); |
| } |
| |
| final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false); |
| |
| // mFabSync = rootView.findViewById(R.id.audience_sync_fab); |
| // if (((PresentationActivity) getActivity()).getSynced() || mRole != Role.AUDIENCE) { |
| // mFabSync.setVisibility(View.INVISIBLE); |
| // } else { |
| // mFabSync.setVisibility(View.VISIBLE); |
| // } |
| // |
| // mFabSync.setOnClickListener(new NavigateClickListener() { |
| // @Override |
| // public void onClick(View v) { |
| // super.onClick(v); |
| // sync(); |
| // mFabSync.setVisibility(View.INVISIBLE); |
| // } |
| // }); |
| View.OnClickListener previousSlideListener = new NavigateClickListener() { |
| @Override |
| void onNavigate() { |
| previousSlide(); |
| } |
| }; |
| View arrowBack = rootView.findViewById(R.id.arrow_back); |
| arrowBack.setOnClickListener(previousSlideListener); |
| mPrevThumb = (ImageView) rootView.findViewById(R.id.prev_thumb); |
| mPrevThumb.setOnClickListener(previousSlideListener); |
| |
| View.OnClickListener nextSlideListener = new NavigateClickListener() { |
| @Override |
| void onNavigate() { |
| nextSlide(); |
| } |
| }; |
| // Show either the arrowForward or the FAB but not both. |
| View arrowForward = rootView.findViewById(R.id.arrow_forward); |
| View fabForward = rootView.findViewById(R.id.primary_navigation_fab); |
| // if (mRole == Role.PRESENTER) { |
| // arrowForward.setVisibility(View.INVISIBLE); |
| // fabForward.setOnClickListener(nextSlideListener); |
| // } else { |
| fabForward.setVisibility(View.INVISIBLE); |
| arrowForward.setOnClickListener(nextSlideListener); |
| // } |
| mNextThumb = (ImageView) rootView.findViewById(R.id.next_thumb); |
| mNextThumb.setOnClickListener(nextSlideListener); |
| // mQuestions = (ImageView) rootView.findViewById(R.id.questions); |
| // // TODO(kash): Hide the mQuestions button if mRole == BROWSER. |
| // mQuestions.setOnClickListener(new NavigateClickListener() { |
| // @Override |
| // public void onClick(View v) { |
| // super.onClick(v); |
| // questionButton(); |
| // } |
| // }); |
| mCurrentSlide = (ImageView) rootView.findViewById(R.id.slide_current_medium); |
| mCurrentSlide.setOnClickListener(new NavigateClickListener() { |
| @Override |
| public void onNavigate() { |
| // TODO(kash): Disallow presenter from switching to fullscreen. |
| ((PresentationActivity) getActivity()).showFullscreenSlide(); |
| } |
| }); |
| |
| mSlideNumText = (TextView) rootView.findViewById(R.id.slide_num_text); |
| mNotes = (EditText) rootView.findViewById(R.id.notes); |
| mNotes.setOnFocusChangeListener((v, hasFocus) -> { |
| if (hasFocus) { |
| ((PresentationActivity) getActivity()).getSupportActionBar().show(); |
| mEditing = true; |
| getActivity().invalidateOptionsMenu(); |
| // We don't want the presentation to advance while the user |
| // is editing the notes. Force the app to stay on this slide. |
| try { |
| mSession.setLocalSlideNum(mSlideNum); |
| } catch (VException e) { |
| handleFatalError("Could not set local slide num", e); |
| } |
| } |
| }); |
| |
| // The parent of mNotes needs to be focusable in order to clear focus |
| // from mNotes when done editing. We set the attributes in code rather |
| // than in XML because it is too easy to add an extra level of layout |
| // in XML and forget to add these attributes. |
| ViewGroup parent = (ViewGroup) mNotes.getParent(); |
| parent.setFocusable(true); |
| parent.setClickable(true); |
| parent.setFocusableInTouchMode(true); |
| |
| // View slideListIcon = rootView.findViewById(R.id.slide_list); |
| // slideListIcon.setOnClickListener(new NavigateClickListener() { |
| // @Override |
| // public void onClick(View v) { |
| // super.onClick(v); |
| // if (mRole == Role.AUDIENCE) { |
| // ((PresentationActivity) getActivity()).showSlideList(); |
| // } else { |
| // getActivity().getSupportFragmentManager().popBackStack(); |
| // } |
| // } |
| // }); |
| // mQuestionsNum = (TextView) rootView.findViewById(R.id.questions_num); |
| // // Start off invisible for everyone. If there are questions, this |
| // // will be set to visible in the mDB.getQuestionerList() callback. |
| // mQuestionsNum.setVisibility(View.INVISIBLE); |
| // |
| // mDB = DB.Singleton.get(getActivity().getApplicationContext()); |
| // mDB.getSlides(mDeckId, new DB.Callback<List<Slide>>() { |
| // @Override |
| // public void done(List<Slide> slides) { |
| // mSlides = slides; |
| // // The CurrentSlideListener could have been notified while we were waiting for |
| // // the slides to load. |
| // if (mLoadingCurrentSlide != -1) { |
| // currentSlideChanged(mLoadingCurrentSlide); |
| // } |
| // updateView(); |
| // } |
| // }); |
| // if (((PresentationActivity) getActivity()).getSynced()) { |
| // sync(); |
| // } else { |
| // unsync(); |
| // } |
| |
| return rootView; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| ((PresentationActivity) getActivity()).setUiImmersive(true); |
| mSlides = mSession.getSlides(); |
| mSlides.addListener(mSlideListListener); |
| mSession.addSlideNumberListener(mSlideNumberListener); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| ((PresentationActivity) getActivity()).setUiImmersive(false); |
| mSession.removeSlideNumberListener(mSlideNumberListener); |
| mSlides.removeListener(mSlideListListener); |
| mSlides = null; |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putString(SESSION_ID_KEY, mSession.getId()); |
| } |
| |
| @Override |
| public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { |
| if (mEditing) { |
| inflater.inflate(R.menu.edit_notes, menu); |
| } |
| super.onCreateOptionsMenu(menu, inflater); |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case R.id.action_save: |
| saveNotes(); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Advances to the next slide, if there is one, and updates the UI. |
| */ |
| private void nextSlide() { |
| if (mSlideNum == -1) { |
| // Wait until the state has loaded before letting the user move around. |
| return; |
| } |
| if (mSlideNum < mSession.getSlides().getItemCount() - 1) { |
| try { |
| mSession.setLocalSlideNum(mSlideNum + 1); |
| } catch (VException e) { |
| handleError("Failed to advance", e); |
| } |
| } |
| } |
| |
| /** |
| * Goes back to the previous slide, if there is one, and updates the UI. |
| */ |
| private void previousSlide() { |
| if (mSlideNum == -1) { |
| // Wait until the state has loaded before letting the user move around. |
| return; |
| } |
| if (mSlideNum > 0) { |
| try { |
| mSession.setLocalSlideNum(mSlideNum - 1); |
| } catch (VException e) { |
| handleError("Failed to go back", e); |
| } |
| } |
| } |
| |
| private void updateView() { |
| if (mSlideNum < 0 || mSlideNum >= mSlides.getItemCount()) { |
| // Still loading. |
| return; |
| } |
| if (mSlideNum > 0) { |
| setThumbBitmap(mPrevThumb, mSlides.get(mSlideNum - 1).getThumb()); |
| } else { |
| setThumbNull(mPrevThumb); |
| } |
| // TODO(kash): Switch to full size image. |
| mCurrentSlide.setImageBitmap(mSlides.get(mSlideNum).getThumb()); |
| if (mSlideNum == mSlides.getItemCount() - 1) { |
| setThumbNull(mNextThumb); |
| } else { |
| setThumbBitmap(mNextThumb, mSlides.get(mSlideNum + 1).getThumb()); |
| } |
| if (!mSlides.get(mSlideNum).getNotes().equals("")) { |
| mNotes.setText(mSlides.get(mSlideNum).getNotes()); |
| } else { |
| mNotes.getText().clear(); |
| } |
| mSlideNumText.setText( |
| String.valueOf(mSlideNum + 1) + " of " + String.valueOf(mSlides.getItemCount())); |
| } |
| |
| private void setThumbBitmap(ImageView thumb, Bitmap bitmap) { |
| thumb.setImageBitmap(bitmap); |
| // In landscape, the height is dependent on the image size. However, if the |
| // image was null, the height is hardcoded to 9/16 of the width in setThumbNull. |
| // This resets it to the actual image size. |
| if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams(); |
| thumbParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; |
| } |
| } |
| |
| private void setThumbNull(ImageView thumb) { |
| thumb.setImageDrawable(null); |
| // In landscape, the height is dependent on the image size. Because we don't have an |
| // image, assume all of the images are 16:9. The width is fixed, so we can calculate |
| // the expected height. |
| if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { |
| ViewGroup grandparent = (ViewGroup) thumb.getParent().getParent(); |
| ViewGroup.LayoutParams thumbParams = thumb.getLayoutParams(); |
| thumbParams.height = (int) ((9 / 16.0) * grandparent.getMeasuredWidth()); |
| } |
| } |
| |
| /** |
| * If the user is editing the text field and the text has changed, save the |
| * notes in Syncbase. That will trigger a notification that the slide has |
| * changed and the UI will refresh. |
| */ |
| public void saveNotes() { |
| final String notes = mNotes.getText().toString(); |
| if (mEditing && (!notes.equals(mSlides.get(mSlideNum).getNotes()))) { |
| try { |
| mSession.setNotes(mSlideNum, notes); |
| } catch (VException e) { |
| handleError("Could not save notes", e); |
| } |
| } |
| mNotes.clearFocus(); |
| mEditing = false; |
| InputMethodManager inputManager = |
| (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); |
| if (getActivity().getCurrentFocus() != null) { |
| inputManager.hideSoftInputFromWindow( |
| getActivity().getCurrentFocus().getWindowToken(), |
| InputMethodManager.HIDE_NOT_ALWAYS); |
| } |
| ((PresentationActivity) getActivity()).setUiImmersive(true); |
| } |
| |
| private void handleError(String msg, Throwable throwable) { |
| Log.e(TAG, msg + ": " + Log.getStackTraceString(throwable)); |
| Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); |
| } |
| |
| private void handleFatalError(String msg, Throwable throwable) { |
| handleError(msg, throwable); |
| getActivity().finish(); |
| } |
| |
| /** |
| * Updates the view whenever the list of slides changes. |
| */ |
| private class SlideListListener implements ListListener { |
| @Override |
| public void notifyDataSetChanged() { |
| updateView(); |
| } |
| |
| @Override |
| public void notifyItemChanged(int position) { |
| updateView(); |
| } |
| |
| @Override |
| public void notifyItemInserted(int position) { |
| updateView(); |
| } |
| |
| @Override |
| public void notifyItemRemoved(int position) { |
| updateView(); |
| } |
| |
| @Override |
| public void onError(Exception e) { |
| handleFatalError("Error watching slide list", e); |
| } |
| } |
| |
| private class SlideNumberListener implements Session.SlideNumberListener { |
| @Override |
| public void onChange(int slideNum) { |
| mSlideNum = slideNum; |
| updateView(); |
| } |
| |
| @Override |
| public void onError(Exception e) { |
| handleFatalError("Error listening to slide number changes", e); |
| } |
| } |
| |
| /** |
| * If the user is editing notes and then clicks anywhere else on the screen, |
| * we want that action to save the notes first. Using this class forces |
| * that behavior. |
| */ |
| private abstract class NavigateClickListener implements View.OnClickListener { |
| @Override |
| public final void onClick(View v) { |
| saveNotes(); |
| onNavigate(); |
| } |
| |
| /** |
| * Called when there is a click. |
| */ |
| abstract void onNavigate(); |
| } |
| } |