blob: 105df9f359061a604ca888f634d26238c9cfb926 [file] [log] [blame]
// 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 onClick(View v) {
// super.onClick(v);
// if (mRole == Role.AUDIENCE || mRole == Role.BROWSER) {
// ((PresentationActivity) getActivity()).showFullscreenSlide(mSlideNum);
// }
// }
// });
//
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) {
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();
}
}