/**************************************************************************************** * Copyright (c) 2011 Kostas Spyropoulos <inigo.aldana@gmail.com> * * Copyright (c) 2014 Bruno Romero de Azevedo <brunodea@inf.ufsm.br> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ // TODO: implement own menu? http://www.codeproject.com/Articles/173121/Android-Menus-My-Way package com.ichi2.anki; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.support.v4.content.ContextCompat; import android.support.v4.view.ActionProvider; import android.support.v4.view.MenuItemCompat; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.SubMenu; import android.view.View; import android.widget.FrameLayout; import com.ichi2.anim.ActivityTransitionAnimation; import com.ichi2.async.DeckTask; import com.ichi2.compat.CompatHelper; import com.ichi2.libanki.Card; import com.ichi2.libanki.Collection; import com.ichi2.libanki.Collection.DismissType; import com.ichi2.libanki.Sched; import com.ichi2.themes.Themes; import com.ichi2.widget.WidgetStatus; import org.json.JSONException; import java.lang.ref.WeakReference; import java.text.MessageFormat; import java.util.List; import timber.log.Timber; public class Reviewer extends AbstractFlashcardViewer { private boolean mHasDrawerSwipeConflicts = false; private boolean mShowWhiteboard = true; private boolean mBlackWhiteboard = true; private boolean mPrefFullscreenReview = false; private static final int ADD_NOTE = 12; private Long mLastSelectedBrowserDid = null; @Override protected void onCreate(Bundle savedInstanceState) { Timber.d("onCreate()"); if (Intent.ACTION_VIEW.equals(getIntent().getAction())) { Timber.d("onCreate() :: received Intent with action = %s", getIntent().getAction()); selectDeckFromExtra(); } super.onCreate(savedInstanceState); } private void selectDeckFromExtra() { Bundle extras = getIntent().getExtras(); long did = extras.getLong("deckId", Long.MIN_VALUE); if(did == Long.MIN_VALUE) { // deckId is not set, load default return; } Timber.d("selectDeckFromExtra() with deckId = %d", did); // Clear the undo history when selecting a new deck if (getCol().getDecks().selected() != did) { getCol().clearUndo(); } // Select the deck getCol().getDecks().select(did); // Reset the schedule so that we get the counts for the currently selected deck getCol().getSched().reset(); } @Override protected void setTitle() { try { String[] title = {""}; if (colIsOpen()) { title = getCol().getDecks().current().getString("name").split("::"); } else { Timber.e("Could not set title in reviewer because collection closed"); } getSupportActionBar().setTitle(title[title.length - 1]); super.setTitle(title[title.length - 1]); } catch (JSONException e) { throw new RuntimeException(e); } getSupportActionBar().setSubtitle(""); } @Override protected int getContentViewAttr(int fullscreenMode) { if (CompatHelper.getSdkVersion() < Build.VERSION_CODES.KITKAT) { fullscreenMode = 0; // The specific fullscreen layouts are only applicable for immersive mode } switch (fullscreenMode) { case 1: return R.layout.reviewer_fullscreen_1; case 2: return R.layout.reviewer_fullscreen_2; default: return R.layout.reviewer; } } @Override protected void onCollectionLoaded(Collection col) { super.onCollectionLoaded(col); // Load the first card and start reviewing. Uses the answer card // task to load a card, but since we send null // as the card to answer, no card will be answered. mPrefWhiteboard = MetaDB.getWhiteboardState(this, getParentDid()); if (mPrefWhiteboard) { setWhiteboardEnabledState(true); setWhiteboardVisibility(true); } col.getSched().reset(); // Reset schedule incase card had previous been loaded DeckTask.launchDeckTask(DeckTask.TASK_TYPE_ANSWER_CARD, mAnswerCardHandler, new DeckTask.TaskData(null, 0)); disableDrawerSwipeOnConflicts(); // Add a weak reference to current activity so that scheduler can talk to to Activity mSched.setContext(new WeakReference<Activity>(this)); // Set full screen/immersive mode if needed if (mPrefFullscreenReview) { CompatHelper.getCompat().setFullScreen(this); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (getDrawerToggle().onOptionsItemSelected(item)) { return true; } switch (item.getItemId()) { case android.R.id.home: Timber.i("Reviewer:: Home button pressed"); closeReviewer(RESULT_OK, true); break; case R.id.action_undo: Timber.i("Reviewer:: Undo button pressed"); if (mShowWhiteboard && mWhiteboard != null && mWhiteboard.undoSize() > 0) { mWhiteboard.undo(); } else { undo(); } break; case R.id.action_mark_card: Timber.i("Reviewer:: Mark button pressed"); onMark(mCurrentCard); break; case R.id.action_replay: Timber.i("Reviewer:: Replay audio button pressed (from menu)"); playSounds(true); break; case R.id.action_edit: Timber.i("Reviewer:: Edit note button pressed"); return editCard(); case R.id.action_bury: Timber.i("Reviewer:: Bury button pressed"); if (!MenuItemCompat.getActionProvider(item).hasSubMenu()) { Timber.d("Bury card due to no submenu"); dismiss(DismissType.BURY_CARD); } break; case R.id.action_suspend: Timber.i("Reviewer:: Suspend button pressed"); if (!MenuItemCompat.getActionProvider(item).hasSubMenu()) { Timber.d("Suspend card due to no submenu"); dismiss(DismissType.SUSPEND_CARD); } break; case R.id.action_delete: Timber.i("Reviewer:: Delete note button pressed"); showDeleteNoteDialog(); break; case R.id.action_clear_whiteboard: Timber.i("Reviewer:: Clear whiteboard button pressed"); if (mWhiteboard != null) { mWhiteboard.clear(); } break; case R.id.action_hide_whiteboard: // toggle whiteboard visibility Timber.i("Reviewer:: Whiteboard visibility set to %b", !mShowWhiteboard); setWhiteboardVisibility(!mShowWhiteboard); refreshActionBar(); break; case R.id.action_enable_whiteboard: // toggle whiteboard enabled state (and show/hide whiteboard item in action bar) mPrefWhiteboard = ! mPrefWhiteboard; Timber.i("Reviewer:: Whiteboard enabled state set to %b", mPrefWhiteboard); setWhiteboardEnabledState(mPrefWhiteboard); setWhiteboardVisibility(mPrefWhiteboard); refreshActionBar(); break; case R.id.action_search_dictionary: Timber.i("Reviewer:: Search dictionary button pressed"); lookUpOrSelectText(); break; case R.id.action_open_deck_options: Intent i = new Intent(this, DeckOptions.class); startActivityForResultWithAnimation(i, DECK_OPTIONS, ActivityTransitionAnimation.FADE); break; case R.id.action_select_tts: Timber.i("Reviewer:: Select TTS button pressed"); showSelectTtsDialogue(); break; case R.id.action_add_note_reviewer: Timber.i("Reviewer:: Add note button pressed"); addNote(); break; default: return super.onOptionsItemSelected(item); } return true; } private void addNote() { Intent intent = new Intent(this, NoteEditor.class); intent.putExtra(NoteEditor.EXTRA_CALLER, NoteEditor.CALLER_REVIEWER_ADD); startActivityForResultWithAnimation(intent, ADD_NOTE, ActivityTransitionAnimation.LEFT); } private void setCustomButtons(Menu menu) { for(int itemId : mCustomButtons.keySet()) { if(mCustomButtons.get(itemId) != MENU_DISABLED) { MenuItemCompat.setShowAsAction(menu.findItem(itemId), mCustomButtons.get(itemId)); } else { menu.findItem(itemId).setVisible(false); } } } @SuppressLint("NewApi") @Override public boolean onCreateOptionsMenu(Menu menu) { // NOTE: This is called every time a new question is shown via invalidate options menu getMenuInflater().inflate(R.menu.reviewer, menu); Resources res = getResources(); setCustomButtons(menu); if (mCurrentCard != null && mCurrentCard.note().hasTag("marked")) { menu.findItem(R.id.action_mark_card).setTitle(R.string.menu_unmark_note).setIcon(R.drawable.ic_star_white_24dp); } else { menu.findItem(R.id.action_mark_card).setTitle(R.string.menu_mark_note).setIcon(R.drawable.ic_star_outline_white_24dp); } if (mShowWhiteboard && mWhiteboard != null && mWhiteboard.undoSize() > 0) { // Whiteboard undo queue non-empty. Switch the undo icon to a whiteboard specific one. menu.findItem(R.id.action_undo).setIcon(R.drawable.ic_eraser_variant_white_24dp); menu.findItem(R.id.action_undo).setEnabled(true).getIcon().setAlpha(Themes.ALPHA_ICON_ENABLED_LIGHT); } else if (mShowWhiteboard && mWhiteboard != null && mWhiteboard.isUndoModeActive()) { // Whiteboard undo queue empty, but user has added strokes to it for current card. Disable undo button. menu.findItem(R.id.action_undo).setIcon(R.drawable.ic_eraser_variant_white_24dp); menu.findItem(R.id.action_undo).setEnabled(false).getIcon().setAlpha(Themes.ALPHA_ICON_DISABLED_LIGHT); } else if (colIsOpen() && getCol().undoAvailable()) { menu.findItem(R.id.action_undo).setIcon(R.drawable.ic_undo_white_24dp); menu.findItem(R.id.action_undo).setEnabled(true).getIcon().setAlpha(Themes.ALPHA_ICON_ENABLED_LIGHT); } else { menu.findItem(R.id.action_undo).setIcon(R.drawable.ic_undo_white_24dp); menu.findItem(R.id.action_undo).setEnabled(false).getIcon().setAlpha(Themes.ALPHA_ICON_DISABLED_LIGHT); } if (mPrefWhiteboard) { // Configure the whiteboard related items in the action bar menu.findItem(R.id.action_enable_whiteboard).setTitle(R.string.disable_whiteboard); if(mCustomButtons.get(R.id.action_hide_whiteboard) != MENU_DISABLED) menu.findItem(R.id.action_hide_whiteboard).setVisible(true); if(mCustomButtons.get(R.id.action_clear_whiteboard) != MENU_DISABLED) menu.findItem(R.id.action_clear_whiteboard).setVisible(true); Drawable whiteboardIcon = ContextCompat.getDrawable(this, R.drawable.ic_gesture_white_24dp); if (mShowWhiteboard) { whiteboardIcon.setAlpha(255); menu.findItem(R.id.action_hide_whiteboard).setIcon(whiteboardIcon); menu.findItem(R.id.action_hide_whiteboard).setTitle(R.string.hide_whiteboard); } else { whiteboardIcon.setAlpha(77); menu.findItem(R.id.action_hide_whiteboard).setIcon(whiteboardIcon); menu.findItem(R.id.action_hide_whiteboard).setTitle(R.string.show_whiteboard); } } else { menu.findItem(R.id.action_enable_whiteboard).setTitle(R.string.enable_whiteboard); } if (!CompatHelper.isHoneycomb() && !mDisableClipboard) { menu.findItem(R.id.action_search_dictionary).setVisible(true).setEnabled(!(mPrefWhiteboard && mShowWhiteboard)) .setTitle(clipboardHasText() ? Lookup.getSearchStringTitle() : res.getString(R.string.menu_select)); } if (getCol().getDecks().isDyn(getParentDid())) { menu.findItem(R.id.action_open_deck_options).setVisible(false); } if(mSpeakText){ if(mCustomButtons.get(R.id.action_select_tts) != MENU_DISABLED) menu.findItem(R.id.action_select_tts).setVisible(true); } // Setup bury / suspend providers MenuItemCompat.setActionProvider(menu.findItem(R.id.action_suspend), new SuspendProvider(this)); MenuItemCompat.setActionProvider(menu.findItem(R.id.action_bury), new BuryProvider(this)); if (dismissNoteAvailable(DismissType.SUSPEND_NOTE)) { menu.findItem(R.id.action_suspend).setIcon(R.drawable.ic_lock_outline_white_24px_dropdown); menu.findItem(R.id.action_suspend).setTitle(R.string.menu_suspend); } else { menu.findItem(R.id.action_suspend).setIcon(R.drawable.ic_lock_outline_white_24dp); menu.findItem(R.id.action_suspend).setTitle(R.string.menu_suspend_card); } if (dismissNoteAvailable(DismissType.BURY_NOTE)) { menu.findItem(R.id.action_bury).setIcon(R.drawable.ic_flip_to_back_white_24px_dropdown); menu.findItem(R.id.action_bury).setTitle(R.string.menu_bury); } else { menu.findItem(R.id.action_bury).setIcon(R.drawable.ic_flip_to_back_white_24dp); menu.findItem(R.id.action_bury).setTitle(R.string.menu_bury_card); } return super.onCreateOptionsMenu(menu); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { char keyPressed = (char) event.getUnicodeChar(); if (mAnswerField != null && !mAnswerField.isFocused()) { if (sDisplayAnswer) { if (keyPressed == '1') { answerCard(EASE_1); return true; } if (keyPressed == '2') { answerCard(EASE_2); return true; } if (keyPressed == '3') { answerCard(EASE_3); return true; } if (keyPressed == '4') { answerCard(EASE_4); return true; } if (keyCode == KeyEvent.KEYCODE_SPACE || keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER) { answerCard(getDefaultEase()); return true; } } if (keyPressed == 'e') { editCard(); return true; } if (keyPressed == '*') { onMark(mCurrentCard); return true; } if (keyPressed == '-') { dismiss(DismissType.BURY_CARD); return true; } if (keyPressed == '=') { dismiss(DismissType.BURY_NOTE); return true; } if (keyPressed == '@') { dismiss(DismissType.SUSPEND_CARD); return true; } if (keyPressed == '!') { dismiss(DismissType.SUSPEND_NOTE); return true; } if (keyPressed == 'r' || keyCode == KeyEvent.KEYCODE_F5) { playSounds(true); return true; } // different from Anki Desktop if (keyPressed == 'z') { undo(); return true; } } return super.onKeyUp(keyCode, event); } @Override protected SharedPreferences restorePreferences() { super.restorePreferences(); SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(getBaseContext()); mBlackWhiteboard = preferences.getBoolean("blackWhiteboard", true); mPrefFullscreenReview = Integer.parseInt(preferences.getString("fullscreenMode", "0")) > 0; return preferences; } @Override public void fillFlashcard() { super.fillFlashcard(); if (!sDisplayAnswer) { if (mShowWhiteboard && mWhiteboard != null) { mWhiteboard.clear(); } } } @Override public void displayCardQuestion() { // show timer, if activated in the deck's preferences initTimer(); super.displayCardQuestion(); } @Override protected void onStop() { super.onStop(); if (!isFinishing()) { if (colIsOpen() && mSched != null) { WidgetStatus.update(this); } } UIUtils.saveCollectionInBackground(this); } @Override protected void initControls() { super.initControls(); if (mPrefWhiteboard) { setWhiteboardVisibility(mShowWhiteboard); } } private void setWhiteboardEnabledState(boolean state) { mPrefWhiteboard = state; MetaDB.storeWhiteboardState(this, getParentDid(), state); if (state && mWhiteboard == null) { createWhiteboard(); } } // Create the whiteboard private void createWhiteboard() { mWhiteboard = new Whiteboard(this, mNightMode, mBlackWhiteboard); FrameLayout.LayoutParams lp2 = new FrameLayout.LayoutParams( android.view.ViewGroup.LayoutParams.FILL_PARENT, android.view.ViewGroup.LayoutParams.FILL_PARENT); mWhiteboard.setLayoutParams(lp2); FrameLayout fl = (FrameLayout) findViewById(R.id.whiteboard); fl.addView(mWhiteboard); mWhiteboard.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (!mShowWhiteboard || (mPrefFullscreenReview && CompatHelper.getCompat().isImmersiveSystemUiVisible(Reviewer.this))) { // Bypass whiteboard listener when it's hidden or fullscreen immersive mode is temporarily suspended return getGestureDetector().onTouchEvent(event); } return mWhiteboard.handleTouchEvent(event); } }); mWhiteboard.setEnabled(true); } // Show or hide the whiteboard private void setWhiteboardVisibility(boolean state) { mShowWhiteboard = state; if (state) { mWhiteboard.setVisibility(View.VISIBLE); disableDrawerSwipe(); } else { mWhiteboard.setVisibility(View.GONE); if (!mHasDrawerSwipeConflicts) { enableDrawerSwipe(); } } } private void disableDrawerSwipeOnConflicts() { SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(getBaseContext()); boolean gesturesEnabled = AnkiDroidApp.initiateGestures(preferences); if (gesturesEnabled) { int gestureSwipeUp = Integer.parseInt(preferences.getString("gestureSwipeUp", "9")); int gestureSwipeDown = Integer.parseInt(preferences.getString("gestureSwipeDown", "0")); int gestureSwipeRight = Integer.parseInt(preferences.getString("gestureSwipeRight", "17")); if (gestureSwipeUp != GESTURE_NOTHING || gestureSwipeDown != GESTURE_NOTHING || gestureSwipeRight != GESTURE_NOTHING) { mHasDrawerSwipeConflicts = true; super.disableDrawerSwipe(); } } } @Override protected void openCardBrowser() { Intent cardBrowser = new Intent(this, CardBrowser.class); cardBrowser.putExtra("selectedDeck", getCol().getDecks().selected()); if (mLastSelectedBrowserDid != null) { cardBrowser.putExtra("defaultDeckId", mLastSelectedBrowserDid); } else { cardBrowser.putExtra("defaultDeckId", getCol().getDecks().selected()); } cardBrowser.putExtra("currentCard", mCurrentCard.getId()); startActivityForResultWithAnimation(cardBrowser, REQUEST_BROWSE_CARDS, ActivityTransitionAnimation.LEFT); } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); // Restore full screen once we regain focus if (hasFocus) { delayedHide(INITIAL_HIDE_DELAY); } else { mFullScreenHandler.removeMessages(0); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_STATISTICS || requestCode == REQUEST_BROWSE_CARDS) { // Store the selected deck if (data != null && data.getBooleanExtra("allDecksSelected", false)) { mLastSelectedBrowserDid = -1L; } else { mLastSelectedBrowserDid = getCol().getDecks().selected(); } // select original deck if the statistics or card browser were opened, which can change the selected deck if (data != null && data.hasExtra("originalDeck")) { getCol().getDecks().select(data.getLongExtra("originalDeck", 0L)); } } super.onActivityResult(requestCode, resultCode, data); } /** * Whether or not dismiss note is available for current card and specified DismissType * @param type Currently only SUSPEND_NOTE and BURY_NOTE supported * @return true if there is another card of same note that could be dismissed */ private boolean dismissNoteAvailable(DismissType type) { if (mCurrentCard == null || mCurrentCard.note() == null || mCurrentCard.note().cards().size() < 2) { return false; } List<Card> cards = mCurrentCard.note().cards(); for(Card card : cards) { if (card.getId() == mCurrentCard.getId()) continue; int queue = card.getQueue(); if(type == DismissType.SUSPEND_NOTE && queue != Card.QUEUE_SUSP) { return true; } else if (type == DismissType.BURY_NOTE && queue != Card.QUEUE_SUSP && queue != Card.QUEUE_USER_BRD && queue != Card.QUEUE_SCHED_BRD) { return true; } } return false; } /** * Inner class which implements the submenu for the Suspend button */ class SuspendProvider extends ActionProvider implements MenuItem.OnMenuItemClickListener { public SuspendProvider(Context context) { super(context); } @Override public View onCreateActionView() { return null; // Just return null for a simple dropdown menu } @Override public boolean hasSubMenu() { return dismissNoteAvailable(DismissType.SUSPEND_NOTE); } @Override public void onPrepareSubMenu(SubMenu subMenu) { subMenu.clear(); getMenuInflater().inflate(R.menu.reviewer_suspend, subMenu); for (int i = 0; i < subMenu.size(); i++) { subMenu.getItem(i).setOnMenuItemClickListener(this); } } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_suspend_card: dismiss(DismissType.SUSPEND_CARD); return true; case R.id.action_suspend_note: dismiss(DismissType.SUSPEND_NOTE); return true; default: return false; } } } /** * Inner class which implements the submenu for the Suspend button */ class BuryProvider extends ActionProvider implements MenuItem.OnMenuItemClickListener { public BuryProvider(Context context) { super(context); } @Override public View onCreateActionView() { return null; // Just return null for a simple dropdown menu } @Override public boolean hasSubMenu() { return dismissNoteAvailable(DismissType.BURY_NOTE); } @Override public void onPrepareSubMenu(SubMenu subMenu) { subMenu.clear(); getMenuInflater().inflate(R.menu.reviewer_bury, subMenu); for (int i = 0; i < subMenu.size(); i++) { subMenu.getItem(i).setOnMenuItemClickListener(this); } } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.action_bury_card: dismiss(DismissType.BURY_CARD); return true; case R.id.action_bury_note: dismiss(DismissType.BURY_NOTE); return true; default: return false; } } } }