| // 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.android.apps.syncslides; |
| |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.graphics.Bitmap; |
| import android.os.Bundle; |
| import android.support.design.widget.Snackbar; |
| import android.support.v4.app.DialogFragment; |
| import android.support.v4.app.Fragment; |
| import android.support.v4.content.ContextCompat; |
| import android.view.Gravity; |
| 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.FrameLayout; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| import android.widget.Toast; |
| |
| import java.util.List; |
| |
| import io.v.android.apps.syncslides.db.DB; |
| import io.v.android.apps.syncslides.model.Question; |
| import io.v.android.apps.syncslides.model.Role; |
| import io.v.android.apps.syncslides.model.Slide; |
| |
| /** |
| * 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 DECK_ID_KEY = "deck_id_key"; |
| private static final String PRESENTATION_ID_KEY = "presentation_id_key"; |
| private static final String SLIDE_NUM_KEY = "slide_num_key"; |
| private static final String ROLE_KEY = "role_key"; |
| private static final int DIALOG_REQUEST_CODE = 23; |
| |
| // TODO(afergan): Move state variables to activity. |
| private String mDeckId; |
| private String mPresentationId; |
| /** |
| * The slide number for the live presentation, if any. |
| */ |
| private int mCurrentSlideNum; |
| /** |
| * While mSlides is loading, we can't validate any slide numbers coming from DB. |
| * We hold them here until mSlides finishes loading. |
| */ |
| private int mLoadingCurrentSlide; |
| /** |
| * The slide number that the user is viewing. This will be different from mCurrentSlideNum |
| * if mRole == AUDIENCE and the user went forwards or backwards in the deck. |
| */ |
| private int mUserSlideNum; |
| private ImageView mPrevThumb; |
| private ImageView mNextThumb; |
| private ImageView mCurrentSlide; |
| private ImageView mQuestions; |
| private View mFabSync; |
| private TextView mQuestionsNum; |
| private EditText mNotes; |
| private List<Slide> mSlides; |
| private Role mRole; |
| private List<Question> mQuestionList; |
| private DB.QuestionListener mQuestionListener; |
| private boolean mEditing; |
| private int mQuestionerPosition; |
| private boolean mSynced; |
| private DB.CurrentSlideListener mCurrentSlideListener; |
| private DB mDB; |
| private TextView mSlideNumText; |
| |
| public static NavigateFragment newInstance( |
| String deckId, String presentationId, int slideNum, Role role) { |
| NavigateFragment fragment = new NavigateFragment(); |
| Bundle args = new Bundle(); |
| args.putString(DECK_ID_KEY, deckId); |
| args.putString(PRESENTATION_ID_KEY, presentationId); |
| args.putInt(SLIDE_NUM_KEY, slideNum); |
| args.putSerializable(ROLE_KEY, role); |
| fragment.setArguments(args); |
| return fragment; |
| } |
| |
| @Override |
| public void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setHasOptionsMenu(true); |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, |
| Bundle savedInstanceState) { |
| Bundle args; |
| if (savedInstanceState != null) { |
| args = savedInstanceState; |
| } else { |
| args = getArguments(); |
| } |
| mDeckId = args.getString(DECK_ID_KEY); |
| mPresentationId = args.getString(PRESENTATION_ID_KEY); |
| mLoadingCurrentSlide = -1; |
| mUserSlideNum = args.getInt(SLIDE_NUM_KEY); |
| mRole = (Role) args.get(ROLE_KEY); |
| if (((PresentationActivity) getActivity()).getSynced()) { |
| sync(); |
| } else { |
| unsync(); |
| } |
| final View rootView = inflater.inflate(R.layout.fragment_navigate, container, false); |
| mFabSync = rootView.findViewById(R.id.audience_sync_fab); |
| if (mSynced || 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 |
| public void onClick(View v) { |
| super.onClick(v); |
| 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 |
| public void onClick(View v) { |
| super.onClick(v); |
| 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 onClick(View v) { |
| super.onClick(v); |
| if (mRole == Role.AUDIENCE || mRole == Role.BROWSER) { |
| ((PresentationActivity) getActivity()).showFullscreenSlide(mUserSlideNum); |
| } |
| } |
| }); |
| |
| mSlideNumText = (TextView) rootView.findViewById(R.id.slide_num_text); |
| mNotes = (EditText) rootView.findViewById(R.id.notes); |
| mNotes.setOnFocusChangeListener(new View.OnFocusChangeListener() { |
| @Override |
| public void onFocusChange(View v, boolean hasFocus) { |
| ((PresentationActivity) getActivity()).getSupportActionBar().show(); |
| mEditing = hasFocus; |
| getActivity().invalidateOptionsMenu(); |
| unsync(); |
| } |
| }); |
| |
| // 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); |
| } else { |
| updateView(); |
| } |
| } |
| }); |
| |
| return rootView; |
| } |
| |
| @Override |
| public void onStart() { |
| super.onStart(); |
| ((PresentationActivity) getActivity()).setUiImmersive(true); |
| if (mRole == Role.AUDIENCE) { |
| mCurrentSlideListener = new DB.CurrentSlideListener() { |
| @Override |
| public void onChange(int slideNum) { |
| NavigateFragment.this.currentSlideChanged(slideNum); |
| } |
| }; |
| mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener); |
| } |
| if (mRole == Role.PRESENTER) { |
| mQuestionListener = new DB.QuestionListener() { |
| @Override |
| public void onChange(List<Question> questions) { |
| mQuestionList = questions; |
| if (mQuestionList.size() > 0) { |
| mQuestionsNum.setVisibility(View.VISIBLE); |
| mQuestionsNum.setText(String.valueOf(mQuestionList.size())); |
| } else { |
| mQuestionsNum.setVisibility(View.INVISIBLE); |
| } |
| } |
| }; |
| mDB.setQuestionListener(mDeckId, mPresentationId, mQuestionListener); |
| } |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| ((PresentationActivity) getActivity()).setUiImmersive(false); |
| if (mRole == Role.AUDIENCE) { |
| mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener); |
| } |
| if (mRole == Role.PRESENTER) { |
| mDB.removeQuestionListener(mDeckId, mPresentationId, mQuestionListener); |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putString(DECK_ID_KEY, mDeckId); |
| outState.putString(PRESENTATION_ID_KEY, mPresentationId); |
| outState.putInt(SLIDE_NUM_KEY, mUserSlideNum); |
| outState.putSerializable(ROLE_KEY, mRole); |
| } |
| |
| @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; |
| } |
| |
| public void saveNotes() { |
| if (mEditing) { |
| Toast.makeText(getContext(), "Saving notes", Toast.LENGTH_SHORT).show(); |
| mNotes.clearFocus(); |
| InputMethodManager inputManager = |
| (InputMethodManager) getContext(). |
| getSystemService(Context.INPUT_METHOD_SERVICE); |
| inputManager.hideSoftInputFromWindow( |
| getActivity().getCurrentFocus().getWindowToken(), |
| InputMethodManager.HIDE_NOT_ALWAYS); |
| ((PresentationActivity) getActivity()).setUiImmersive(true); |
| } |
| } |
| |
| private void unsync() { |
| if (mRole == Role.AUDIENCE && mSynced) { |
| mSynced = false; |
| ((PresentationActivity) getActivity()).setUnsynced(); |
| mFabSync.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| private void sync() { |
| mSynced = true; |
| mUserSlideNum = mCurrentSlideNum; |
| ((PresentationActivity) getActivity()).setSynced(); |
| updateView(); |
| } |
| |
| /** |
| * Advances to the next slide, if there is one, and updates the UI. |
| */ |
| private void nextSlide() { |
| if (mSlides == null) { |
| // Wait until the slides have loaded before letting the user move around. |
| return; |
| } |
| if (mUserSlideNum < mSlides.size() - 1) { |
| mUserSlideNum++; |
| if (mRole == Role.PRESENTER) { |
| mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum); |
| } |
| updateView(); |
| unsync(); |
| } |
| } |
| |
| /** |
| * Goes back to the previous slide, if there is one, and updates the UI. |
| */ |
| private void previousSlide() { |
| if (mSlides == null) { |
| // Wait until the slides have loaded before letting the user move around. |
| return; |
| } |
| if (mUserSlideNum > 0) { |
| mUserSlideNum--; |
| if (mRole == Role.PRESENTER) { |
| mDB.setCurrentSlide(mDeckId, mPresentationId, mUserSlideNum); |
| } |
| updateView(); |
| unsync(); |
| } |
| } |
| |
| private void currentSlideChanged(int slideNum) { |
| if (mSlides == null) { |
| // We can't validate that slideNum is within the bounds of mSlides. Hold it off |
| // to the side until mSlides finishes loading. |
| mLoadingCurrentSlide = slideNum; |
| return; |
| } |
| if (slideNum < 0 || slideNum >= mSlides.size()) { |
| return; |
| } |
| mCurrentSlideNum = slideNum; |
| if (mSynced) { |
| mUserSlideNum = slideNum; |
| updateView(); |
| } |
| } |
| |
| /** |
| * When the user presses the icon, add the user's identity to the presenter's question queue. |
| * If presenter presses the button, get a list of users who are asking questions. |
| */ |
| private void questionButton() { |
| DB db = DB.Singleton.get(getActivity().getApplicationContext()); |
| switch (mRole) { |
| case AUDIENCE: |
| db.askQuestion(mDeckId, mPresentationId, "Audience member", "#1"); |
| Toast toast = Toast.makeText(getActivity().getApplicationContext(), |
| "You have been added to the Q&A queue.", Toast.LENGTH_LONG); |
| toast.show(); |
| break; |
| case PRESENTER: |
| if (mQuestionList == null || mQuestionList.size() == 0) { |
| break; |
| } |
| // TODO(kash): It would be better to pass the entire Question to the dialog. |
| String[] questioners = new String[mQuestionList.size()]; |
| for (int i = 0; i < mQuestionList.size(); i++) { |
| questioners[i] = mQuestionList.get(i).getFirstName() + " " |
| + mQuestionList.get(i).getLastName(); |
| } |
| DialogFragment dialog = QuestionDialogFragment.newInstance(questioners); |
| dialog.setTargetFragment(this, DIALOG_REQUEST_CODE); |
| dialog.show(getFragmentManager(), "QuestionerDialogFragment"); |
| break; |
| case BROWSER: |
| // Do nothing. |
| break; |
| } |
| } |
| |
| private void updateView() { |
| if (mSlides == null) { |
| // We can't do anything until the slides have loaded. |
| return; |
| } |
| if (mUserSlideNum > 0) { |
| setThumbBitmap(mPrevThumb, mSlides.get(mUserSlideNum - 1).getImage()); |
| } else { |
| setThumbNull(mPrevThumb); |
| } |
| mCurrentSlide.setImageBitmap(mSlides.get(mUserSlideNum).getImage()); |
| if (mUserSlideNum == mSlides.size() - 1) { |
| setThumbNull(mNextThumb); |
| } else { |
| setThumbBitmap(mNextThumb, mSlides.get(mUserSlideNum + 1).getImage()); |
| } |
| if (!mSlides.get(mUserSlideNum).getNotes().equals("")) { |
| mNotes.setText(mSlides.get(mUserSlideNum).getNotes()); |
| } else { |
| mNotes.getText().clear(); |
| } |
| mSlideNumText.setText( |
| String.valueOf(mUserSlideNum + 1) + " of " + String.valueOf(mSlides.size())); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (requestCode == DIALOG_REQUEST_CODE) { |
| // TODO(afergan): Using the position is insufficient if the list of questioners changes |
| // while the dialog is showing. |
| mQuestionerPosition = data.getIntExtra( |
| QuestionDialogFragment.QUESTION_BUNDLE_KEY, 0); |
| handoffControl(); |
| } |
| } |
| |
| /** |
| * Handoff control of the presentation to a questioner. A snackbar displays the status while |
| * the audience member is in control, and control ends when the presenter presses the action |
| * text. |
| */ |
| private void handoffControl() { |
| //TODO(afergan): Change slide presenter to the audience member at mQuestionerPosition. |
| View.OnClickListener snackbarClickListener = new NavigateClickListener() { |
| @Override |
| public void onClick(View v) { |
| super.onClick(v); |
| //TODO(afergan): End handoff, presenter regains control of presentation. |
| } |
| }; |
| |
| ((PresentationActivity) getActivity()).setUiImmersive(true); |
| Snackbar snack = Snackbar.make(getView(), getResources().getString( |
| R.string.handoff_message) + " " |
| + mQuestionList.get(mQuestionerPosition).getFirstName() + " " |
| + mQuestionList.get(mQuestionerPosition).getLastName(), |
| Snackbar.LENGTH_INDEFINITE) |
| .setAction(getResources().getString(R.string.end_handoff), |
| snackbarClickListener) |
| .setActionTextColor(ContextCompat.getColor(getContext(), R.color.action_orange)); |
| |
| // Needed to set the location of the snackbar (default is bottom center, which hides buttons |
| // in landscape mode). |
| View view = snack.getView(); |
| FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) view.getLayoutParams(); |
| params.gravity = Gravity.RIGHT | Gravity.BOTTOM; |
| view.setLayoutParams(params); |
| snack.show(); |
| } |
| |
| 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()); |
| } |
| } |
| |
| public class NavigateClickListener implements View.OnClickListener { |
| @Override |
| public void onClick(View v) { |
| saveNotes(); |
| } |
| } |
| } |