Java tutorial
/* * Copyright 2013-2017 Carmen Alvarez * * This file is part of Scrum Chatter. * * Scrum Chatter 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. * * Scrum Chatter 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 Scrum Chatter. If not, see <http://www.gnu.org/licenses/>. */ package ca.rmen.android.scrumchatter.meeting.detail; import android.app.Activity; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.MainThread; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.NavUtils; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v7.widget.LinearLayoutManager; import android.text.format.DateUtils; import ca.rmen.android.scrumchatter.chart.MeetingChartActivity; import ca.rmen.android.scrumchatter.databinding.MeetingFragmentBinding; import ca.rmen.android.scrumchatter.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 ca.rmen.android.scrumchatter.Constants; import ca.rmen.android.scrumchatter.R; import ca.rmen.android.scrumchatter.dialog.DialogFragmentFactory; import ca.rmen.android.scrumchatter.meeting.Meetings; import ca.rmen.android.scrumchatter.provider.MeetingColumns; import ca.rmen.android.scrumchatter.provider.MeetingColumns.State; import ca.rmen.android.scrumchatter.provider.MeetingMemberColumns; import ca.rmen.android.scrumchatter.provider.MemberColumns; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; /** * Displays info about a meeting (the duration) as well as the list of members participating in a particular meeting. */ public class MeetingFragment extends Fragment { private String TAG = Constants.TAG + "/" + MeetingFragment.class.getSimpleName() + "/" + System.currentTimeMillis(); private MeetingCursorAdapter mAdapter; private final MeetingObserver mMeetingObserver; private Meeting mMeeting; private long mMeetingId; private Meetings mMeetings; private MeetingFragmentBinding mBinding; /** * @return the {@link MeetingFragment} added to the given {@link FragmentManager}, if any. */ public static MeetingFragment lookupMeetingFragment(FragmentManager fragmentManager) { return (MeetingFragment) fragmentManager.findFragmentById(R.id.meeting_fragment_placeholder); } /** * Add a new {@link MeetingFragment} to the given {@link FragmentManager}, for the given meeting. */ public static void startMeeting(FragmentManager fragmentManager, Meeting meeting) { Bundle bundle = new Bundle(1); bundle.putLong(Meetings.EXTRA_MEETING_ID, meeting.getId()); bundle.putSerializable(Meetings.EXTRA_MEETING_STATE, meeting.getState()); MeetingFragment meetingFragment = new MeetingFragment(); meetingFragment.setArguments(bundle); fragmentManager.beginTransaction().replace(R.id.meeting_fragment_placeholder, meetingFragment).commit(); } public MeetingFragment() { super(); Log.v(TAG, "Constructor"); mMeetingObserver = new MeetingObserver(new Handler()); } @Override public void onAttach(Context activity) { super.onAttach(activity); mMeetings = new Meetings((FragmentActivity) activity); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.v(TAG, "onCreateView: savedInstanceState = " + savedInstanceState); // Create our views mBinding = DataBindingUtil.inflate(inflater, R.layout.meeting_fragment, container, false); mBinding.setMeetingStopListener(new MeetingStopListener()); mBinding.recyclerViewContent.recyclerView .setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); mMeetingId = getArguments().getLong(Meetings.EXTRA_MEETING_ID); if (!TAG.endsWith("" + mMeetingId)) TAG += "/" + mMeetingId; // Load the meeting and register for DB changes on the meeting Uri uri = Uri.withAppendedPath(MeetingColumns.CONTENT_URI, String.valueOf(mMeetingId)); getActivity().getContentResolver().registerContentObserver(uri, false, mMeetingObserver); loadMeeting(); return mBinding.getRoot(); } @Override public void onDestroyView() { Log.v(TAG, "onDestroyView"); getActivity().getContentResolver().unregisterContentObserver(mMeetingObserver); super.onDestroyView(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { Log.v(TAG, "onCreateOptionsMenu: mMeeting =" + mMeeting); inflater.inflate(R.menu.meeting_menu, menu); // Only share and show charts for finished meetings final MenuItem shareItem = menu.findItem(R.id.action_share_meeting); if (shareItem != null) shareItem.setVisible(mMeeting != null && mMeeting.getState() == State.FINISHED); final MenuItem chartItem = menu.findItem(R.id.action_charts_meeting); if (chartItem != null) chartItem.setVisible(mMeeting != null && mMeeting.getState() == State.FINISHED); // Delete a meeting in any state. final MenuItem deleteItem = menu.findItem(R.id.action_delete_meeting); if (deleteItem != null) deleteItem.setVisible(mMeeting != null); } @Override public boolean onOptionsItemSelected(MenuItem item) { Log.v(TAG, "onOptionsItemSelected: item = " + item.getItemId() + ": " + item.getTitle()); if (getActivity().isFinishing()) { Log.w(TAG, "User clicked on a menu item while the activity is finishing. Surely a monkey is involved"); return true; } switch (item.getItemId()) { // Respond to the action bar's Up/Home button case android.R.id.home: NavUtils.navigateUpFromSameTask(getActivity()); return true; case R.id.action_share_meeting: mMeetings.export(mMeeting.getId()); return true; case R.id.action_charts_meeting: MeetingChartActivity.start(getContext(), mMeeting.getId()); return true; case R.id.action_delete_meeting: mMeetings.confirmDelete(mMeeting); return true; default: super.onOptionsItemSelected(item); return false; } } /** * As soon as this fragment is visible and we have loaded the meeting, let's update the action bar icons. */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { Log.v(TAG, "setUserVisibleHint: " + isVisibleToUser); super.setUserVisibleHint(isVisibleToUser); setHasOptionsMenu(isVisibleToUser && mMeeting != null); } /** * Read the given meeting in the background. Init or restart the loader for the meeting members. Update the views for the meeting. */ private void loadMeeting() { Log.v(TAG, "loadMeeting: current meeting = " + mMeeting); Activity activity = getActivity(); if (activity == null) { Log.w(TAG, "loadMeeting called when we are no longer attached to the activity. A monkey might be involved"); return; } State meetingState = mMeeting == null ? (State) getArguments().getSerializable(Meetings.EXTRA_MEETING_STATE) : mMeeting.getState(); Bundle bundle = new Bundle(1); bundle.putSerializable(Meetings.EXTRA_MEETING_STATE, meetingState); if (mAdapter == null) { mAdapter = new MeetingCursorAdapter(activity, mMemberStartStopListener); mBinding.recyclerViewContent.recyclerView.setAdapter(mAdapter); getLoaderManager().initLoader((int) mMeetingId, bundle, mLoaderCallbacks); } else { getLoaderManager().restartLoader((int) mMeetingId, bundle, mLoaderCallbacks); } mMeetings.readMeeting(mMeetingId).doOnSuccess(meeting -> mMeeting = meeting).subscribe(this::displayMeeting, throwable -> activity.getContentResolver().unregisterContentObserver(mMeetingObserver)); } @MainThread private void displayMeeting(Meeting meeting) { FragmentActivity activity = getActivity(); if (activity == null) return; setHasOptionsMenu(true); activity.supportInvalidateOptionsMenu(); // Update the UI views Log.v(TAG, "meetingState = " + meeting.getState()); // Show the "stop meeting" button if the meeting is not finished. mBinding.btnStopMeeting.setVisibility( meeting.getState() == State.NOT_STARTED || meeting.getState() == State.IN_PROGRESS ? View.VISIBLE : View.INVISIBLE); // Only enable the "stop meeting" button if the meeting is in progress. mBinding.btnStopMeeting.setEnabled(meeting.getState() == State.IN_PROGRESS); // Show the horizontal progress bar for in progress meetings mBinding.headerProgressBar .setVisibility(meeting.getState() == State.IN_PROGRESS ? View.VISIBLE : View.INVISIBLE); // Update the chronometer if (meeting.getState() == State.IN_PROGRESS) { // If the meeting is in progress, show the Chronometer. long timeSinceMeetingStartedMillis = System.currentTimeMillis() - meeting.getStartDate(); mBinding.tvMeetingDuration.setBase(SystemClock.elapsedRealtime() - timeSinceMeetingStartedMillis); mBinding.tvMeetingDuration.start(); } else if (meeting.getState() == State.FINISHED) { // For finished meetings, show the duration we retrieved from the db. mBinding.tvMeetingDuration.stop(); mBinding.tvMeetingDuration.setText(DateUtils.formatElapsedTime(meeting.getDuration())); } } public long getMeetingId() { return mMeetingId; } public State getState() { return mMeeting == null ? State.NOT_STARTED : mMeeting.getState(); } /** * Stop the meeting. Set the state to finished, stop the chronometer, hide the "stop meeting" button, persist the meeting duration, and stop the * chronometers for all team members who are still talking. */ public void stopMeeting() { Schedulers.io().scheduleDirect(() -> mMeeting.stop()); } /** * Delete the current meeting, and close the activity, to return to the list of meetings. */ void deleteMeeting() { mBinding.btnStopMeeting.setVisibility(View.INVISIBLE); getActivity().getContentResolver().unregisterContentObserver(mMeetingObserver); Completable.fromRunnable(() -> mMeeting.delete()).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()).subscribe(() -> { // TODO might be better for the activity to finish itself instead. FragmentActivity activity = getActivity(); if (activity != null) activity.finish(); }); } /** * Cursor on the MeetingMember table */ private final LoaderCallbacks<Cursor> mLoaderCallbacks = new LoaderCallbacks<Cursor>() { @Override public Loader<Cursor> onCreateLoader(int loaderId, Bundle bundle) { Log.v(TAG, "onCreateLoader, loaderId = " + loaderId + ", bundle = " + bundle); State meetingState = (State) bundle.getSerializable(Meetings.EXTRA_MEETING_STATE); String selection = null; String orderBy = MemberColumns.NAME + " COLLATE NOCASE"; // For finished meetings, show the member who spoke the most first. // For meetings in progress (or not started), sort alphabetically. if (meetingState == State.FINISHED) { selection = MeetingMemberColumns.DURATION + ">0"; orderBy = MeetingMemberColumns.DURATION + " DESC"; } String[] projection = new String[] { MeetingMemberColumns._ID, MeetingMemberColumns.MEMBER_ID, MemberColumns.NAME, MeetingMemberColumns.DURATION, MeetingColumns.STATE, MeetingMemberColumns.TALK_START_TIME }; Uri uri = Uri.withAppendedPath(MeetingMemberColumns.CONTENT_URI, String.valueOf(loaderId)); return new CursorLoader(getActivity(), uri, projection, selection, null, orderBy); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { Log.v(TAG, "onLoadFinished"); mBinding.recyclerViewContent.progressContainer.setVisibility(View.GONE); mAdapter.changeCursor(cursor); if (mAdapter.getItemCount() > 0) { mBinding.recyclerViewContent.recyclerView.setVisibility(View.VISIBLE); mBinding.recyclerViewContent.empty.setVisibility(View.GONE); } else { mBinding.recyclerViewContent.recyclerView.setVisibility(View.GONE); mBinding.recyclerViewContent.empty.setVisibility(View.VISIBLE); } } @Override public void onLoaderReset(Loader<Cursor> loader) { Log.v(TAG, "onLoaderReset"); mAdapter.changeCursor(null); mBinding.recyclerViewContent.recyclerView.setVisibility(View.GONE); mBinding.recyclerViewContent.empty.setVisibility(View.VISIBLE); } }; /** * Observer on the Meeting table. When a meeting changes, we reload the meeting data itself as well as the * list of members for this meeting. The data on the meeting itself will impact how we display the list * of members. */ private class MeetingObserver extends ContentObserver { private final String TAG = MeetingFragment.this.TAG + "/" + MeetingObserver.class.getSimpleName(); public MeetingObserver(Handler handler) { super(handler); Log.v(TAG, "Constructor"); } @Override public void onChange(boolean selfChange) { Log.v(TAG, "MeetingObserver onChange, selfChange: " + selfChange + ", mMeeting = " + mMeetingId); super.onChange(selfChange); loadMeeting(); } } /** * Manage clicks on items inside the meeting fragment. */ private final MeetingCursorAdapter.MemberStartStopListener mMemberStartStopListener = new MeetingCursorAdapter.MemberStartStopListener() { /** * Switch a member from the talking to non-talking state: * * If they were talking, they will no longer be talking, and their button will go back to a "start" button. * If they were not talking, they will start talking, and their button will be a "stop" button. */ public void toggleTalkingMember(final long memberId) { Log.v(TAG, "toggleTalkingMember " + memberId); Schedulers.io().scheduleDirect(() -> { if (mMeeting.getState() != State.IN_PROGRESS) mMeeting.start(); mMeeting.toggleTalkingMember(memberId); }); } }; // Used from xml for data binding @SuppressWarnings("WeakerAccess") public class MeetingStopListener { public void onMeetingStopped(@SuppressWarnings("UnusedParameters") View view) { // Let's ask him if he's sure. DialogFragmentFactory.showConfirmDialog(getActivity(), getString(R.string.action_stop_meeting), getString(R.string.dialog_confirm), R.id.btn_stop_meeting, null); } } }