eu.focusnet.app.ui.activity.ProjectsListingActivity.java Source code

Java tutorial

Introduction

Here is the source code for eu.focusnet.app.ui.activity.ProjectsListingActivity.java

Source

/*
 * The MIT License (MIT)
 * Copyright (c) 2015 Berner Fachhochschule (BFH) - www.bfh.ch
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
 * and associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package eu.focusnet.app.ui.activity;

import android.app.FragmentManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import eu.focusnet.app.BuildConfig;
import eu.focusnet.app.R;
import eu.focusnet.app.controller.CronService;
import eu.focusnet.app.controller.FocusAppLogic;
import eu.focusnet.app.ui.common.DrawerListAdapter;
import eu.focusnet.app.ui.common.FocusDialogBuilder;
import eu.focusnet.app.ui.common.SimpleListItem;
import eu.focusnet.app.ui.common.UiHelper;
import eu.focusnet.app.ui.fragment.BookmarkFragment;
import eu.focusnet.app.ui.fragment.ProjectsListingFragment;
import eu.focusnet.app.util.ApplicationHelper;
import eu.focusnet.app.util.Constant;
import eu.focusnet.app.util.FocusInternalErrorException;

/**
 * This {@code Activity} contains the listing of all projects, and is therefore the welcome screen
 * of our application after the login and loading screens.
 */
public class ProjectsListingActivity extends ToolbarEnabledActivity {
    /**
     * The layout containing our drawer menu.
     */
    protected DrawerLayout drawerLayout;

    /**
     * The {@code ListView} defining the menu entries of our drawer menu.
     */
    protected ListView drawerListMenu;

    /**
     * Facility for linking the menu drawer and the action bar.
     */
    protected ActionBarDrawerToggle drawerToggle;

    /**
     * List of items to include in the menu drawer.
     */
    protected ArrayList<SimpleListItem> drawerItems;

    /**
     * Previously selected {@code Fragment} identifier.
     */
    private int previouslySelectedSectionToRender;

    /**
     * Currently selected {@code Fragment} identifier.
     */
    private int sectionToRender;

    /**
     * {@link CronService} to link to for allowing the user to trigger data synchronization
     */
    private CronService cronService;

    /**
     * Tells whether the {@link CronService} is bound or not.
     */
    private boolean cronBound = false;

    /**
     * Defines callbacks for {@link CronService} binding, passed to bindService()
     */
    private ServiceConnection cronServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            // We've bound to LocalService, cast the IBinder and get CronService instance
            CronService.CronBinder binder = (CronService.CronBinder) service;
            cronService = binder.getService();
            cronBound = true;
            // if we are in this Activity, this means that the application is ready to display
            // something, so let's update the cron's applicationReady flag that may have not been
            // set if the service was not bound in FocusAppLogic when we tried to do it.
            // Not likely, though.
            cronService.onChangeStatus(true);
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            cronBound = false;
        }
    };

    /**
     * Create the activity.
     * - the parent {@code onCreate()} method will setup and render the UI
     * - we bind to {@link CronService}
     *
     * @param savedInstanceState Inherited.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // this method will take care of building the UI and rendering, using later
        // overriden methods
        super.onCreate(savedInstanceState);

        // bind cron service
        Intent intent = new Intent(this, CronService.class);
        bindService(intent, this.cronServiceConnection, Context.BIND_AUTO_CREATE);

    }

    /**
     * Destroy the activity, and unbind the registered {@link CronService}.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();

        // unbind cron service
        if (this.cronBound) {
            unbindService(this.cronServiceConnection);
            this.cronBound = false;
        }
    }

    /**
     * Defines the layout of this activity.
     *
     * @return Inherited.
     */
    @Override
    protected int getTargetView() {
        return R.layout.activity_projects_listing;
    }

    /**
     * Defines the target container of this Activity.
     *
     * @return Inherited.
     */
    @Override
    protected int getTargetLayoutContainer() {
        return R.id.focus_drawer_content_container;
    }

    /**
     * Prepare the view. This is called in {@link #onCreate(Bundle)}, before actually rendering the
     * UI.
     * <p/>
     * See {@link super#onCreate(Bundle)}.
     */
    @Override
    protected void setupSpecificUiElements() {
        LayoutInflater inflater = getLayoutInflater();

        // set the default section that will initially be loaded
        this.sectionToRender = this.previouslySelectedSectionToRender = Constant.Navigation.UI_MENU_ENTRY_PROJECTS_LISTING;

        // create a Drawer
        this.drawerItems = this.getDrawerItems();
        this.drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        this.drawerListMenu = (ListView) findViewById(R.id.focus_drawer);
        if (this.drawerListMenu == null) {
            throw new FocusInternalErrorException("Cannot retrieve drawer view.");
        }

        // get the header and add it to the ListView
        // FIXME pass something else as the Context
        LinearLayout menuHeader = (LinearLayout) inflater.inflate(R.layout.include_drawer_header, null);
        menuHeader.setEnabled(false);
        menuHeader.setOnClickListener(null);
        this.drawerListMenu.addHeaderView(menuHeader, null, false);
        // adjust content of header
        /** @deprecated for production version, we should put the name/email of the user */
        String appVersion = getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME;
        TextView appInfo = (TextView) findViewById(R.id.drawer_app_info);
        if (appInfo != null) {
            appInfo.setText(appVersion);
        }
        // scroll the menu to the very top (at the top of the header)
        // set in a runnable such that this is called when the system decides, an not too early.
        this.drawerListMenu.post(new Runnable() {
            @Override
            public void run() {
                drawerListMenu.smoothScrollToPositionFromTop(0, 0);
            }
        });

        this.drawerLayout.bringToFront();
        this.drawerListMenu.bringToFront();
        this.drawerListMenu.setOnItemClickListener(this.getOnClickDrawerItemListener());

        // Assign a ListAdapter to our Drawer
        DrawerListAdapter adapter = new DrawerListAdapter(ApplicationHelper.getApplicationContext(),
                this.drawerItems);
        drawerListMenu.setAdapter(adapter);

        this.drawerToggle = new ActionBarDrawerToggle(this, this.drawerLayout, this.toolbar,
                R.string.focus_drawer_open_description, R.string.focus_drawer_close_description);

        // overrides getSupportActionBar().setDisplayHomeAsUpEnabled
        this.drawerToggle.setDrawerIndicatorEnabled(true);

        // Set the drawer toggle as the DrawerListener
        this.drawerLayout.addDrawerListener(drawerToggle);

        // set a subtitle
        this.actionBar.setSubtitle(FocusAppLogic.getCurrentApplicationContent().getTitle());
    }

    /**
     * Do any UI operation that do not imply creating and including a new Fragment in the UI.
     * The following is performed depending on which menu item is selected:
     * - if projects or bookmarks listing: highlight the appropriate menu item
     * - if About, then display a dialog with the information about the app
     * - If Logout, then reset the application
     * <p/>
     * See {@link super#applyUiChanges()}.
     */
    @Override
    protected void doInPageUiOperations() {
        switch (this.sectionToRender) {
        case Constant.Navigation.UI_MENU_ENTRY_PROJECTS_LISTING:
        case Constant.Navigation.UI_MENU_ENTRY_BOOKMARK:
            highlightSelectedMenuItem(this.sectionToRender);
            break;
        case Constant.Navigation.UI_MENU_ENTRY_ABOUT:
            LayoutInflater inflater = LayoutInflater.from(this);
            // FIXME set a better Context than null
            final WebView dialogContent = (WebView) inflater.inflate(R.layout.dialog_content_about, null);
            dialogContent.loadUrl("file:///android_asset/" + Constant.AppConfig.ASSETS_ABOUT_PAGE);
            final FocusDialogBuilder builder = new FocusDialogBuilder(this).removeNeutralButton()
                    .removeNegativeButton().removePositiveButton().setCancelable(true).insertContent(dialogContent)
                    .setTitle(getString(R.string.about_focus_title));
            AlertDialog dialog = builder.create();
            dialog.show();
            break;
        case Constant.Navigation.UI_MENU_ENTRY_LOGOUT:
            final Thread logoutThread = new Thread() {
                public void run() {
                    // reset all when logging out
                    FocusAppLogic.getInstance().reset();
                    // redirect to the EntryPointActivity
                    try {
                        Intent i = new Intent(ProjectsListingActivity.this, EntryPointActivity.class);
                        i.putExtra(Constant.Extra.UI_EXTRA_LOADING_INFO_TEXT,
                                getString(R.string.wiping_user_data_logout_msg));
                        startActivity(i);
                    } finally {
                        finish();
                    }
                }
            };
            logoutThread.start();
            break;
        default:
            break;
        }

        // close the drawer and cleanup
        if (this.sectionToRender != 0) {
            drawerLayout.closeDrawer(this.drawerListMenu);

            // do not keep selection on the current item
            switch (this.sectionToRender) {
            case Constant.Navigation.UI_MENU_ENTRY_ABOUT:
                highlightSelectedMenuItem(this.previouslySelectedSectionToRender);
                break;
            }
        }
    }

    /**
     * Defines the Fragment to include in the container for this Activity.
     * Either the project listing fragment or the bookmarks listing fragment.
     * <p/>
     * See {@link super#applyUiChanges()}.
     */
    @Override
    protected void prepareNewFragment() {
        switch (this.sectionToRender) {
        case Constant.Navigation.UI_MENU_ENTRY_PROJECTS_LISTING:
            this.fragment = new ProjectsListingFragment();
            if (this.actionBar != null) {
                this.actionBar.setTitle(R.string.project_listing_title);
            }
            break;
        case Constant.Navigation.UI_MENU_ENTRY_BOOKMARK:
            this.fragment = new BookmarkFragment();
            if (this.actionBar != null) {
                this.actionBar.setTitle(R.string.bookmarks_title);
            }
            break;
        default:
            break;
        }
    }

    /**
     * Create the option menu of the app
     *
     * @param menu the menu
     * @return true
     */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_projects_listing, menu);
        return true;
    }

    /**
     * This method gives a list of items to display in the left drawer.
     *
     * @return An array list of items to display in the drawer
     */
    private ArrayList<SimpleListItem> getDrawerItems() {
        // load menu items and icons
        String[] navMenuTitles = getResources().getStringArray(R.array.drawer_items);
        TypedArray navMenuIcons = getResources().obtainTypedArray(R.array.drawer_icons);

        ArrayList<SimpleListItem> drawerItems = new ArrayList<>();

        for (int i = 0; i < navMenuTitles.length; i++) {
            String menuTitle = navMenuTitles[i];
            SimpleListItem drawListItem = new SimpleListItem(
                    UiHelper.getBitmap(this, navMenuIcons.getResourceId(i, -1)), menuTitle);
            drawerItems.add(drawListItem);
        }

        // Release the typed array
        navMenuIcons.recycle();

        return drawerItems;
    }

    /**
     * Create a click listener tailored for the Drawer menu.
     *
     * @return A click listener.
     */
    private ListView.OnItemClickListener getOnClickDrawerItemListener() {
        return new ListView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (position != Constant.Navigation.UI_MENU_ENTRY_HEADER_NON_CLICKABLE) {
                    boolean doIt = true;
                    switch (position) {
                    case Constant.Navigation.UI_MENU_ENTRY_PROJECTS_LISTING:
                    case Constant.Navigation.UI_MENU_ENTRY_BOOKMARK:
                        if (position == sectionToRender) {
                            doIt = false;
                        }
                        break;
                    default:
                        break;
                    }
                    if (doIt) {
                        previouslySelectedSectionToRender = sectionToRender;
                        sectionToRender = position;
                        applyUiChanges();
                    }
                    drawerLayout.closeDrawer(drawerListMenu);
                }
            }
        };
    }

    /**
     * Listener for the back button. We have 2 menu entries in the drawer menu that load their
     * fragment in the main content container.
     * <p/>
     * In {@link #getOnClickDrawerItemListener()}, we make sure we do not reload the same page
     * indefinitely such that the below logic can work.
     */
    @Override
    public void onBackPressed() {
        //Navigate back to the last Fragment or exit the application
        FragmentManager fragmentManager = getFragmentManager();
        // If we have more than one fragments in the stack remove the last inserted
        // This is the case where we move from Bookmarks back to the home page via the BACK button
        if (fragmentManager.getBackStackEntryCount() > 1) {
            //remove the last inserted fragment from the stack
            fragmentManager.popBackStackImmediate();

            // hard coded things is ok as we are only playing with 2 possible targets, and
            // hence hitting the BACK button will always make us go back to the home page
            // (if we hit the BACK button on the home page, we exit the app)
            this.actionBar.setTitle(R.string.project_listing_title);
            highlightSelectedMenuItem(Constant.Navigation.UI_MENU_ENTRY_PROJECTS_LISTING);

            // close the drawer
            this.drawerLayout.closeDrawer(this.drawerListMenu);
        } else {
            // Exit the application
            super.onBackPressed();
        }
    }

    /**
     * Highlight the currently selected menu item
     *
     * @param position 1-based position of the menu item to highlight.
     */
    private void highlightSelectedMenuItem(int position) {
        this.drawerListMenu.setItemChecked(position, true);
        this.drawerListMenu.setSelection(position);
    }

    /**
     * Required by {@link ActionBarDrawerToggle}.
     *
     * @param newConfig Inherited.
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        this.drawerToggle.onConfigurationChanged(newConfig);
    }

    /**
     * Required by {@link ActionBarDrawerToggle}.
     *
     * @param savedInstanceState Inherited.
     */
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        this.drawerToggle.syncState();
    }

    /**
     * When clicking the action bar buttons:
     * - display an alert dialog that allows to trigger data synchronization or displays the current
     * status of the synchronization.
     *
     * @param item Inherited.
     * @return Inherited.
     */
    @Override
    public boolean onOptionsItemSelected(final MenuItem item) {
        switch (item.getItemId()) {
        case R.id.action_sync:

            if (!this.cronBound) {
                return false;
            }

            LayoutInflater inflater = getLayoutInflater();
            // FIXME pass a better Context than null.
            final LinearLayout dialogContent = (LinearLayout) inflater
                    .inflate(R.layout.dialog_content_synchronization, null);

            final Context context = this;
            final FocusDialogBuilder builder = new FocusDialogBuilder(context).removeNeutralButton()
                    .setNegativeButtonText(getString(R.string.cancel))
                    .setPositiveButtonText(getString(R.string.start)).setCancelable(false)
                    .insertContent(dialogContent).setTitle(getString(R.string.data_sync_title));
            final AlertDialog dialog = builder.create();
            dialog.show();

            // Cancelation listener
            builder.getNegativeButton().setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    builder.setNegativeButtonText(getString(R.string.sync_continue_background));
                    dialog.dismiss();
                }
            });

            final View instructions = dialogContent.findViewById(R.id.dialog_sync_instructions);
            final View progress = dialogContent.findViewById(R.id.dialog_sync_progress);
            final View status = dialogContent.findViewById(R.id.dialog_sync_status);
            final TextView instructionsField = (TextView) instructions
                    .findViewById(R.id.dialog_sync_instructions_msg);
            final TextView statusField = (TextView) status.findViewById(R.id.dialog_sync_status_field);

            // update dynamic content (last sync and last data volume)
            String lastSync;
            if (this.cronService.getLastSync() == 0) {
                lastSync = getString(R.string.n_a);
            } else {
                DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance();
                lastSync = dateFormat.format(new Date(this.cronService.getLastSync()));
            }
            TextView lastSyncField = (TextView) instructions.findViewById(R.id.dialog_sync_last_sync_field);
            lastSyncField.setText(getString(R.string.last_sync_label) + lastSync);
            TextView lastDataVolumeField = (TextView) instructions.findViewById(R.id.dialog_sync_data_volume_field);

            long rawDbSize = FocusAppLogic.getDataManager().getDatabaseSize();
            lastDataVolumeField.setText(getString(R.string.last_sync_data_volume_label)
                    + (rawDbSize == 0 ? "N/A" : UiHelper.getFileSize(rawDbSize)));

            // too early since last sync
            // display instructions with custom message, disable START
            if (cronService.getLastSync() >= System.currentTimeMillis()
                    - Constant.AppConfig.CRON_SERVICE_MINIMUM_DURATION_BETWEEN_SYNC_DATA_IN_MILLISECONDS) {
                instructionsField.setText(R.string.sync_too_recent);
                builder.removePositiveButton();
                builder.setNegativeButtonText(getString(R.string.close));
            }
            // ongoing sync
            // display status and remove START
            else if (this.cronService.getSyncInProgress()) {
                builder.removePositiveButton();
                builder.setNegativeButtonText(getString(R.string.close));

                statusField.setText(R.string.sync_already_in_progress);
                statusField.setTextColor(getResources().getColor(R.color.orange));

                instructions.setVisibility(View.GONE);
                status.setVisibility(View.VISIBLE);
            }
            // Otherwise, set information about last synchronization and data volume and expect the user to start a manual synchronization
            else {
                // create START listener
                builder.getPositiveButton().setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        builder.removePositiveButton();
                        builder.setNegativeButtonText(getString(R.string.sync_continue_background));
                        instructions.setVisibility(View.GONE);
                        progress.setVisibility(View.VISIBLE);

                        // Run the periodic task
                        new SyncTask(builder, dialogContent, dialog)
                                .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
                    }
                });
            }
            return true;

        default:
            // If we got here, the user's action was not recognized.
            // Invoke the superclass to handle it.
            // toggle nav drawer on selecting action bar app icon/title
            if (drawerToggle.onOptionsItemSelected(item)) {
                return true;
            }
            // Handle action bar actions click
            switch (item.getItemId()) {
            default:
                return super.onOptionsItemSelected(item);
            }
        }
    }

    /**
     * Task responsible for synchronizing data
     * <p/>
     * Called when the user explicitly launches data sync via the dialog of the action bar.
     */
    private class SyncTask extends AsyncTask<Void, Void, Void> {

        /**
         * Layout containing the content of the dialog
         */
        final private LinearLayout dialogContent;

        /**
         * Dialog builder
         */
        final private FocusDialogBuilder builder;

        /**
         * The dialog being displayed.
         */
        final private AlertDialog dialog;

        /**
         * Constructor.
         *
         * @param builder       Input value for setting instance variable.
         * @param dialogContent Input value for setting instance variable.
         * @param dialog        Input value for setting instance variable.
         */
        public SyncTask(FocusDialogBuilder builder, LinearLayout dialogContent, AlertDialog dialog) {
            this.builder = builder;
            this.dialogContent = dialogContent;
            this.dialog = dialog;
        }

        /**
         * Perform the data synchronization in the background.
         *
         * @param params Nothing
         * @return Nothing
         */
        @Override
        protected Void doInBackground(Void... params) {
            if (!cronBound) {
                return null;
            }
            boolean newSync = cronService.syncData();

            // if already started (periodic execution), let's just wait for completion
            if (!newSync) {
                while (cronService.getSyncInProgress()) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ignored) {

                    }
                }
            }
            return null;
        }

        /**
         * After execution, update the dialog to reflect success or failure.
         *
         * @param v Nothing
         */
        @Override
        protected void onPostExecute(Void v) {
            // setStatus()
            final View progress = this.dialogContent.findViewById(R.id.dialog_sync_progress);
            final View status = this.dialogContent.findViewById(R.id.dialog_sync_status);
            final TextView statusField = (TextView) status.findViewById(R.id.dialog_sync_status_field);
            progress.setVisibility(View.GONE);
            status.setVisibility(View.VISIBLE);
            statusField.setText(R.string.done);
            statusField.setTextColor(getResources().getColor(R.color.colorPrimary));

            this.builder.getNegativeButton().setText(R.string.close);
            this.builder.getNegativeButton().setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    dialog.dismiss();
                    finish();
                    startActivity(new Intent(ProjectsListingActivity.this, ProjectsListingActivity.class));
                }
            });
        }
    }

}