Java tutorial
/* * Copyright (c) 2016-2017 Carmen Alvarez * * This file is part of Poet Assistant. * * Poet Assistant 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. * * Poet Assistant 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 Poet Assistant. If not, see <http://www.gnu.org/licenses/>. */ package ca.rmen.android.poetassistant.main.reader; import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.speech.tts.TextToSpeech; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; 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 org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import javax.inject.Inject; import ca.rmen.android.poetassistant.Constants; import ca.rmen.android.poetassistant.DaggerHelper; import ca.rmen.android.poetassistant.compat.HtmlCompat; import ca.rmen.android.poetassistant.R; import ca.rmen.android.poetassistant.Tts; import ca.rmen.android.poetassistant.databinding.FragmentReaderBinding; import ca.rmen.android.poetassistant.main.TextPopupMenu; import ca.rmen.android.poetassistant.main.dictionaries.ConfirmDialogFragment; import ca.rmen.android.poetassistant.main.dictionaries.rt.OnWordClickListener; import ca.rmen.android.poetassistant.settings.SettingsActivity; import ca.rmen.android.poetassistant.widget.CABEditText; public class ReaderFragment extends Fragment implements PoemFile.PoemFileCallback, ConfirmDialogFragment.ConfirmDialogListener { private static final String TAG = Constants.TAG + ReaderFragment.class.getSimpleName(); private static final String EXTRA_INITIAL_TEXT = "initial_text"; private static final String DIALOG_TAG = "dialog"; private static final int ACTION_FILE_OPEN = 0; private static final int ACTION_FILE_SAVE_AS = 1; private static final int ACTION_FILE_NEW = 2; private FragmentReaderBinding mBinding; @Inject Tts mTts; private Handler mHandler; private PoemPrefs mPoemPrefs; private final PlayButtonListener mPlayButtonListener = new PlayButtonListener(); public static ReaderFragment newInstance(String initialText) { Log.d(TAG, "newInstance() called with: " + "initialText = [" + initialText + "]"); ReaderFragment fragment = new ReaderFragment(); fragment.setRetainInstance(true); Bundle bundle = new Bundle(1); bundle.putString(EXTRA_INITIAL_TEXT, initialText); fragment.setArguments(bundle); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { Log.d(TAG, "onCreate() called with: " + "savedInstanceState = [" + savedInstanceState + "]"); super.onCreate(savedInstanceState); DaggerHelper.getAppComponent(getContext()).inject(this); setHasOptionsMenu(true); mPoemPrefs = new PoemPrefs(getActivity()); EventBus.getDefault().register(this); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Log.d(TAG, "onActivityCreated() called with: " + "savedInstanceState = [" + savedInstanceState + "]"); loadPoem(); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "onCreateView() called with: " + "inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]"); mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_reader, container, false); mBinding.setPlayButtonListener(mPlayButtonListener); mBinding.tvText.addTextChangedListener(mTextWatcher); mBinding.tvText.setImeListener(() -> { Activity activity = getActivity(); if (activity instanceof CABEditText.ImeListener) ((CABEditText.ImeListener) activity).onImeClosed(); }); TextPopupMenu.addSelectionPopupMenu(mBinding.tvText, (OnWordClickListener) getActivity()); mHandler = new Handler(); return mBinding.getRoot(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); Log.d(TAG, "onCreateOptionsMenu() called with: " + "menu = [" + menu + "], inflater = [" + inflater + "]"); inflater.inflate(R.menu.menu_tts, menu); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { menu.findItem(R.id.action_new).setTitle(R.string.file_clear); menu.findItem(R.id.action_open).setVisible(false); menu.findItem(R.id.action_save).setVisible(false); menu.findItem(R.id.action_save_as).setVisible(false); } } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); boolean hasEnteredText = !TextUtils.isEmpty(mBinding.tvText.getText()); MenuItem menuItem = menu.findItem(R.id.action_new); if (menuItem == null) { Log.d(TAG, "Unexpected: new menu item missing from reader fragment. Monkey?"); } else { menuItem.setEnabled(hasEnteredText); } menuItem = menu.findItem(R.id.action_save); if (menuItem == null) { Log.d(TAG, "Unexpected: save menu item missing from reader fragment. Monkey?"); } else { menuItem.setEnabled(mPoemPrefs.hasSavedPoem()); } menuItem = menu.findItem(R.id.action_save_as); if (menuItem == null) { Log.d(TAG, "Unexpected: save as menu item missing from reader fragment. Monkey?"); } else { menuItem.setEnabled(hasEnteredText); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == R.id.action_new) { ConfirmDialogFragment.show(ACTION_FILE_NEW, getString(R.string.file_new_confirm_title), getString(R.string.action_clear), getChildFragmentManager(), DIALOG_TAG); } else if (item.getItemId() == R.id.action_open) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) open(); } else if (item.getItemId() == R.id.action_save) { PoemFile poemFile = mPoemPrefs.getSavedPoem(); PoemFile.save(getActivity(), poemFile.uri, mBinding.tvText.getText().toString(), this); } else if (item.getItemId() == R.id.action_save_as) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) saveAs(); } else if (item.getItemId() == R.id.action_share) { Intent intent = new Intent(Intent.ACTION_SEND); intent.putExtra(Intent.EXTRA_TEXT, mBinding.tvText.getText().toString()); intent.setType("text/plain"); startActivity(Intent.createChooser(intent, getString(R.string.share))); } return true; } @TargetApi(Build.VERSION_CODES.KITKAT) private void open() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); PoemFile poemFile = mPoemPrefs.getSavedPoem(); if (poemFile != null) intent.setData(poemFile.uri); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); startActivityForResult(intent, ACTION_FILE_OPEN); } @TargetApi(Build.VERSION_CODES.KITKAT) private void saveAs() { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("text/plain"); PoemFile poemFile = mPoemPrefs.getSavedPoem(); final String fileName; if (poemFile != null) { fileName = poemFile.name; } else { fileName = PoemFile.generateFileName(mBinding.tvText.getText().toString()); } if (!TextUtils.isEmpty(fileName)) intent.putExtra(Intent.EXTRA_TITLE, fileName); startActivityForResult(intent, ACTION_FILE_SAVE_AS); } @Override public void onPause() { Log.d(TAG, "onPause() called with: " + ""); mPoemPrefs.updatePoemText(mBinding.tvText.getText().toString()); super.onPause(); } @Override public void onDestroy() { Log.d(TAG, "onDestroy() called with: " + ""); EventBus.getDefault().unregister(this); super.onDestroy(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); Log.d(TAG, "onActivityResult() called with: " + "requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]"); if (requestCode == ACTION_FILE_OPEN && resultCode == Activity.RESULT_OK) { if (data != null) { Uri uri = data.getData(); PoemFile.open(getActivity(), uri, this); } } else if (requestCode == ACTION_FILE_SAVE_AS && resultCode == Activity.RESULT_OK) { if (data != null) { Uri uri = data.getData(); PoemFile.save(getActivity(), uri, mBinding.tvText.getText().toString(), this); } } } public void setText(String text) { Log.d(TAG, "speak() called with: " + "text = [" + text + "]"); PoemFile poemFile = new PoemFile(null, null, text); mPoemPrefs.setSavedPoem(poemFile); mBinding.tvText.setText(text); } @Override public void onPoemLoaded(PoemFile poemFile) { Log.d(TAG, "onPoemLoaded() called with: " + "poemFile = [" + poemFile + "]"); if (poemFile == null) { mPoemPrefs.clear(); Snackbar.make(mBinding.tvText, getString(R.string.file_opened_error), Snackbar.LENGTH_LONG).show(); } else { mBinding.tvText.setText(poemFile.text); mPoemPrefs.setSavedPoem(poemFile); getActivity().supportInvalidateOptionsMenu(); Snackbar.make(mBinding.tvText, getString(R.string.file_opened, poemFile.name), Snackbar.LENGTH_LONG) .show(); } getActivity().supportInvalidateOptionsMenu(); } @Override public void onPoemSaved(PoemFile poemFile) { if (poemFile == null) { Snackbar.make(mBinding.tvText, getString(R.string.file_saved_error), Snackbar.LENGTH_LONG).show(); } else { Log.d(TAG, "onPoemSaved() called with: " + "poemFile = [" + poemFile + "]"); mPoemPrefs.setSavedPoem(poemFile); Snackbar.make(mBinding.tvText, getString(R.string.file_saved, poemFile.name), Snackbar.LENGTH_LONG) .show(); } getActivity().supportInvalidateOptionsMenu(); } /** * The button shall be disabled if TTS isn't initialized, or if there is no text to play. * The button should display a "Play" icon if TTS isn't running but can be started. * The button should display a "Stop" icon if TTS is currently running. * This is called from a background thread by TTS. */ private void updatePlayButton() { Log.d(TAG, "updatePlayButton: tts status = " + mTts.getStatus() + ", tts is speaking = " + mTts.isSpeaking()); mHandler.post(() -> { boolean enabled = !TextUtils.isEmpty(mBinding.tvText.getText()); mBinding.btnPlay.setEnabled(enabled); if (mTts.isSpeaking()) { mBinding.btnPlay.setImageResource(R.drawable.ic_stop); } else if (!enabled) { mBinding.btnPlay.setImageResource(R.drawable.ic_play_disabled); } else { mBinding.btnPlay.setImageResource(R.drawable.ic_play_enabled); } }); } @Override public void onOk(int actionId) { if (actionId == ACTION_FILE_NEW) { mPoemPrefs.clear(); mBinding.tvText.setText(""); getActivity().supportInvalidateOptionsMenu(); } } public class PlayButtonListener { public void onPlayButtonClicked(@SuppressWarnings("UnusedParameters") View v) { Log.v(TAG, "Play button clicked"); if (mTts.isSpeaking()) { mTts.stop(); } else if (mTts.getStatus() == TextToSpeech.SUCCESS) { speak(); } else { Snackbar snackBar = Snackbar.make(mBinding.getRoot(), HtmlCompat.fromHtml(getString(R.string.tts_error)), Snackbar.LENGTH_LONG); final Intent intent = new Intent("com.android.settings.TTS_SETTINGS"); if (intent.resolveActivity(getActivity().getPackageManager()) != null) { snackBar.setAction(R.string.tts_error_open_system_settings, view -> startActivity(intent)); } else { snackBar.setAction(R.string.tts_error_open_app_settings, view -> startActivity(new Intent(getContext(), SettingsActivity.class))); } snackBar.show(); } updatePlayButton(); } } /** * Read the text in our text view. */ private void speak() { String text = mBinding.tvText.getText().toString(); int startPosition = mBinding.tvText.getSelectionStart(); if (startPosition == text.length()) startPosition = 0; int endPosition = mBinding.tvText.getSelectionEnd(); if (startPosition == endPosition) endPosition = text.length(); text = text.substring(startPosition, endPosition); mTts.speak(text); } private void loadPoem() { Log.d(TAG, "loadPoem() called with: " + ""); // First see if we have poem in the arguments // (the user chose to share some text with our app) Bundle arguments = getArguments(); if (arguments != null) { String initialText = arguments.getString(EXTRA_INITIAL_TEXT); if (!TextUtils.isEmpty(initialText)) { mBinding.tvText.setText(initialText); PoemFile poemFile = new PoemFile(null, null, initialText); mPoemPrefs.setSavedPoem(poemFile); getActivity().supportInvalidateOptionsMenu(); return; } } // Load the poem we previously saved if (mPoemPrefs.hasSavedPoem()) { PoemFile poemFile = mPoemPrefs.getSavedPoem(); mBinding.tvText.setText(poemFile.text); } else if (mPoemPrefs.hasTempPoem()) { String tempPoemText = mPoemPrefs.getTempPoem(); mBinding.tvText.setText(tempPoemText); } } private final TextWatcher mTextWatcher = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { updatePlayButton(); } }; @SuppressWarnings("unused") @Subscribe public void onTtsInitialized(Tts.OnTtsInitialized event) { Log.d(TAG, "onTtsInitialized() called with: " + "event = [" + event + "]"); updatePlayButton(); // Sometimes when the tts engine is initialized, the "isSpeaking()" method returns true // if you call it immediately. If we call updatePlayButton only once at this point, we // will show a "stop" button instead of a "play" button. We workaround this by updating // the button again after a brief moment, hoping that isSpeaking() will correctly // return false, allowing us to display a "play" button. mHandler.postDelayed(this::updatePlayButton, 5000); } @SuppressWarnings("unused") @Subscribe public void onTtsUtteranceCompleted(Tts.OnUtteranceCompleted event) { Log.d(TAG, "onTtsUtteranceCompleted() called with: " + "event = [" + event + "]"); updatePlayButton(); mHandler.postDelayed(this::updatePlayButton, 1000); } }