Java tutorial
// 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.db.VPerson; import io.v.android.apps.syncslides.misc.V23Manager; import io.v.android.apps.syncslides.model.Question; import io.v.android.apps.syncslides.model.Role; import io.v.android.apps.syncslides.model.Slide; import io.v.v23.security.Blessings; /** * 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 mDriving = false; private DB.DriverListener mDriverListener; private boolean mEditing; private String mQuestionId; 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; mCurrentSlideNum = mUserSlideNum = args.getInt(SLIDE_NUM_KEY); mRole = (Role) args.get(ROLE_KEY); 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 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); } updateView(); } }); if (((PresentationActivity) getActivity()).getSynced()) { sync(); } else { unsync(); } return rootView; } @Override public void onStart() { super.onStart(); ((PresentationActivity) getActivity()).setUiImmersive(true); mCurrentSlideListener = new DB.CurrentSlideListener() { @Override public void onChange(int slideNum) { NavigateFragment.this.currentSlideChanged(slideNum); } }; mDB.addCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener); if (mRole == Role.AUDIENCE) { final Blessings blessings = V23Manager.Singleton.get().getBlessings(); mDriverListener = new DB.DriverListener() { @Override public void onChange(VPerson driver) { if (driver != null && driver.getBlessing().equals(blessings.toString())) { mDriving = true; } else { mDriving = false; } } }; mDB.setDriverListener(mDeckId, mPresentationId, mDriverListener); } 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); mDB.removeCurrentSlideListener(mDeckId, mPresentationId, mCurrentSlideListener); if (mRole == Role.AUDIENCE) { mDB.removeDriverListener(mDeckId, mPresentationId, mDriverListener); } 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; } /** * If the user is editing the text field and the text has changed, save the * notes locally and in Syncbase. */ public void saveNotes() { final String notes = mNotes.getText().toString(); if (mEditing && (!notes.equals(mSlides.get(mUserSlideNum).getNotes()))) { toast("Saving notes"); mSlides.get(mUserSlideNum).setNotes(notes); mDB.setSlideNotes(mDeckId, mUserSlideNum, notes); } mNotes.clearFocus(); 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 unsync() { if (mRole == Role.AUDIENCE && ((PresentationActivity) getActivity()).getSynced()) { ((PresentationActivity) getActivity()).setUnsynced(); mFabSync.setVisibility(View.VISIBLE); } } private void sync() { 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 || mDriving) { 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 || mDriving) { 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 (((PresentationActivity) getActivity()).getSynced()) { 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, SignInActivity.getUserName(getActivity())); toast("You have been added to the Q&A queue."); break; case PRESENTER: if (mQuestionList == null || mQuestionList.size() == 0) { break; } DialogFragment dialog = QuestionDialogFragment.newInstance(mQuestionList); 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).getThumb()); } else { setThumbNull(mPrevThumb); } mCurrentSlide.setImageBitmap(mSlides.get(mUserSlideNum).getImage()); if (mUserSlideNum == mSlides.size() - 1) { setThumbNull(mNextThumb); } else { setThumbBitmap(mNextThumb, mSlides.get(mUserSlideNum + 1).getThumb()); } 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) { mQuestionId = data.getStringExtra(QuestionDialogFragment.QUESTION_ID_KEY); 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() { Question handoff = null; for (Question question : mQuestionList) { if (question.getId().equals(mQuestionId)) { handoff = question; break; } } if (handoff == null) { toast("No such question"); return; } sync(); mDB.handoffQuestion(mDeckId, mPresentationId, handoff.getId()); View.OnClickListener snackbarClickListener = new NavigateClickListener() { @Override public void onClick(View v) { super.onClick(v); mDB.resumeControl(mDeckId, mPresentationId); } }; ((PresentationActivity) getActivity()).setUiImmersive(true); Snackbar snack = Snackbar .make(getView(), getResources().getString(R.string.handoff_message) + " " + handoff.getName(), 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()); } } private void toast(String message) { Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show(); } public class NavigateClickListener implements View.OnClickListener { @Override public void onClick(View v) { saveNotes(); } } }