se.chalmers.watchme.activity.MovieDetailsActivity.java Source code

Java tutorial

Introduction

Here is the source code for se.chalmers.watchme.activity.MovieDetailsActivity.java

Source

/**
*   MovieDetailsActivity.java
*
*   @author Robin Andersson
*   @copyright (c) 2012 Johan Brook, Robin Andersson, Lisa Stenberg, Mattias Henriksson
*   @license MIT
*/

package se.chalmers.watchme.activity;

import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

import se.chalmers.watchme.R;
import se.chalmers.watchme.database.DatabaseAdapter;
import se.chalmers.watchme.model.Movie;
import se.chalmers.watchme.model.Tag;
import se.chalmers.watchme.net.IMDBHandler;
import se.chalmers.watchme.net.ImageDownloadTask;
import se.chalmers.watchme.net.MovieSource;
import se.chalmers.watchme.ui.DatePickerFragment;
import se.chalmers.watchme.ui.DatePickerFragment.DatePickerListener;
import se.chalmers.watchme.ui.ImageDialog;
import se.chalmers.watchme.utils.DateTimeUtils;
import se.chalmers.watchme.utils.MovieHelper;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.NavUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RatingBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;

public class MovieDetailsActivity extends FragmentActivity implements DatePickerListener {

    public static final String MOVIE_EXTRA = "movie";

    private Movie movie;
    private MovieSource imdb;

    private AsyncTask<String, Void, Bitmap> imageTask;

    // Timeout for fetching IMDb info (in milliseconds)
    private final static int IMDB_FETCH_TIMEOUT = 10000;

    private ImageView poster;
    private ImageDialog dialog;

    private EditText tagField;
    private EditText noteField;
    private Button imdbButton;

    private RatingBar myRatingBar;

    private DatabaseAdapter db;

    private Calendar tempReleaseDate;

    private Menu menu;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_movie_details);
        getActionBar().setDisplayHomeAsUpEnabled(true);

        db = new DatabaseAdapter(getContentResolver());

        this.movie = (Movie) getIntent().getSerializableExtra(MOVIE_EXTRA);
        this.imdb = new IMDBHandler();

        this.poster = (ImageView) findViewById(R.id.poster);
        this.poster.setOnClickListener(new OnPosterClickListener());

        this.imdbButton = (Button) findViewById(R.id.browser_button);
        this.imdbButton.setEnabled(false);

        this.tagField = (EditText) findViewById(R.id.tag_field_details);
        this.noteField = (EditText) findViewById(R.id.note_field_details);

        this.myRatingBar = (RatingBar) findViewById(R.id.my_rating_bar);
        this.myRatingBar.setEnabled(false); // Unable to do this in XML (?)

        this.dialog = new ImageDialog(this);

        // Hide the progress spinner on init
        findViewById(R.id.imdb_loading_spinner).setVisibility(View.INVISIBLE);

        /*
        * Create a new image download task for the poster image
        */
        this.imageTask = new ImageDownloadTask(new ImageDownloadTask.TaskActions() {

            public void onFinished(Bitmap image) {
                if (image != null) {
                    poster.setImageBitmap(image);
                }
            }
        });

        /*
         * If no movie id was received earlier then finish this activity before
         * anything else is done
         */
        if (this.movie == null) {
            // TODO Why does this cause a crash?
            finish();
        }

        // Kick off the fetch for IMDb info IF there's a set API id
        // set.
        if (this.movie.hasApiIDSet()) {
            final AsyncTask<Integer, Void, JSONObject> t = new IMDBTask()
                    .execute(new Integer[] { this.movie.getApiID() });

            // Cancel the task after a timeout
            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {

                public void run() {
                    if (t.getStatus() == AsyncTask.Status.RUNNING) {
                        t.cancel(true);
                        System.err.println("Fetching IMDb info did timeout");
                    }
                }
            }, IMDB_FETCH_TIMEOUT);
        }

        // Populate various view fields from the Movie object
        populateFieldsFromMovie(this.movie);

    }

    /**
     * Click callback when clicking on "View on IMDb" button.
     * Opens the current movie in the Android browser.
     * 
     * Shows an error toast if it wasn't possible to go to
     * the website, perhaps due to missing IMDb id.
     * 
     * @param view The view that triggered the event
     */
    public void goToIMDb(View view) {
        final String BASE_URL = "http://m.imdb.com/title/";
        final String imdbID = (String) view.getTag();

        if (imdbID != null) {
            String url = BASE_URL + imdbID;
            startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
        } else {
            Toast.makeText(this, R.string.movie_url_error, Toast.LENGTH_LONG).show();
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        System.out.println("new");
    }

    /**
     * Populate various view fields with data from a Movie.
     * 
     * @param m The movie to fill the fields with
     */
    public void populateFieldsFromMovie(Movie m) {

        setTitle(m.getTitle());

        RatingBar ratingBar = (RatingBar) findViewById(R.id.my_rating_bar);
        TextView releaseDateLabel = (TextView) findViewById(R.id.releaseDate);

        this.noteField.setText(m.getNote());
        ratingBar.setRating(m.getRating());
        releaseDateLabel.setText(DateTimeUtils.toSimpleDate(m.getDate()));

        String tags = MovieHelper.getCursorString(db.getAttachedTags(m));

        if (tags != null && !tags.isEmpty()) {
            tagField.setText(tags);
        }
        this.tagField.setText(tags.toString());
    }

    public void populateFieldsFromJSON(JSONObject json) {

        /*
         * Enable the browser button and tag it with the IMDB id
         * from the JSON response. Used to build an URL to the
         * movie on IMDB.com 
         */
        this.imdbButton.setEnabled(true);
        this.imdbButton.setTag(json.optString("imdb_id"));

        TextView rating = (TextView) findViewById(R.id.imdb_rating_number_label);
        TextView plot = (TextView) findViewById(R.id.plot_content);
        TextView cast = (TextView) findViewById(R.id.cast_list);
        TextView duration = (TextView) findViewById(R.id.duration);
        TextView genres = (TextView) findViewById(R.id.genres);

        double imdbRating = json.optDouble("rating");
        if (!Double.isNaN(imdbRating)) {
            rating.setText(String.valueOf(imdbRating));
        }

        String imdbPlot = json.optString("overview");
        if (!imdbPlot.isEmpty()) {
            plot.setText(imdbPlot);
        }

        int runtime = json.optInt("runtime");
        if (runtime != 0) {
            duration.setText(
                    getString(R.string.movie_detail_runtime) + " " + DateTimeUtils.minutesToHuman(runtime));
        }

        JSONArray imdbGenres = json.optJSONArray("genres");

        if (imdbGenres != null && imdbGenres.length() > 0) {
            String genreString = "";

            for (int i = 0; i < imdbGenres.length(); i++) {
                genreString += imdbGenres.optJSONObject(i).optString("name") + ", ";
            }

            genres.setText(genreString);
        }

        JSONArray posters = json.optJSONArray("posters");
        String imageURL = MovieHelper.getPosterFromCollection(posters, Movie.PosterSize.MID);

        // Fetch movie poster
        this.imageTask.execute(new String[] { imageURL });

        JSONArray imdbCast = json.optJSONArray("cast");
        String actors = "";

        if (imdbCast != null) {
            for (int i = 0; i < imdbCast.length(); i++) {
                JSONObject o = imdbCast.optJSONObject(i);
                if (o.optString("department").equalsIgnoreCase("actors")) {
                    actors += o.optString("name") + ", ";
                }
            }

            cast.setText(actors);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_movie_details, menu);

        this.menu = menu; // Can't get reference outside of this method,
        // reference needs to be stored here

        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {

        case android.R.id.home:
            NavUtils.navigateUpFromSameTask(this);
            return true;

        case R.id.menu_save:
            this.saveUserChanges();
            return true;

        case R.id.menu_cancel:
            this.setEditable(false);

            // Restore changes to visual elements
            populateFieldsFromMovie(this.movie);
            return true;

        case R.id.menu_edit:
            this.setEditable(true);
            return true;

        }
        return super.onOptionsItemSelected(item);

    }

    /**
     * Click callback. Shows the date picker for a movie's release date
     */
    public void onDatePickerButtonClick(View v) {
        DialogFragment datePickerFragment = new DatePickerFragment();
        datePickerFragment.show(getSupportFragmentManager(), "datePicker");
    }

    public void setDate(Calendar pickedDate) {
        this.tempReleaseDate = pickedDate;

        TextView releaseDateLabel = (TextView) findViewById(R.id.releaseDate);
        releaseDateLabel.setText(DateTimeUtils.toSimpleDate(tempReleaseDate));
    }

    /**
      * Saves the user editable fields
      */
    private void saveUserChanges() {

        db = new DatabaseAdapter(getContentResolver());

        if (tempReleaseDate != null) {
            movie.setDate(tempReleaseDate);
        }

        movie.setNote(noteField.getText().toString());
        movie.setRating((int) myRatingBar.getRating());

        /* 
         * Split the text input into separate strings input at
         * commas (",") from tag-field
         */
        String[] tagStrings = tagField.getText().toString().split(",");
        List<Tag> tempTags = MovieHelper.stringArrayToTagList(tagStrings);

        List<Tag> newTags = new LinkedList<Tag>(tempTags);

        /*
         * If there are some Tags in the new list that doesn't exist in the
         * old list, then those tags are new
         */
        newTags.removeAll(movie.getTags());

        if (!newTags.isEmpty()) {

            db.attachTags(movie, newTags);
            Log.i("Custom", movie.getTitle() + " - attached Tags: " + newTags.toString());
        }

        List<Tag> removedTags = new LinkedList<Tag>(movie.getTags());

        /*
         * If there are some Tags in the old list that doesn't exist in the
         * new list, then those tags have been removed
         */
        removedTags.removeAll(tempTags);

        if (!removedTags.isEmpty()) {
            db.detachTags(movie, removedTags);
            Log.i("Custom", movie.getTitle() + " - detached Tags: " + removedTags.toString());
        }

        this.setEditable(false);

        db.updateMovie(movie); // Updates release date, rating and note

        /*
         * Fetches a new instance of the movie straight from the database to
         * avoid having two different versions. Also vital because new Tags
         * id's are not set before this update.
         */
        movie = db.getMovie(movie.getId());

        // Show status toast when saved
        Toast.makeText(MovieDetailsActivity.this,
                "\"" + movie.getTitle() + "\" " + getString(R.string.movie_updated_toast_suffix),
                Toast.LENGTH_SHORT).show();
    }

    /**
     * Set whether user is able to edit movie data
     * 
     * @param isEditable True if user is able to edit
     */
    private void setEditable(boolean isEditable) {

        MenuItem editMenuButton = menu.findItem(R.id.menu_edit);
        MenuItem saveMenuButton = menu.findItem(R.id.menu_save);
        MenuItem cancelMenuButton = menu.findItem(R.id.menu_cancel);
        Button releaseDateButton = (Button) findViewById(R.id.release_date_button);

        editMenuButton.setVisible(!isEditable);
        saveMenuButton.setVisible(isEditable);
        cancelMenuButton.setVisible(isEditable);

        if (isEditable) {
            releaseDateButton.setVisibility(Button.VISIBLE);
        }

        else {
            releaseDateButton.setVisibility(Button.GONE);
        }

        myRatingBar.setIsIndicator(!isEditable);
        myRatingBar.setEnabled(isEditable);

        /*
         * setFocusable(true) does not work on EditText if it were
         * previously set to 'false'. This is a reported android bug and
         * at the time of writing there is no fix.
         * setFocusableInTouchMode(true) gets the job done for now.
         */
        tagField.setFocusableInTouchMode(isEditable);
        tagField.setFocusable(isEditable);
        tagField.setEnabled(isEditable);
        noteField.setFocusableInTouchMode(isEditable);
        noteField.setFocusable(isEditable);
        noteField.setEnabled(isEditable);
    }

    /**
     * The IMDb info fetch task.
     * 
     *  <p>This async task calls the IMDb API in order to fetch and
     *  show detailed JSON data from a single movie ID.</p>
     * 
     * @author Johan
     */
    private class IMDBTask extends AsyncTask<Integer, Void, JSONObject> {

        private ProgressBar spinner;

        public IMDBTask() {
            this.spinner = (ProgressBar) findViewById(R.id.imdb_loading_spinner);
        }

        @Override
        protected void onPreExecute() {
            this.spinner.setVisibility(View.VISIBLE);
        }

        @Override
        public void onCancelled() {
            this.spinner.setVisibility(View.INVISIBLE);

            Toast.makeText(getBaseContext(), getString(R.string.imdb_fetch_error_text), Toast.LENGTH_SHORT).show();

        }

        @Override
        protected JSONObject doInBackground(Integer... params) {
            JSONObject response = imdb.getMovieById(params[0]);

            return response;
        }

        @Override
        protected void onPostExecute(JSONObject res) {
            this.spinner.setVisibility(View.INVISIBLE);

            // Update the UI with the JSON data
            if (res != null) {
                populateFieldsFromJSON(res);
            } else {
                Toast.makeText(getBaseContext(), R.string.imdb_fetch_error_text, Toast.LENGTH_LONG).show();
            }
        }
    }

    /**
     * Listener class for when user clicks on the poster.
     * 
     *  <p>Gets the bitmap image from the poster and set it to the
     *  custom full screen overlay, then show it.</p>
     * 
     * @author Johan
     */
    private class OnPosterClickListener implements OnClickListener {

        public void onClick(View v) {
            ImageView view = (ImageView) v;
            Bitmap bm = ((BitmapDrawable) view.getDrawable()).getBitmap();

            dialog.setImage(bm);
            dialog.show();
        }

    }
}