Java tutorial
/* * Copyright 2016-2017 Carmen Alvarez * <p/> * This file is part of Scrum Chatter. * <p/> * 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. * <p/> * 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. * <p/> * 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.main; import android.app.Activity; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.databinding.DataBindingUtil; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.annotation.MainThread; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.design.widget.Snackbar; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.GravityCompat; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.graphics.drawable.DrawerArrowDrawable; import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import java.util.Arrays; import ca.rmen.android.scrumchatter.Constants; import ca.rmen.android.scrumchatter.R; import ca.rmen.android.scrumchatter.about.AboutActivity; import ca.rmen.android.scrumchatter.chart.ChartsActivity; import ca.rmen.android.scrumchatter.databinding.ActivityMainBinding; import ca.rmen.android.scrumchatter.dialog.ChoiceDialogFragment.DialogItemListener; import ca.rmen.android.scrumchatter.dialog.ConfirmDialogFragment.DialogButtonListener; import ca.rmen.android.scrumchatter.dialog.DialogFragmentFactory; import ca.rmen.android.scrumchatter.dialog.InputDialogFragment.DialogInputListener; import ca.rmen.android.scrumchatter.dialog.ProgressDialogFragment; import ca.rmen.android.scrumchatter.export.DBExport; import ca.rmen.android.scrumchatter.export.FileExport; import ca.rmen.android.scrumchatter.export.MeetingsExport; import ca.rmen.android.scrumchatter.meeting.Meetings; import ca.rmen.android.scrumchatter.meeting.detail.MeetingFragment; import ca.rmen.android.scrumchatter.meeting.list.MeetingsListFragment; import ca.rmen.android.scrumchatter.member.list.Members; import ca.rmen.android.scrumchatter.provider.DBImport; import ca.rmen.android.scrumchatter.provider.MeetingColumns; import ca.rmen.android.scrumchatter.settings.SettingsActivity; import ca.rmen.android.scrumchatter.settings.Theme; import ca.rmen.android.scrumchatter.team.Teams; import ca.rmen.android.scrumchatter.team.Teams.Team; import ca.rmen.android.scrumchatter.team.TeamsObserver; import ca.rmen.android.scrumchatter.util.Log; import ca.rmen.android.scrumchatter.util.StrictModeUtil; import io.reactivex.Single; import io.reactivex.schedulers.Schedulers; /** * The main screen of the app. Part of this code was generated by the ADT * plugin. */ public class MainActivity extends AppCompatActivity implements DialogButtonListener, DialogItemListener, DialogInputListener { private static final String TAG = Constants.TAG + "/" + MainActivity.class.getSimpleName(); private static final String EXTRA_IMPORT_URI = "import_uri"; private static final String EXTRA_IMPORT_RESULT = "import_result"; private static final String EXTRA_EXPORT_RESULT = "export_result"; private static final String ACTION_IMPORT_COMPLETE = "action_import_complete"; private static final String ACTION_EXPORT_COMPLETE = "action_export_complete"; private static final int ACTIVITY_REQUEST_CODE_IMPORT = 1; private static final String PROGRESS_DIALOG_FRAGMENT_TAG = "progress_dialog_fragment_tag"; /** * UI elements for the side menu (left drawer). */ private ActionBarDrawerToggle mDrawerToggle; private final Teams mTeams = new Teams(this); private final Meetings mMeetings = new Meetings(this); private final Members mMembers = new Members(this); private Team mTeam = null; private int mTeamCount = 0; private ActivityMainBinding mBinding; private TeamNavigationMenu mTeamNavigationMenu; private TeamsObserver mTeamsObserver; private MainPagerAdapter mMainPagerAdapter; @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "onCreate"); super.onCreate(savedInstanceState); Theme.checkTheme(this); mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main); StrictModeUtil.enable(); // Set up the action bar. setSupportActionBar(mBinding.toolbarTabs.toolbar); ActionBar supportActionBar = getSupportActionBar(); assert supportActionBar != null; supportActionBar.setDisplayHomeAsUpEnabled(true); supportActionBar.setHomeButtonEnabled(true); mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */ mBinding.drawerLayout, /* DrawerLayout object */ R.string.drawer_open, /* "open drawer" description */ R.string.drawer_close /* "close drawer" description */); // Explanation of setDrawerIndicatorEnabled and setHomeAsUpIndicator: // We want to only have a hamburger icon, always, without any animation. // If we don't call either of these methods, we use the default indicator, // which is the hamburger icon transitioning to a left-arrow icon, as the drawer is opened. // If we only call setDrawerIndicatorUpEnabled, we'll have the left arrow icon always. // If we only call setHomeAsUpIndicator (with a hamburger icon), we'll have a hamburger icon // but with a bug: If you open the drawer, rotate, and close it, you'll have the left arrow // again. // With the combination of both setDrawerIndicatorEnabled and setHomeAsUpIndicator, we // have a hamburger icon always. mDrawerToggle.setDrawerIndicatorEnabled(false); mDrawerToggle.setHomeAsUpIndicator(new DrawerArrowDrawable(this)); mMainPagerAdapter = new MainPagerAdapter(this, getSupportFragmentManager()); // Set up the ViewPager with the sections adapter. mBinding.pager.setAdapter(mMainPagerAdapter); mBinding.toolbarTabs.tabs.setupWithViewPager(mBinding.pager); // If our activity was opened by choosing a file from a mail attachment, file browser, or other program, // import the database from this file. Intent intent = getIntent(); if (intent != null) { if (Intent.ACTION_VIEW.equals(intent.getAction())) importDB(intent.getData()); } // Register various observers. mTeamsObserver = new TeamsObserver(this, mOnTeamsChangedListener); mOnTeamsChangedListener.onTeamsChanged(); mTeamsObserver.register(); mTeamNavigationMenu = new TeamNavigationMenu(this, mBinding.leftDrawer.getMenu()); mTeamNavigationMenu.load(); mBinding.leftDrawer.setNavigationItemSelectedListener(mOnNavigationItemSelectedListener); IntentFilter filter = new IntentFilter(ACTION_IMPORT_COMPLETE); filter.addAction(ACTION_EXPORT_COMPLETE); LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(mBroadcastReceiver, filter); } @Override protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); // Sync the toggle state after onRestoreInstanceState has occurred. mDrawerToggle.syncState(); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mDrawerToggle.onConfigurationChanged(newConfig); } @Override protected void onResumeFragments() { Log.v(TAG, "onResumeFragments: intent = " + getIntent()); super.onResumeFragments(); // The user chose a DB file to import. We saved the URI in onActivityForResult. Now // we show a confirmation dialog fragment to actually import the file. Intent intent = getIntent(); if (intent != null && intent.getExtras() != null) { Uri importUri = getIntent().getExtras().getParcelable(EXTRA_IMPORT_URI); if (importUri != null) { // Remove the uri extra, otherwise the confirmation dialog will keep popping up when // we rotate the device. getIntent().removeExtra(EXTRA_IMPORT_URI); importDB(importUri); } } } @Override protected void onDestroy() { Log.v(TAG, "onDestroy"); mTeamsObserver.destroy(); mTeamNavigationMenu.destroy(); super.onDestroy(); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { Log.v(TAG, "onPrepareOptionsMenu " + menu); // Only enable the "delete team" menu item if we have at least two teams. MenuItem deleteItem = menu.findItem(R.id.action_team_delete); deleteItem.setEnabled(mTeamCount > 1); // Add the current team name to the delete and rename menu items if (mTeam != null) { deleteItem.setTitle(getString(R.string.action_team_delete_name, mTeam.teamName)); MenuItem renameItem = menu.findItem(R.id.action_team_rename); renameItem.setTitle(getString(R.string.action_team_rename_name, mTeam.teamName)); } // Only show the settings in v14+. Currently the only setting // we have is for the day/night theme switch, which only works on // v14+. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { MenuItem settingsItem = menu.findItem(R.id.action_settings); settingsItem.setVisible(false); } // Only show the menu items for sharing a meeting and stats for a meeting, if the selected meeting is finished. MeetingFragment meetingFragment = MeetingFragment.lookupMeetingFragment(getSupportFragmentManager()); boolean meetingIsFinished = meetingFragment != null && meetingFragment.getState() == MeetingColumns.State.FINISHED; MenuItem menuItem = menu.findItem(R.id.action_share_meeting); if (menuItem != null) menuItem.setVisible(meetingIsFinished); menuItem = menu.findItem(R.id.action_charts_meeting); if (menuItem != null) menuItem.setVisible(meetingIsFinished); // Don't show the global share/stats menu items unless we have at least one meeting MeetingsListFragment meetingsListFragment = (MeetingsListFragment) mMainPagerAdapter .instantiateItem(mBinding.pager, 0); boolean hasMeetings = meetingsListFragment != null && meetingsListFragment.hasMeetings(); menu.findItem(R.id.action_share).setVisible(hasMeetings); menu.findItem(R.id.action_charts).setVisible(hasMeetings); menuItem = menu.findItem(R.id.action_share_submenu); if (menuItem != null) menuItem.setVisible(hasMeetings); menuItem = menu.findItem(R.id.action_charts_submenu); if (menuItem != null) menuItem.setVisible(hasMeetings); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: if (mBinding.drawerLayout.isDrawerVisible(GravityCompat.START)) { mBinding.drawerLayout.closeDrawer(GravityCompat.START); } else { mBinding.drawerLayout.openDrawer(GravityCompat.START); } return true; case R.id.action_team_rename: mTeams.promptRenameTeam(mTeam); return true; case R.id.action_team_delete: mTeams.confirmDeleteTeam(mTeam); return true; case R.id.action_import: startFileChooser(); return true; case R.id.action_charts: startActivity(new Intent(this, ChartsActivity.class)); return true; case R.id.action_share: // Build a chooser dialog for the file format. DialogFragmentFactory.showChoiceDialog(this, getString(R.string.export_choice_title), getResources().getStringArray(R.array.export_choices), -1, R.id.action_share); return true; case R.id.action_settings: Intent settingsIntent = new Intent(this, SettingsActivity.class); startActivity(settingsIntent); return true; case R.id.action_about: Intent aboutIntent = new Intent(this, AboutActivity.class); startActivity(aboutIntent); return true; } super.onOptionsItemSelected(item); return false; } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.v(TAG, "onActivityResult: requestCode = " + requestCode + ", resultCode = " + resultCode + ", intent = " + intent); // The user chose a DB file to import. if (requestCode == ACTIVITY_REQUEST_CODE_IMPORT && resultCode == Activity.RESULT_OK) { if (intent == null || intent.getData() == null) { Snackbar.make(mBinding.getRoot(), R.string.import_result_no_file, Snackbar.LENGTH_SHORT).show(); return; } final String filePath = intent.getData().getPath(); if (TextUtils.isEmpty(filePath)) { Snackbar.make(mBinding.getRoot(), R.string.import_result_no_file, Snackbar.LENGTH_SHORT).show(); return; } // Save the uri of the file. We will import it in onResumeFragments. getIntent().putExtra(EXTRA_IMPORT_URI, intent.getData()); } else { super.onActivityResult(requestCode, resultCode, intent); } } @Override public void onBackPressed() { Log.v(TAG, "onBackPressed"); // Prevent the monkey from exiting the app, to maximize the time the monkey spends testing the app. if (ActivityManager.isUserAMonkey()) { Log.v(TAG, "Sorry, monkeys must stay in the cage"); return; } super.onBackPressed(); } /** * Import the given database file. This will replace the current database. */ private void importDB(final Uri uri) { Bundle extras = new Bundle(1); extras.putParcelable(EXTRA_IMPORT_URI, uri); DialogFragmentFactory.showConfirmDialog(this, getString(R.string.import_confirm_title), getString(R.string.import_confirm_message, uri.getEncodedPath()), R.id.action_import, extras); } /** * Share a file using an intent chooser. * * @param fileExport The object responsible for creating the file to share. */ private void shareFile(final FileExport fileExport) { DialogFragmentFactory.showProgressDialog(MainActivity.this, getString(R.string.progress_dialog_message), PROGRESS_DIALOG_FRAGMENT_TAG); Schedulers.io().scheduleDirect(() -> { boolean result = fileExport.export(); Intent intent = new Intent(ACTION_EXPORT_COMPLETE); intent.putExtra(EXTRA_EXPORT_RESULT, result); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); Log.v(TAG, "broadcast " + intent); }); } /** * Called when the current team was changed. Update our cache of the current team and update the ui (menu items, action bar title). */ private final TeamsObserver.OnTeamsChangedListener mOnTeamsChangedListener = () -> Single .zip(mTeams.readCurrentTeam(), mTeams.getTeamCount(), TeamInfo::new).doOnSuccess(teamInfo -> { mTeam = teamInfo.team; mTeamCount = teamInfo.teamCount; }).subscribe(this::updateActionBar); private static class TeamInfo { final Team team; final int teamCount; TeamInfo(Team team, int teamCount) { this.team = team; this.teamCount = teamCount; } } @MainThread private void updateActionBar(TeamInfo teamInfo) { ActionBar supportActionBar = getSupportActionBar(); if (supportActionBar != null) { // If the user has renamed the default team or added other teams, show the current team name in the title if (teamInfo.teamCount > 1 || (teamInfo.team != null && !teamInfo.team.teamName.equals(Constants.DEFAULT_TEAM_NAME))) { supportActionBar.setTitle(teamInfo.team.teamName); } // otherwise the user doesn't care about team management: just show the app title. else { supportActionBar.setTitle(R.string.app_name); } } supportInvalidateOptionsMenu(); } /** * The user tapped on the OK button of a confirmation dialog. Execute the action requested by the user. * * @param actionId the action id which was provided to the {@link DialogFragmentFactory} when creating the dialog. * @param extras any extras which were provided to the {@link DialogFragmentFactory} when creating the dialog. * @see ca.rmen.android.scrumchatter.dialog.ConfirmDialogFragment.DialogButtonListener#onOkClicked(int, android.os.Bundle) */ @Override public void onOkClicked(int actionId, Bundle extras) { Log.v(TAG, "onClicked: actionId = " + actionId + ", extras = " + extras); if (actionId == R.id.action_delete_meeting) { long meetingId = extras.getLong(Meetings.EXTRA_MEETING_ID); mMeetings.delete(meetingId); } else if (actionId == R.id.btn_stop_meeting) { MeetingFragment meetingFragment = MeetingFragment.lookupMeetingFragment(getSupportFragmentManager()); if (meetingFragment != null) meetingFragment.stopMeeting(); } else if (actionId == R.id.action_delete_member) { long memberId = extras.getLong(Members.EXTRA_MEMBER_ID); mMembers.deleteMember(memberId); } else if (actionId == R.id.action_team_delete) { Uri teamUri = extras.getParcelable(Teams.EXTRA_TEAM_URI); mTeams.deleteTeam(teamUri); } else if (actionId == R.id.action_import) { final Uri uri = extras.getParcelable(EXTRA_IMPORT_URI); DialogFragmentFactory.showProgressDialog(MainActivity.this, getString(R.string.progress_dialog_message), PROGRESS_DIALOG_FRAGMENT_TAG); Schedulers.io().scheduleDirect(() -> { boolean result = false; try { Log.v(TAG, "Importing db from " + uri); DBImport.importDB(MainActivity.this, uri); result = true; } catch (Exception e) { Log.e(TAG, "Error importing db: " + e.getMessage(), e); } // Notify ourselves with a broadcast. If the user rotated the device, this activity // won't be visible any more. The new activity will receive the broadcast and update // the UI. Intent intent = new Intent(ACTION_IMPORT_COMPLETE).putExtra(EXTRA_IMPORT_RESULT, result); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); }); } } /** * The user selected an item in a choice dialog. Perform the action for the selected item. * * @param actionId the action id which was provided to the {@link DialogFragmentFactory} when creating the dialog. * @param choices the localized labels of the items. * @param which the index of the item which was selected * @see ca.rmen.android.scrumchatter.dialog.ChoiceDialogFragment.DialogItemListener#onItemSelected(int, java.lang.CharSequence[], int) */ @Override public void onItemSelected(int actionId, CharSequence[] choices, int which) { Log.v(TAG, "onItemSelected: actionId = " + actionId + ", choices = " + Arrays.toString(choices) + ", which = " + which); if (actionId == R.id.action_share) { FileExport fileExport = null; if (getString(R.string.export_format_excel).equals(choices[which])) fileExport = new MeetingsExport(MainActivity.this); else if (getString(R.string.export_format_db).equals(choices[which])) fileExport = new DBExport(MainActivity.this); shareFile(fileExport); } } /** * The user tapped on the OK button on a dialog in which s/he entered text. * * @param actionId the action id which was provided to the {@link DialogFragmentFactory} when creating the dialog. * @param input the text entered by the user. * @param extras any extras which were provided to the {@link DialogFragmentFactory} when creating the dialog. * @see ca.rmen.android.scrumchatter.dialog.InputDialogFragment.DialogInputListener#onInputEntered(int, java.lang.String, android.os.Bundle) */ @Override public void onInputEntered(int actionId, String input, Bundle extras) { Log.v(TAG, "onInputEntered: actionId = " + actionId + ", input = " + input + ", extras = " + extras); if (actionId == R.id.fab_new_member) { long teamId = extras.getLong(Teams.EXTRA_TEAM_ID); mMembers.createMember(teamId, input); } else if (actionId == R.id.action_rename_member) { long memberId = extras.getLong(Members.EXTRA_MEMBER_ID); mMembers.renameMember(memberId, input); } else if (actionId == R.id.action_team) { mTeams.createTeam(input); } else if (actionId == R.id.action_team_rename) { Uri teamUri = extras.getParcelable(Teams.EXTRA_TEAM_URI); mTeams.renameTeam(teamUri, input); } } private void startFileChooser() { final Intent importIntent; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) importIntent = new Intent(Intent.ACTION_GET_CONTENT); else importIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); importIntent.setType("*/*"); importIntent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(Intent.createChooser(importIntent, getResources().getText(R.string.action_import)), ACTIVITY_REQUEST_CODE_IMPORT); } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.v(TAG, "onReceive: intent = " + intent); // The DB import has completed. Dismiss the progress dialog and show a toast. if (ACTION_IMPORT_COMPLETE.equals(intent.getAction())) { Boolean result = intent.getExtras().getBoolean(EXTRA_IMPORT_RESULT); ProgressDialogFragment dialogFragment = (ProgressDialogFragment) getSupportFragmentManager() .findFragmentByTag(PROGRESS_DIALOG_FRAGMENT_TAG); if (dialogFragment != null) dialogFragment.dismiss(); Snackbar.make(mBinding.getRoot(), result ? R.string.import_result_success : R.string.import_result_failed, Snackbar.LENGTH_SHORT).show(); } // The file export has completed. Dismiss the progress dialog and, if there was an error, show a toast. else if (ACTION_EXPORT_COMPLETE.equals(intent.getAction())) { Boolean result = intent.getExtras().getBoolean(EXTRA_EXPORT_RESULT); ProgressDialogFragment dialogFragment = (ProgressDialogFragment) getSupportFragmentManager() .findFragmentByTag(PROGRESS_DIALOG_FRAGMENT_TAG); if (dialogFragment != null) dialogFragment.dismiss(); if (!result) Snackbar.make(mBinding.getRoot(), R.string.export_error, Snackbar.LENGTH_LONG).show(); } } }; /** * Select a team the user picked from the left drawer. */ private final NavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener = new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { Log.v(TAG, "onNavigationItemSelected: " + item.getTitle()); // When running monkey tests, we should load a DB with enough members and some meetings, // before running the tests. If the monkey tries to switch teams, and creates a new team, // it will require many random clicks before he creates a member, and therefore many random // clicks before he is able to create meetings. So, we force the monkey to stay within the existing team. if (ActivityManager.isUserAMonkey()) { Log.v(TAG, "Sorry, monkeys are not allowed to switch teams"); return false; } if (item.getGroupId() == R.id.teams_list_items) { CharSequence selectedTeamName = item.getTitle(); mTeams.switchTeam(selectedTeamName); } else if (item.getItemId() == R.id.action_new_team) { mTeams.promptCreateTeam(); } mBinding.drawerLayout.closeDrawers(); return false; } }; }