ca.spencerelliott.mercury.Changesets.java Source code

Java tutorial

Introduction

Here is the source code for ca.spencerelliott.mercury.Changesets.java

Source

package ca.spencerelliott.mercury;

/************************************************************************
 * This file is part of Mercury.
 *
 * Mercury 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.
 *
 * Mercury 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 Mercury.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Spencer Elliott
 * @author spencer@spencerelliott.ca
 ************************************************************************/

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import org.apache.http.client.ClientProtocolException;
import org.xml.sax.SAXException;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Xml;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.provider.Contacts.People;
import android.database.Cursor;

public class Changesets extends Activity {
    //Views for the activity
    private ListView changesets_listview;
    private ArrayList<Map<String, String>> contacts_list;
    private ArrayList<Map<String, ?>> changesets_list;
    private ArrayList<Beans.ChangesetBean> changesets_data;
    private volatile Thread load_thread;

    private int current_bean_count = 0;

    //Handler messages
    private final int SUCCESSFUL = 0;
    private final int CANCELLED = 1;
    private final int SETUP_COUNT = 2;
    private final int UPDATE_PROGRESS = 3;

    private boolean is_search_window = false;

    //Handles the changing of the adapter in the list
    private Handler list_handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case SUCCESSFUL:
                //Put here just in case
                break;
            case CANCELLED:
                //The dialog was cancelled
                changesets_list.clear();
                break;
            case SETUP_COUNT:
                //Setup progress bar
                current_bean_count = msg.getData().getInt("max");
                break;
            case UPDATE_PROGRESS:
                //Update progress bar
                if (current_bean_count != 0)
                    getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
                            msg.getData().getInt("progress") * (9999 / current_bean_count));
                else
                    getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 10000);
                break;
            }

            if (msg.what == SUCCESSFUL || msg.what == CANCELLED) {
                //Set the data of the new list to show up in the list view
                changesets_listview.setAdapter(new SimpleAdapter(Changesets.this, changesets_list,
                        R.layout.changeset_item, new String[] { COMMIT, FORMATTED_INFO },
                        new int[] { R.id.changesets_commit, R.id.changesets_info }));
                setProgressBarVisibility(false);
            }
        }
    };

    //Group for context menu
    private final int CONTACT_GROUP = 1;

    //Labels for the context menu
    private final int EMAIL_PERSON = Menu.FIRST;
    private final int VIEW_BROWSER = Menu.FIRST + 1;
    private final int LINK_COMMITTER = Menu.FIRST + 2;
    private final int MESSAGE_COMMITTER = Menu.FIRST + 3;
    private final int UNLINK_COMMITTER = Menu.FIRST + 4;

    private final int REFRESH = Menu.FIRST + 3;
    private final int SEARCH = Menu.FIRST + 4;

    //Labels for the hash map
    private final String COMMIT = "commit";
    private final String FORMATTED_INFO = "info";

    //Globals needed to keep track of a couple things
    private int last_item_touched = -1;
    private long repo_id = -1;

    //Creates a new item for the list view based on the commit text and info
    private Map<String, ?> createChangeset(String commit_text, String info) {
        Map<String, String> changeset_data = new HashMap<String, String>();

        //Put the strings in to the appropriate key in the hash map
        changeset_data.put(COMMIT, commit_text);
        changeset_data.put(FORMATTED_INFO, info);

        return changeset_data;
    }

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);

        this.requestWindowFeature(Window.FEATURE_PROGRESS);
        this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        setContentView(R.layout.changesets);

        //Cancel any notifications previously setup
        NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(1);

        //Create a new array list for the changesets
        changesets_list = new ArrayList<Map<String, ?>>();
        changesets_data = new ArrayList<Beans.ChangesetBean>();

        //Get the list view to store the changesets
        changesets_listview = (ListView) findViewById(R.id.changesets_list);

        TextView empty_text = (TextView) findViewById(R.id.changesets_empty_text);

        //Set the empty view
        changesets_listview.setEmptyView(empty_text);

        //Use a simple adapter to display the changesets based on the array list made earlier
        changesets_listview.setAdapter(new SimpleAdapter(this, changesets_list, R.layout.changeset_item,
                new String[] { COMMIT, FORMATTED_INFO },
                new int[] { R.id.changesets_commit, R.id.changesets_info }));

        //Set the on click listener
        changesets_listview.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterview, View view, int position, long id) {
                Intent intent = new Intent(Changesets.this, ChangesetViewer.class);

                //Pass the changeset information to the changeset viewer
                Bundle params = new Bundle();
                params.putString("changeset_commit_text", changesets_data.get(position).getTitle());
                params.putString("changeset_changes", changesets_data.get(position).getContent());
                params.putLong("changeset_updated", changesets_data.get(position).getUpdated());
                params.putString("changeset_authors", changesets_data.get(position).getAuthor());
                params.putString("changeset_link", changesets_data.get(position).getLink());
                //params.putBoolean("is_https", is_https);

                intent.putExtras(params);

                startActivity(intent);
            }

        });

        //Register the list view for opening the context menu
        registerForContextMenu(changesets_listview);

        //Get the intent passed by the program
        if (getIntent() != null) {
            //Check to see if this is a search window
            if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
                //Change the title of the activity and the empty text of the list so it looks like a search window
                this.setTitle(R.string.search_results_label);
                empty_text.setText(R.string.search_results_empty);

                //Retrieve the query the user entered
                String query = getIntent().getStringExtra(SearchManager.QUERY);

                //Convert the query to lower case
                query = query.toLowerCase();

                //Retrieve the bundle data
                Bundle retrieved_data = getIntent().getBundleExtra(SearchManager.APP_DATA);

                //If the bundle was passed, grab the changeset data
                if (retrieved_data != null) {
                    changesets_data = retrieved_data
                            .getParcelableArrayList("ca.spencerelliott.mercury.SEARCH_DATA");
                }

                //If we're missing changeset data, stop here
                if (changesets_data == null)
                    return;

                //Create a new array list to store the changesets that were a match
                ArrayList<Beans.ChangesetBean> search_beans = new ArrayList<Beans.ChangesetBean>();

                //Loop through each changeset
                for (Beans.ChangesetBean b : changesets_data) {
                    //Check to see if any changesets match
                    if (b.getTitle().toLowerCase().contains(query)) {
                        //Get the title and date of the commit
                        String commit_text = b.getTitle();
                        Date commit_date = new Date(b.getUpdated());

                        //Add a new changeset to display in the list view
                        changesets_list.add(createChangeset(
                                (commit_text.length() > 30 ? commit_text.substring(0, 30) + "..." : commit_text),
                                b.getRevisionID() + " - " + commit_date.toLocaleString()));

                        //Add this bean to the list of found search beans
                        search_beans.add(b);
                    }
                }

                //Switch the changeset data over to the changeset data that was a match
                changesets_data = search_beans;

                //Update the list in the activity
                list_handler.sendEmptyMessage(SUCCESSFUL);

                //Notify the activity that it is a search window
                is_search_window = true;

                //Stop loading here
                return;
            }

            //Get the data from the intent
            Uri data = getIntent().getData();

            if (data != null) {
                //Extract the path in the intent
                String path_string = data.getEncodedPath();

                //Split it by the forward slashes
                String[] split_path = path_string.split("/");

                //Make sure a valid path was passed
                if (split_path.length == 3) {
                    //Get the repository id from the intent
                    repo_id = Long.parseLong(split_path[2].toString());
                } else {
                    //Notify the user if there was a problem
                    Toast.makeText(this, R.string.invalid_intent, 1000).show();
                }
            }
        }

        //Retrieve the changesets
        refreshChangesets();
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);
        int i = 0;

        //Get the context menu info from the adapter
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;

        //Find which item has been selected
        last_item_touched = info.position;

        //Create the menu
        //menu.add(0, EMAIL_PERSON, ++i, R.string.changesets_link_contact);
        menu.add(0, VIEW_BROWSER, ++i, R.string.changesets_view_browser);//.setEnabled(!is_https);

        SubMenu link_menu = menu.addSubMenu(0, LINK_COMMITTER, ++i, R.string.changesets_link_contact);

        if (contacts_list == null) {
            //Create the contacts list
            contacts_list = new ArrayList<Map<String, String>>();

            //Store which columns are needed from the contact
            String[] columns = { People._ID, People.NAME };

            //Get the Uri to the contacts content provider
            Uri contacts = People.CONTENT_URI;

            //Run the query to get all of the contacts on the device
            Cursor all_contacts = managedQuery(contacts, columns, null, null, People.NAME + " ASC");

            //Store the column number of the name and id from the content provider
            int id_column = all_contacts.getColumnIndex(People._ID);
            int name_column = all_contacts.getColumnIndex(People.NAME);

            //If there are columns
            if (all_contacts.moveToFirst()) {
                //Loop through each contact and add them to the list
                do {
                    //Create a new contact map
                    Map<String, String> new_contact = new HashMap<String, String>();

                    //Add the id and name to the map
                    new_contact.put("id", all_contacts.getString(id_column));
                    new_contact.put("name", all_contacts.getString(name_column));

                    //Add the new contact to the list
                    contacts_list.add(new_contact);
                } while (all_contacts.moveToNext());
            }
        }

        //Set count to -1 since the loop pre-increments the variable so the first
        //used value of count will be 0
        int count = -1;

        //Add all the contacts to the sub-menu
        for (Map<String, String> c : contacts_list) {
            link_menu.add(CONTACT_GROUP, Integer.parseInt(c.get("id")), ++count, c.get("name"));
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        super.onContextItemSelected(item);

        //Check to see if this selection was a contact selected from the sub menu
        if (item.getGroupId() == CONTACT_GROUP) {
            //Setup committer linking here

            return false;
        }

        //Called if the user clicked on the option to email the committer
        if (item.getItemId() == EMAIL_PERSON) {
            Intent intent = new Intent(Intent.ACTION_VIEW,
                    Uri.parse("mailto:test@test.com?subject=Regarding revision..."));
            startActivity(intent);
        }

        //Opens up the changeset on the native web page
        if (item.getItemId() == VIEW_BROWSER) {
            Intent intent = new Intent(Intent.ACTION_VIEW,
                    Uri.parse(changesets_data.get(last_item_touched).getLink()));

            if (intent != null)
                startActivity(intent);
        }

        //Open the window to link the committer with a contact in the device
        if (item.getItemId() == LINK_COMMITTER) {

        }

        //Message the contact linked to the committer
        if (item.getItemId() == MESSAGE_COMMITTER) {

        }

        //Unlink the contact from the committer
        if (item.getItemId() == UNLINK_COMMITTER) {

        }

        return true;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        //For some reason android.R.drawable.ic_menu_refresh is missing so I needed to grab the 
        //actual file from the SDK and include it in the project
        if (!is_search_window) {
            menu.add(0, REFRESH, 0, R.string.refresh).setIcon(R.drawable.ic_menu_refresh);
            menu.add(0, SEARCH, 1, R.string.changesets_search_option).setIcon(android.R.drawable.ic_menu_search);
        }

        return true;
    }

    @Override
    public boolean onMenuItemSelected(int featureId, MenuItem item) {
        super.onMenuItemSelected(featureId, item);

        switch (item.getItemId()) {
        //Refresh the changeset list if the user selected "Refresh"
        case REFRESH:
            refreshChangesets();
            break;
        case SEARCH:
            onSearchRequested();
            break;
        }

        return true;
    }

    @Override
    public void onConfigurationChanged(Configuration config) {
        //Just switch the layout without respawning the activity
        super.onConfigurationChanged(config);
    }

    @Override
    public boolean onSearchRequested() {
        //If it's already a search window, block the search from happening again
        if (is_search_window)
            return false;

        //Create a new bundle to store the changeset data
        Bundle app_data = new Bundle();

        //Pack up the array list data in to a parcel
        app_data.putParcelableArrayList("ca.spencerelliott.mercury.SEARCH_DATA", changesets_data);

        //Start the search
        startSearch(null, false, app_data, false);
        return true;
    }

    public void refreshChangesets() {
        //Set up the window to show the progress dialog in the title bar
        this.getWindow().setFeatureInt(Window.PROGRESS_VISIBILITY_ON, 1);
        this.getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 0);
        this.setProgressBarVisibility(true);

        //Create a new thread to process all the data in the background
        startThread();
    }

    private synchronized void startThread() {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext());

        //Create the thread that will process the incoming feed
        load_thread = new Thread() {
            @Override
            public void run() {
                changesets_list.clear();

                DatabaseHelper db_helper = DatabaseHelper.getInstance(getApplicationContext());
                EncryptionHelper encrypt_helper = EncryptionHelper.getInstance("DEADBEEF".toCharArray(),
                        new byte[] { 'L', 'O', 'L' });

                //Get the repository information from the local database
                Beans.RepositoryBean repo_type = db_helper.getRepository(repo_id, encrypt_helper);
                AtomHandler feed_handler = null;

                //Detect the type of repository and create a parser based on that
                switch (repo_type.getType()) {
                case Mercury.RepositoryTypes.HGSERVE:
                    feed_handler = new HGWebAtomHandler();
                    break;
                case Mercury.RepositoryTypes.GOOGLECODE:
                    feed_handler = new GoogleCodeAtomHandler();
                    break;
                case Mercury.RepositoryTypes.BITBUCKET:
                    feed_handler = new BitbucketAtomHandler();
                    break;
                case Mercury.RepositoryTypes.CODEPLEX:
                    feed_handler = new CodePlexAtomHandler();
                    break;
                }

                HttpURLConnection conn = null;
                boolean connected = false;

                try {
                    // XXX We need to use our own factory to make all ssl certs work
                    HttpsURLConnection.setDefaultSSLSocketFactory(NaiveSSLSocketFactory.getSocketFactory());

                    String repo_url_string = (repo_type.getUrl().endsWith("/") || repo_type.getUrl().endsWith("\\")
                            ? feed_handler
                                    .formatURL(repo_type.getUrl().substring(0, repo_type.getUrl().length() - 1))
                            : feed_handler.formatURL(repo_type.getUrl()));

                    switch (repo_type.getType()) {
                    case Mercury.RepositoryTypes.BITBUCKET:
                        //Only add the token if the user requested it
                        if (repo_type.getAuthentication() == Mercury.AuthenticationTypes.TOKEN)
                            repo_url_string = repo_url_string + "?token=" + repo_type.getSSHKey();
                        break;
                    }

                    URL repo_url = new URL(repo_url_string);
                    conn = (HttpURLConnection) repo_url.openConnection();

                    //Check to see if the user enabled HTTP authentication
                    if (repo_type.getAuthentication() == Mercury.AuthenticationTypes.HTTP) {
                        //Get their username and password
                        byte[] decrypted_info = (repo_type.getUsername() + ":" + repo_type.getPassword())
                                .getBytes();

                        //Add the header to the http request
                        conn.setRequestProperty("Authorization", "Basic " + Base64.encodeBytes(decrypted_info));
                    }
                    conn.connect();
                    connected = true;
                } catch (ClientProtocolException e2) {
                    AlertDialog.Builder alert = new AlertDialog.Builder(getBaseContext());
                    alert.setMessage("There was a problem with the HTTP protocol");
                    alert.setPositiveButton(android.R.string.ok, null);
                    alert.show();

                    //Do not allow the app to continue with loading
                    connected = false;
                } catch (IOException e2) {
                    AlertDialog.Builder alert = new AlertDialog.Builder(getBaseContext());
                    alert.setMessage("Server did not respond with a valid HTTP response");
                    alert.setPositiveButton(android.R.string.ok, null);
                    alert.show();

                    //Do not allow the app to continue with loading
                    connected = false;
                } catch (NullPointerException e3) {

                } catch (Exception e) {

                }

                BufferedReader reader = null;

                //Create a new reader based on the information retrieved
                if (connected) {
                    try {
                        reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                    } catch (IllegalStateException e1) {
                        e1.printStackTrace();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                } else {
                    list_handler.sendEmptyMessage(CANCELLED);
                    return;
                }

                //Make sure both the feed handler and info loaded from the web are not null
                if (reader != null && feed_handler != null) {
                    try {
                        Xml.parse(reader, feed_handler);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (SAXException e) {
                        e.printStackTrace();
                    }
                } else {
                    list_handler.sendEmptyMessage(CANCELLED);
                    return;
                }

                //Stored beans in the devices database
                ArrayList<Beans.ChangesetBean> stored_beans = null;

                if (prefs.getBoolean("caching", false)) {
                    long last_insert = db_helper.getHighestID(DatabaseHelper.DB_TABLE_CHANGESETS, repo_id);

                    if (last_insert >= 0) {
                        //Get all of the stored changesets
                        stored_beans = db_helper.getAllChangesets(repo_id, null);

                        String rev_id = "";

                        //Try to find the revision id of the bean that has the id of the last inserted value
                        for (Beans.ChangesetBean b : stored_beans) {
                            if (b.getID() == last_insert) {
                                rev_id = b.getRevisionID();
                                break;
                            }
                        }

                        //Trim the list starting from this revision
                        feed_handler.trimStartingFromRevision(rev_id);
                    }
                }

                //Create a new bundle for the progress
                Bundle progress_bundle = new Bundle();

                //Retreive all the beans from the handler
                ArrayList<Beans.ChangesetBean> beans = feed_handler.getAllChangesets();
                int bean_count = beans.size();

                //Store the amount of changesets
                progress_bundle.putInt("max", bean_count);

                //Create a new message and store the bundle and what type of message it is
                Message msg = new Message();
                msg.setData(progress_bundle);
                msg.what = SETUP_COUNT;
                list_handler.sendMessage(msg);

                //Add each of the beans to the list
                for (int i = 0; i < bean_count; i++) {
                    String commit_text = beans.get(i).getTitle();
                    Date commit_date = new Date(beans.get(i).getUpdated());
                    changesets_list.add(createChangeset(
                            (commit_text.length() > 30 ? commit_text.substring(0, 30) + "..." : commit_text),
                            beans.get(i).getRevisionID() + " - " + commit_date.toLocaleString()));

                    //Store the current progress of the changeset loading
                    progress_bundle.putInt("progress", i);

                    //Reuse the old message and send an update progress message
                    msg = new Message();
                    msg.setData(progress_bundle);
                    msg.what = UPDATE_PROGRESS;
                    list_handler.sendMessage(msg);
                }

                //Get the current count of changesets and the shared preferences
                long changeset_count = db_helper.getChangesetCount(repo_id);

                if (prefs.getBoolean("caching", false)) {
                    //Get all of the stored beans from the device if not already done
                    if (stored_beans == null)
                        stored_beans = db_helper.getAllChangesets(repo_id, null);

                    //Add all the changesets from the device
                    for (Beans.ChangesetBean b : stored_beans) {
                        changesets_list.add(createChangeset(
                                (b.getTitle().length() > 30 ? (b.getTitle().substring(0, 30)) + "..."
                                        : b.getTitle()),
                                b.getRevisionID() + " - " + new Date(b.getUpdated()).toLocaleString()));
                    }

                    //Reverse the list so the oldest changesets are stored first
                    Collections.reverse(beans);

                    //Iterate through each bean and add it to the device's database
                    for (Beans.ChangesetBean b : beans) {
                        db_helper.insert(b, repo_id);
                    }

                    //Get the amount of changesets allowed to be stored on the device
                    int max_changes = Integer.parseInt(prefs.getString("max_changesets", "-1"));

                    //Delete the oldest changesets if too many have been stored
                    if (changeset_count > max_changes) {
                        db_helper.deleteNumChangesets(repo_id, (changeset_count - max_changes));
                    }
                } else if (changeset_count > 0) {
                    //Since the user does not have caching enabled, delete the changesets
                    db_helper.deleteAllChangesets(repo_id);
                }

                //Update the tables to the newest revision
                if (!beans.isEmpty())
                    db_helper.updateLastRev(repo_id, beans.get(0).getRevisionID());

                //Add all of the data to the changeset list
                changesets_data.addAll(beans);

                if (prefs.getBoolean("caching", false))
                    changesets_data.addAll(stored_beans);

                //Clean up the sql connection
                db_helper.cleanup();
                db_helper = null;

                //Notify the handler that the loading of the list was successful
                list_handler.sendEmptyMessage(SUCCESSFUL);
            }
        };

        //Start the thread
        load_thread.start();
    }

    private synchronized void stopThread() {
        //Make sure the load thread exists before attempting to stop it
        if (load_thread != null) {
            //Save the old thread
            Thread old_thread = load_thread;

            //Get rid of the old thread
            load_thread = null;

            //Interrupt the old thread
            old_thread.interrupt();
        }
    }
}