Compare commits

21 Commits

Author SHA1 Message Date
danijoo
69baf54b46 removed error message for trailers 2015-11-01 16:35:59 +01:00
danijoo
ca930b8f88 cleaned comments 2015-11-01 16:27:38 +01:00
danijoo
710ffe1307 new trailer icon 2015-11-01 16:27:04 +01:00
danijoo
b29fb68caa reviews implemented 2015-11-01 16:20:58 +01:00
danijoo
ecc6bafb31 Merge branch 'reviews' of https://github.com/danijoo/UdacityProject1 into reviews 2015-11-01 14:32:06 +01:00
danijoo
425140cdd5 stashing 2015-11-01 14:10:17 +01:00
danijoo
6edffb2944 fixed crash for non tablets 2015-11-01 14:10:08 +01:00
danijoo
8ea8a27d74 string rename 2015-11-01 14:05:59 +01:00
danijoo
6d7427e50b lint warnings & hardcoded text 2015-11-01 14:05:26 +01:00
danijoo
0c35ab6f10 master details flow for tablet 2015-11-01 13:55:49 +01:00
danijoo
35def383d8 not found poster 2015-11-01 13:35:36 +01:00
danijoo
4eedae1dd0 comments' 2015-11-01 13:21:26 +01:00
danijoo
f9fcb4d4ff fixed restore state for favorites list 2015-11-01 13:18:39 +01:00
danijoo
a9a7710141 fixed NPE for missing Date in movie 2015-11-01 13:18:28 +01:00
danijoo
41abda5031 favoritelist 2015-11-01 13:09:05 +01:00
danijoo
73445906ed sbt config 2015-11-01 10:42:16 +01:00
danijoo
f8cf189d70 share feature 2015-11-01 00:06:38 +01:00
danijoo
e88513b4d8 stashing 2015-10-30 16:15:50 +01:00
danijoo
1f1d3ee57b video trailers 2015-10-30 14:55:28 +01:00
danijoo
c7e2afb783 butterknife view binding 2015-10-30 11:31:54 +01:00
danijoo
f76bb32e5c restore movielist on config change 2015-10-30 11:26:57 +01:00
74 changed files with 1264 additions and 122 deletions

View File

@@ -1,5 +1,16 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'android-apt'
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
android { android {
compileSdkVersion 23 compileSdkVersion 23
buildToolsVersion "23.0.1" buildToolsVersion "23.0.1"
@@ -17,6 +28,14 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
lintOptions {
disable 'InvalidPackage'
}
packagingOptions {
exclude 'META-INF/services/javax.annotation.processing.Processor'
}
} }
dependencies { dependencies {
@@ -29,4 +48,9 @@ dependencies {
compile 'com.squareup.picasso:picasso:2.5.2' compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.retrofit:retrofit:2.0.0-beta2' compile 'com.squareup.retrofit:retrofit:2.0.0-beta2'
compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2' compile 'com.squareup.retrofit:converter-gson:2.0.0-beta2'
compile 'com.jakewharton:butterknife:7.0.1'
apt 'net.simonvt.schematic:schematic-compiler:0.6.3'
compile 'net.simonvt.schematic:schematic:0.6.3'
} }

1
app/build.sbt Normal file
View File

@@ -0,0 +1 @@
protifySettings

View File

@@ -20,6 +20,11 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<provider
android:authorities="net.headlezz.udacityproject1.FavoriteProvider"
android:name="net.headlezz.udacityproject1.provider.FavoriteProvider"
android:exported="false" />
</application> </application>
</manifest> </manifest>

View File

@@ -4,7 +4,10 @@ import android.os.Bundle;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.View;
import net.headlezz.udacityproject1.moviedetails.MovieDetailsFragment;
import net.headlezz.udacityproject1.movielist.MovieListFragment;
import net.headlezz.udacityproject1.tmdbapi.Movie; import net.headlezz.udacityproject1.tmdbapi.Movie;
/** /**
@@ -12,10 +15,13 @@ import net.headlezz.udacityproject1.tmdbapi.Movie;
*/ */
public class MainActivity extends AppCompatActivity implements MovieNavigation, FragmentManager.OnBackStackChangedListener { public class MainActivity extends AppCompatActivity implements MovieNavigation, FragmentManager.OnBackStackChangedListener {
View detailsPlaceholderView;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
detailsPlaceholderView = findViewById(R.id.main_details_placeHolderText);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
setTitle(getString(R.string.app_name)); setTitle(getString(R.string.app_name));
@@ -37,8 +43,11 @@ public class MainActivity extends AppCompatActivity implements MovieNavigation,
*/ */
public void showDetailsFragment(Movie movie) { public void showDetailsFragment(Movie movie) {
MovieDetailsFragment frag = MovieDetailsFragment.newInstance(movie); MovieDetailsFragment frag = MovieDetailsFragment.newInstance(movie);
int holderId = getResources().getBoolean(R.bool.master_detail) ? R.id.fragmentHolderDetails : R.id.fragmentHolder;
if(detailsPlaceholderView != null)
detailsPlaceholderView.setVisibility(View.GONE);
getSupportFragmentManager().beginTransaction() getSupportFragmentManager().beginTransaction()
.replace(R.id.fragmentHolder, frag) .replace(holderId, frag)
.addToBackStack(MovieDetailsFragment.TAG) .addToBackStack(MovieDetailsFragment.TAG)
.commit(); .commit();
} }

View File

@@ -1,73 +0,0 @@
package net.headlezz.udacityproject1;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import net.headlezz.udacityproject1.tmdbapi.Movie;
import net.headlezz.udacityproject1.tmdbapi.TMDBApi;
/**
* Fragment to show the details of a movie
*/
public class MovieDetailsFragment extends Fragment {
public static final String TAG = MovieDetailsFragment.class.getSimpleName();
public static final String BUNDLE_ARG_MOVIE = "movie";
ImageView ivPoster;
TextView tvTitle;
TextView tvOverview;
TextView tvReleaseDate;
TextView tvRating;
Movie mMovie;
public static MovieDetailsFragment newInstance(Movie movie) {
MovieDetailsFragment frag = new MovieDetailsFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_ARG_MOVIE, movie);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(getArguments().containsKey(BUNDLE_ARG_MOVIE)) {
mMovie = getArguments().getParcelable(BUNDLE_ARG_MOVIE);
Log.d(TAG, "Bundled movie: " + mMovie.getTitle());
} else
throw new RuntimeException(TAG + " opened without bundled movie!");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_movie_details, container, false);
ivPoster = (ImageView) view.findViewById(R.id.movie_details_ivPoster);
tvTitle = (TextView) view.findViewById(R.id.movie_details_tvTitle);
tvOverview = (TextView) view.findViewById(R.id.movie_details_tvOverview);
tvReleaseDate = (TextView) view.findViewById(R.id.movie_details_tvReleaseDate);
tvRating = (TextView) view.findViewById(R.id.movie_details_tvRating);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
TMDBApi.loadPoster(ivPoster, mMovie);
tvTitle.setText(mMovie.getTitle());
tvOverview.setText(mMovie.getOverview());
String formatedDate = DateUtils.formatDateTime(tvReleaseDate.getContext(), mMovie.getReleaseDate().getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
tvReleaseDate.setText(getString(R.string.movie_release_date, formatedDate));
tvRating.setText(getString(R.string.movie_rating, mMovie.getAvRating()));
}
}

View File

@@ -0,0 +1,21 @@
package net.headlezz.udacityproject1.favorites;
import net.simonvt.schematic.annotation.DataType;
import net.simonvt.schematic.annotation.NotNull;
import net.simonvt.schematic.annotation.PrimaryKey;
public interface FavoriteColumns {
@DataType(DataType.Type.INTEGER) @PrimaryKey String _ID = "_id";
@DataType(DataType.Type.TEXT) @NotNull String TITLE = "title";
@DataType(DataType.Type.TEXT) @NotNull String POSTERPATH = "posterpath";
@DataType(DataType.Type.REAL) @NotNull String AVRATING = "avrating";
@DataType(DataType.Type.INTEGER) @NotNull String RELEASEDATE = "release";
@DataType(DataType.Type.TEXT) @NotNull String OVERVIEW = "overview";
}

View File

@@ -0,0 +1,12 @@
package net.headlezz.udacityproject1.favorites;
import net.simonvt.schematic.annotation.Database;
import net.simonvt.schematic.annotation.Table;
@Database(version=FavoriteDatabase.VERSION, packageName="net.headlezz.udacityproject1.provider")
public class FavoriteDatabase {
public static final int VERSION = 1;
@Table(FavoriteColumns.class) public static final String favorites = "favorites";
}

View File

@@ -0,0 +1,24 @@
package net.headlezz.udacityproject1.favorites;
import android.net.Uri;
import net.simonvt.schematic.annotation.ContentProvider;
import net.simonvt.schematic.annotation.ContentUri;
import net.simonvt.schematic.annotation.TableEndpoint;
@ContentProvider(authority = FavoriteProvider.AUTHORITY, database = FavoriteDatabase.class, packageName="net.headlezz.udacityproject1.provider")
public class FavoriteProvider {
public static final String AUTHORITY = "net.headlezz.udacityproject1.FavoriteProvider";
static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);
@TableEndpoint(table = FavoriteDatabase.favorites)
public static class Favorites {
@ContentUri(
path = "favorites",
type = "vnd.android.cursor.dir/list",
defaultSort = FavoriteColumns.TITLE + " ASC")
public static final Uri FAVORITES_URI = Uri.parse("content://" + AUTHORITY + "/favorites");
}
}

View File

@@ -0,0 +1,58 @@
package net.headlezz.udacityproject1.favorites;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import net.headlezz.udacityproject1.tmdbapi.Movie;
/**
* Helper class for accessing the Favorite database
*/
public class FavoriteProviderHelper {
private ContentResolver mResolver;
public FavoriteProviderHelper(ContentResolver resolver) {
mResolver = resolver;
}
public Cursor getAllFavorites() {
String[] projection = {
FavoriteColumns._ID,
FavoriteColumns.TITLE,
FavoriteColumns.POSTERPATH,
FavoriteColumns.AVRATING,
FavoriteColumns.RELEASEDATE,
FavoriteColumns.OVERVIEW
};
return mResolver.query(FavoriteProvider.Favorites.FAVORITES_URI, projection, null, null, null);
}
public boolean isFavorite(Movie movie) {
String[] projection = {FavoriteColumns._ID};
String selection = FavoriteColumns._ID + "=?";
String[] selectionArgs = new String[]{String.valueOf(movie.getId())};
Cursor cursor = mResolver.query(FavoriteProvider.Favorites.FAVORITES_URI, projection, selection, selectionArgs, null);
boolean isFavourite = (cursor != null && cursor.getCount() > 0);
if(cursor != null)
cursor.close();
return isFavourite;
}
public void addFavorite(Movie movie) {
ContentValues values = new ContentValues();
values.put(FavoriteColumns._ID, movie.getId());
values.put(FavoriteColumns.TITLE, movie.getTitle());
values.put(FavoriteColumns.OVERVIEW, movie.getOverview());
values.put(FavoriteColumns.POSTERPATH, movie.getPosterPath());
values.put(FavoriteColumns.AVRATING, movie.getAvRating());
values.put(FavoriteColumns.RELEASEDATE, movie.getReleaseDate().getTime());
mResolver.insert(FavoriteProvider.Favorites.FAVORITES_URI, values);
}
public void removeFavorite(Movie movie) {
mResolver.delete(FavoriteProvider.Favorites.FAVORITES_URI, FavoriteColumns._ID + "=?", new String[]{String.valueOf(movie.getId())});
}
}

View File

@@ -0,0 +1,266 @@
package net.headlezz.udacityproject1.moviedetails;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import net.headlezz.udacityproject1.R;
import net.headlezz.udacityproject1.favorites.FavoriteProviderHelper;
import net.headlezz.udacityproject1.tmdbapi.Movie;
import net.headlezz.udacityproject1.tmdbapi.ReviewList;
import net.headlezz.udacityproject1.tmdbapi.TMDBApi;
import net.headlezz.udacityproject1.tmdbapi.Video;
import net.headlezz.udacityproject1.tmdbapi.VideoList;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
import retrofit.Call;
import retrofit.Callback;
import retrofit.Response;
import retrofit.Retrofit;
/**
* Fragment to show the details of a movie
*/
public class MovieDetailsFragment extends Fragment implements Callback<VideoList> {
public static final String TAG = MovieDetailsFragment.class.getSimpleName();
public static final String BUNDLE_ARG_MOVIE = "movie";
@Bind(R.id.movie_details_ivPoster) ImageView ivPoster;
@Bind(R.id.movie_details_tvTitle) TextView tvTitle;
@Bind(R.id.movie_details_tvOverview) TextView tvOverview;
@Bind(R.id.movie_details_tvReleaseDate) TextView tvReleaseDate;
@Bind(R.id.movie_details_tvRating) TextView tvRating;
@Bind(R.id.movie_details_trailerHolder) ViewGroup trailerHolder;
Movie mMovie;
VideoList mVideoList;
FavoriteProviderHelper mProvderHelper;
boolean isFavorite;
/**
* Hold a reference to the videolist download call so
* it can be canceled when stopping
*/
Call<VideoList> mVideoListCall;
public static MovieDetailsFragment newInstance(Movie movie) {
MovieDetailsFragment frag = new MovieDetailsFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_ARG_MOVIE, movie);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mProvderHelper = new FavoriteProviderHelper(getContext().getContentResolver());
if(getArguments().containsKey(BUNDLE_ARG_MOVIE)) {
mMovie = getArguments().getParcelable(BUNDLE_ARG_MOVIE);
Log.d(TAG, "Bundled movie: " + mMovie.getTitle());
isFavorite = mProvderHelper.isFavorite(mMovie);
} else
throw new RuntimeException(TAG + " opened without bundled movie!");
if(savedInstanceState != null && savedInstanceState.containsKey("videolist"))
mVideoList = savedInstanceState.getParcelable("videolist");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_movie_details, container, false);
ButterKnife.bind(this, view);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
TMDBApi.loadPoster(ivPoster, mMovie);
tvTitle.setText(mMovie.getTitle());
tvOverview.setText(mMovie.getOverview());
String formatedDate = DateUtils.formatDateTime(tvReleaseDate.getContext(), mMovie.getReleaseDate().getTime(), DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR);
tvReleaseDate.setText(getString(R.string.movie_release_date, formatedDate));
tvRating.setText(getString(R.string.movie_rating, mMovie.getAvRating()));
if(mVideoList == null)
loadVideoList();
else
showVideoList(mVideoList);
}
private void loadVideoList() {
if(mVideoListCall != null)
mVideoListCall.cancel();
mVideoListCall = TMDBApi.getVideosForMovie(mMovie, getString(R.string.api_key));
mVideoListCall.enqueue(this);
}
@Override
public void onResponse(Response<VideoList> response, Retrofit retrofit) {
if(response.isSuccess()) {
mVideoList = response.body();
if(getActivity() != null)
showVideoList(mVideoList);
} else {
trailerHolder.getChildAt(0).setVisibility(View.VISIBLE);
}
}
@Override
public void onFailure(Throwable t) {
trailerHolder.getChildAt(0).setVisibility(View.VISIBLE);
}
@OnClick(R.id.movie_details_btReviews)
public void onReviewButtonClick(View view) {
final ProgressDialog dialog = new ProgressDialog(getContext());
dialog.setIndeterminate(true);
dialog.setMessage(getString(R.string.movie_download_review_progress));
dialog.show();
TMDBApi.getReviewsForMovie(mMovie, getString(R.string.api_key)).enqueue(new Callback<ReviewList>() {
@Override
public void onResponse(Response<ReviewList> response, Retrofit retrofit) {
if(response.body().getReviews().size() == 0)
Toast.makeText(getContext(), R.string.movie_download_review_not_found, Toast.LENGTH_SHORT).show();
else {
ReviewDialogFragment frag = ReviewDialogFragment.newInstance(response.body());
frag.show(getChildFragmentManager(), ReviewDialogFragment.TAG);
}
if(dialog.isShowing())
dialog.dismiss();
}
@Override
public void onFailure(Throwable t) {
if(dialog.isShowing())
dialog.dismiss();
Toast.makeText(getContext(), R.string.movie_download_review_fail, Toast.LENGTH_SHORT).show();
}
});
}
/**
* shows a list of links to youtube videos for the selected movie
* if the videolist has no videos, the user sees a message that there are no.
* @param videoList VideoList to show
*/
private void showVideoList(@NonNull VideoList videoList) {
List<Video> videos = videoList.getVideos();
if(videos.size() > 0) {
trailerHolder.getChildAt(0).setVisibility(View.GONE);
for (Video video : videoList.getVideos()) {
View view = LayoutInflater.from(getActivity()).inflate(R.layout.movie_details_trailer_item, trailerHolder, false);
TextView tv = (TextView) view.findViewById(R.id.trailer_item_tvName);
tv.setText(video.getName());
view.setTag(video);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String videoId = ((Video) view.getTag()).getKey();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("vnd.youtube:" + videoId));
intent.putExtra("VIDEO_ID", videoId);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.movie_share_fail, Toast.LENGTH_SHORT).show();
}
}
});
trailerHolder.addView(view);
}
} else {
trailerHolder.getChildAt(0).setVisibility(View.VISIBLE);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.movie_details_menu, menu);
MenuItem favItem = menu.findItem(R.id.action_favorite);
favItem.setIcon(isFavorite ? R.drawable.ic_action_star_10 : R.drawable.ic_action_star_0);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if(item.getItemId() == R.id.action_share) {
shareTrailer();
return true;
} else if(item.getItemId() == R.id.action_favorite) {
switchFavorite();
item.setIcon(isFavorite ? R.drawable.ic_action_star_10 : R.drawable.ic_action_star_0);
return true;
}
else
return super.onOptionsItemSelected(item);
}
/**
* removes or adds the movie to stored favorites
*/
private void switchFavorite() {
if(isFavorite)
mProvderHelper.removeFavorite(mMovie);
else
mProvderHelper.addFavorite(mMovie);
isFavorite = !isFavorite;
}
/**
* Fires an intent to share the trailer url of this movie
* Code from http://www.techrepublic.com/blog/software-engineer/get-social-using-android-intents-to-share-a-link/
*/
private void shareTrailer() {
if(mVideoList == null || mVideoList.getVideos().size() == 0)
Toast.makeText(getActivity(), R.string.movie_share_no_trailer_found, Toast.LENGTH_SHORT).show();
else {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("text/plain");
i.putExtra(Intent.EXTRA_TEXT, "https://youtu.be/" + mVideoList.getVideos().get(0).getKey());
i.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.movie_share_title));
startActivity(Intent.createChooser(i, "Share"));
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
if(mVideoList != null)
outState.putParcelable("videolist", mVideoList);
super.onSaveInstanceState(outState);
}
@Override
public void onStop() {
if(mVideoListCall != null)
mVideoListCall.cancel();
super.onStop();
}
}

View File

@@ -0,0 +1,126 @@
package net.headlezz.udacityproject1.moviedetails;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ImageButton;
import android.widget.TextView;
import net.headlezz.udacityproject1.R;
import net.headlezz.udacityproject1.tmdbapi.Review;
import net.headlezz.udacityproject1.tmdbapi.ReviewList;
import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class ReviewDialogFragment extends DialogFragment {
public static final String TAG = ReviewDialogFragment.class.getSimpleName();
public static final String BUNDLE_ARG_REVIEWS = "reviews";
ReviewList mReviews;
int mCurrentPosition;
@Bind(R.id.review_tvReview)
TextView tvReview;
@Bind(R.id.review_tvReviewUser)
TextView tvReviewUser;
@Bind(R.id.review_btNext)
ImageButton btNext;
@Bind(R.id.review_btPrevious)
ImageButton btPrev;
@Bind(R.id.review_tvPosition)
TextView tvPosition;
public static ReviewDialogFragment newInstance(ReviewList reviews) {
ReviewDialogFragment frag = new ReviewDialogFragment();
Bundle args = new Bundle();
args.putParcelable(BUNDLE_ARG_REVIEWS, reviews);
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(getArguments() != null && getArguments().containsKey(BUNDLE_ARG_REVIEWS))
mReviews = getArguments().getParcelable(BUNDLE_ARG_REVIEWS);
else
throw new RuntimeException("No arguments found");
if(savedInstanceState != null)
mCurrentPosition = savedInstanceState.getInt("position", 0);
else
mCurrentPosition = 0;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View dialogView = inflater.inflate(R.layout.movie_details_review_dialog, container, false);
ButterKnife.bind(this, dialogView);
updateReview();
checkButtonState();
return dialogView;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
return dialog;
}
@OnClick(R.id.review_btNext)
public void onNextClick(View view) {
mCurrentPosition += 1;
updateReview();
checkButtonState();
}
@OnClick(R.id.review_btPrevious)
public void onPreviousClick(View view) {
mCurrentPosition -= 1;
updateReview();
checkButtonState();
}
private void updateReview() {
Review rv = mReviews.getReviews().get(mCurrentPosition);
tvReview.setText(rv.getContent());
tvReviewUser.setText(rv.getAuthor());
tvPosition.setText(String.format("%1$s/%2$s", mCurrentPosition + 1, mReviews.getReviews().size()));
}
private void checkButtonState() {
if(mCurrentPosition < 1)
btPrev.setVisibility(View.GONE);
else
btPrev.setVisibility(View.VISIBLE);
if(mCurrentPosition >= mReviews.getReviews().size() - 1)
btNext.setVisibility(View.GONE);
else
btNext.setVisibility(View.VISIBLE);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt("position", mCurrentPosition);
super.onSaveInstanceState(outState);
}
}

View File

@@ -1,4 +1,4 @@
package net.headlezz.udacityproject1; package net.headlezz.udacityproject1.movielist;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -6,20 +6,20 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import net.headlezz.udacityproject1.MovieNavigation;
import net.headlezz.udacityproject1.R;
import net.headlezz.udacityproject1.tmdbapi.Movie; import net.headlezz.udacityproject1.tmdbapi.Movie;
import net.headlezz.udacityproject1.tmdbapi.MovieList;
import net.headlezz.udacityproject1.tmdbapi.TMDBApi; import net.headlezz.udacityproject1.tmdbapi.TMDBApi;
import java.util.List; /**
* Abstract class for shwoing a list of movie posters
public class MovieListAdapter extends RecyclerView.Adapter<MovieListAdapter.MovieViewHolder>{ */
public abstract class AbstractMovieListAdapter extends RecyclerView.Adapter<AbstractMovieListAdapter.MovieViewHolder> {
MovieNavigation mMovieNavigation; MovieNavigation mMovieNavigation;
List<Movie> mMovies;
public MovieListAdapter(MovieNavigation mn, MovieList movieList) { public AbstractMovieListAdapter(MovieNavigation mn) {
mMovieNavigation = mn; mMovieNavigation = mn;
mMovies = movieList.getList();
} }
@Override @Override
@@ -30,13 +30,14 @@ public class MovieListAdapter extends RecyclerView.Adapter<MovieListAdapter.Movi
@Override @Override
public void onBindViewHolder(MovieViewHolder holder, int position) { public void onBindViewHolder(MovieViewHolder holder, int position) {
holder.setMovie(mMovies.get(position)); holder.setMovie(getMovieForPosition(position));
} }
@Override /**
public int getItemCount() { * @param position of the list
return mMovies.size(); * @return the movie displayed for the given position
} */
protected abstract Movie getMovieForPosition(int position);
/** /**
* Viewholder for the movie items * Viewholder for the movie items
@@ -62,5 +63,4 @@ public class MovieListAdapter extends RecyclerView.Adapter<MovieListAdapter.Movi
TMDBApi.loadPoster(ivPoster, movie); TMDBApi.loadPoster(ivPoster, movie);
} }
} }
} }

View File

@@ -0,0 +1,82 @@
package net.headlezz.udacityproject1.movielist;
import android.database.Cursor;
import android.database.DataSetObserver;
import net.headlezz.udacityproject1.MovieNavigation;
import net.headlezz.udacityproject1.favorites.FavoriteColumns;
import net.headlezz.udacityproject1.tmdbapi.Movie;
/**
* MovieListAdapter implementation for showing movies from a cursor
* Modified version of https://gist.github.com/skyfishjy/443b7448f59be978bc59
*/
public class CursorMovieListAdapter extends AbstractMovieListAdapter {
private Cursor mCursor;
private boolean mDataValid;
private int mRowIdColumn;
private DataSetObserver mDataSetObserver;
public CursorMovieListAdapter(Cursor cursor, MovieNavigation mn) {
super(mn);
mCursor = cursor;
mDataValid = cursor != null;
mRowIdColumn = mDataValid ? mCursor.getColumnIndex(FavoriteColumns._ID) : -1;
mDataSetObserver = new NotifyingDataSetObserver();
if (mCursor != null) {
mCursor.registerDataSetObserver(mDataSetObserver);
}
}
@Override
protected Movie getMovieForPosition(int position) {
if (!mDataValid) {
throw new IllegalStateException("this should only be called when the cursor is valid");
}
if (!mCursor.moveToPosition(position)) {
throw new IllegalStateException("couldn't move cursor to position " + position);
}
return Movie.fromCurrentCursorPosition(mCursor);
}
@Override
public int getItemCount() {
if (mDataValid && mCursor != null) {
return mCursor.getCount();
}
return 0;
}
@Override
public long getItemId(int position) {
if (mDataValid && mCursor != null && mCursor.moveToPosition(position)) {
return mCursor.getLong(mRowIdColumn);
}
return 0;
}
@Override
public void setHasStableIds(boolean hasStableIds) {
super.setHasStableIds(true);
}
private class NotifyingDataSetObserver extends DataSetObserver {
@Override
public void onChanged() {
super.onChanged();
mDataValid = true;
notifyDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mDataValid = false;
notifyDataSetChanged();
//There is no notifyDataSetInvalidated() method in RecyclerView.Adapter
}
}
}

View File

@@ -0,0 +1,30 @@
package net.headlezz.udacityproject1.movielist;
import net.headlezz.udacityproject1.MovieNavigation;
import net.headlezz.udacityproject1.tmdbapi.Movie;
import net.headlezz.udacityproject1.tmdbapi.MovieList;
import java.util.List;
/**
* MovieListAdapter implementation for showing movies from a list of movies
*/
public class MovieListAdapter extends AbstractMovieListAdapter {
List<Movie> mMovies;
public MovieListAdapter(MovieNavigation mn, MovieList movieList) {
super(mn);
mMovies = movieList.getList();
}
@Override
public int getItemCount() {
return mMovies.size();
}
@Override
protected Movie getMovieForPosition(int position) {
return mMovies.get(position);
}
}

View File

@@ -1,8 +1,10 @@
package net.headlezz.udacityproject1; package net.headlezz.udacityproject1.movielist;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@@ -16,9 +18,14 @@ import android.view.ViewGroup;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Toast; import android.widget.Toast;
import net.headlezz.udacityproject1.MovieNavigation;
import net.headlezz.udacityproject1.R;
import net.headlezz.udacityproject1.favorites.FavoriteProviderHelper;
import net.headlezz.udacityproject1.tmdbapi.MovieList; import net.headlezz.udacityproject1.tmdbapi.MovieList;
import net.headlezz.udacityproject1.tmdbapi.TMDBApi; import net.headlezz.udacityproject1.tmdbapi.TMDBApi;
import butterknife.Bind;
import butterknife.ButterKnife;
import retrofit.Call; import retrofit.Call;
import retrofit.Callback; import retrofit.Callback;
import retrofit.Response; import retrofit.Response;
@@ -38,11 +45,20 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
private static final int NUM_COLUMNS_LAND = 5; private static final int NUM_COLUMNS_LAND = 5;
/** /**
* The currently selected sorting order of the list * The currently selected sorting order of the list
*
* @see TMDBApi * @see TMDBApi
*/ */
private int mSortingOrder = TMDBApi.SORT_ORDER_MOST_POPULAR; private int mSortingOrder = SORT_ORDER_MOST_POPULAR;
public static final int SORT_ORDER_MOST_POPULAR = TMDBApi.SORT_ORDER_MOST_POPULAR;
public static final int SORT_ORDER_HIGHEST_RATED = TMDBApi.SORT_ORDER_HIGHEST_RATED;
public static final int SORT_ORDER_FAVORITES = 3;
private MovieList mMovieList;
@Bind(R.id.movie_list_grid)
RecyclerView mMovieGridView; RecyclerView mMovieGridView;
@Bind(R.id.movie_list_progressBar)
ProgressBar mProgressBar; ProgressBar mProgressBar;
/** /**
@@ -60,8 +76,7 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_movie_list, container, false); View view = inflater.inflate(R.layout.fragment_movie_list, container, false);
mMovieGridView = (RecyclerView) view.findViewById(R.id.movie_list_grid); ButterKnife.bind(this, view);
mProgressBar = (ProgressBar) view.findViewById(R.id.movie_list_progressBar);
return view; return view;
} }
@@ -69,9 +84,15 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
mMovieGridView.setLayoutManager(new GridLayoutManager(getActivity(), getNumColumns())); mMovieGridView.setLayoutManager(new GridLayoutManager(getActivity(), getNumColumns()));
if (savedInstanceState != null)
restoreState(savedInstanceState);
}
private void restoreState(@NonNull Bundle savedInstanceState) {
mSortingOrder = savedInstanceState.getInt("sort_order");
if (savedInstanceState.containsKey("movielist"))
mMovieList = savedInstanceState.getParcelable("movielist");
if(savedInstanceState != null)
mSortingOrder = savedInstanceState.getInt("sort_order");
} }
/** /**
@@ -80,7 +101,7 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
*/ */
private int getNumColumns() { private int getNumColumns() {
int orientation = getActivity().getResources().getConfiguration().orientation; int orientation = getActivity().getResources().getConfiguration().orientation;
if(orientation == Configuration.ORIENTATION_LANDSCAPE) if (orientation == Configuration.ORIENTATION_LANDSCAPE)
return NUM_COLUMNS_LAND; return NUM_COLUMNS_LAND;
else else
return NUM_COLUMNS_PORTRAIT; return NUM_COLUMNS_PORTRAIT;
@@ -90,7 +111,18 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
loadMovieList(); initMovieList();
}
/**
* sets the adapter from the restored movie list if the instance state is restored
* starts the download of the new movie list if no restored one is present
*/
private void initMovieList() {
if (mMovieList == null)
loadMovieList();
else
mMovieGridView.setAdapter(new MovieListAdapter((MovieNavigation) getActivity(), mMovieList));
} }
@Override @Override
@@ -103,11 +135,15 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_sort_popularity: case R.id.action_sort_popularity:
mSortingOrder = TMDBApi.SORT_ORDER_MOST_POPULAR; mSortingOrder = SORT_ORDER_MOST_POPULAR;
loadMovieList(); loadMovieList();
return true; return true;
case R.id.action_sort_rating: case R.id.action_sort_rating:
mSortingOrder = TMDBApi.SORT_ORDER_HIGHEST_RATED; mSortingOrder = SORT_ORDER_HIGHEST_RATED;
loadMovieList();
return true;
case R.id.action_sort_favorites:
mSortingOrder = SORT_ORDER_FAVORITES;
loadMovieList(); loadMovieList();
return true; return true;
default: default:
@@ -121,15 +157,26 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
*/ */
private void loadMovieList() { private void loadMovieList() {
stopLoadingMovies(); // stop any running query before starting a new one stopLoadingMovies(); // stop any running query before starting a new one
mProgressBar.setVisibility(View.VISIBLE); if (mSortingOrder != SORT_ORDER_FAVORITES) {
mMovieListCall = TMDBApi.discoverMovies(mSortingOrder, getString(R.string.api_key)); mProgressBar.setVisibility(View.VISIBLE);
mMovieListCall.enqueue(this); mMovieListCall = TMDBApi.discoverMovies(mSortingOrder, getString(R.string.api_key));
mMovieListCall.enqueue(this);
} else
loadMovieListFromFavorites();
}
private void loadMovieListFromFavorites() {
mMovieList = null;
Cursor cursor = new FavoriteProviderHelper(getContext().getContentResolver()).getAllFavorites();
mMovieGridView.setAdapter(new CursorMovieListAdapter(cursor, (MovieNavigation) getActivity()));
} }
@Override @Override
public void onResponse(Response<MovieList> response, Retrofit retrofit) { public void onResponse(Response<MovieList> response, Retrofit retrofit) {
if(response.isSuccess()) if (response.isSuccess()) {
setListToGrid(response.body()); mMovieList = response.body();
setListToGrid(mMovieList);
}
mProgressBar.setVisibility(View.GONE); mProgressBar.setVisibility(View.GONE);
} }
@@ -137,7 +184,7 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
Log.d(TAG, t.getClass().getSimpleName() + " " + t.getMessage()); Log.d(TAG, t.getClass().getSimpleName() + " " + t.getMessage());
mProgressBar.setVisibility(View.GONE); mProgressBar.setVisibility(View.GONE);
if(!t.getMessage().equals("Canceled") && getActivity() != null) if (!t.getMessage().equals("Canceled") && getActivity() != null)
Toast.makeText(getActivity(), t.getMessage(), Toast.LENGTH_SHORT).show(); Toast.makeText(getActivity(), t.getMessage(), Toast.LENGTH_SHORT).show();
} }
@@ -149,7 +196,7 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
* Stops the current api query * Stops the current api query
*/ */
private void stopLoadingMovies() { private void stopLoadingMovies() {
if(mMovieListCall != null) { if (mMovieListCall != null) {
mMovieListCall.cancel(); mMovieListCall.cancel();
mMovieListCall = null; mMovieListCall = null;
} }
@@ -164,13 +211,15 @@ public class MovieListFragment extends Fragment implements Callback<MovieList> {
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
if(!(context instanceof MovieNavigation)) if (!(context instanceof MovieNavigation))
throw new RuntimeException("Activity must implement " + MovieNavigation.class.getSimpleName()); throw new RuntimeException("Activity must implement " + MovieNavigation.class.getSimpleName());
} }
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
outState.putInt("sort_order", mSortingOrder); outState.putInt("sort_order", mSortingOrder);
if (mMovieList != null && mSortingOrder != SORT_ORDER_FAVORITES)
outState.putParcelable("movielist", mMovieList);
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
} }
} }

View File

@@ -1,10 +1,13 @@
package net.headlezz.udacityproject1.tmdbapi; package net.headlezz.udacityproject1.tmdbapi;
import android.database.Cursor;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import net.headlezz.udacityproject1.favorites.FavoriteColumns;
import java.util.Date; import java.util.Date;
/** /**
@@ -16,6 +19,8 @@ public class Movie implements Parcelable {
// empty constructor for gson/retrofit // empty constructor for gson/retrofit
public Movie() {} public Movie() {}
long id;
String title; String title;
@SerializedName("poster_path") @SerializedName("poster_path")
@@ -49,6 +54,21 @@ public class Movie implements Parcelable {
return releaseDate; return releaseDate;
} }
public long getId() {
return id;
}
public static Movie fromCurrentCursorPosition(Cursor cursor) {
Movie movie = new Movie();
movie.id = cursor.getLong(cursor.getColumnIndex(FavoriteColumns._ID));
movie.title = cursor.getString(cursor.getColumnIndex(FavoriteColumns.TITLE));
movie.posterPath = cursor.getString(cursor.getColumnIndex(FavoriteColumns.POSTERPATH));
movie.overview = cursor.getString(cursor.getColumnIndex(FavoriteColumns.OVERVIEW));
movie.releaseDate = new Date(cursor.getLong(cursor.getColumnIndex(FavoriteColumns.RELEASEDATE)));
movie.avRating = cursor.getDouble(cursor.getColumnIndex(FavoriteColumns.AVRATING));
return movie;
}
/**** Parcelable stuff *****/ /**** Parcelable stuff *****/
@Override @Override
@@ -62,7 +82,8 @@ public class Movie implements Parcelable {
dest.writeString(posterPath); dest.writeString(posterPath);
dest.writeString(overview); dest.writeString(overview);
dest.writeDouble(avRating); dest.writeDouble(avRating);
dest.writeLong(releaseDate.getTime()); dest.writeLong( (releaseDate == null) ? -1 : releaseDate.getTime());
dest.writeLong(id);
} }
public static final Parcelable.Creator<Movie> CREATOR = new Parcelable.Creator<Movie>() { public static final Parcelable.Creator<Movie> CREATOR = new Parcelable.Creator<Movie>() {
@@ -80,7 +101,10 @@ public class Movie implements Parcelable {
posterPath = src.readString(); posterPath = src.readString();
overview = src.readString(); overview = src.readString();
avRating = src.readDouble(); avRating = src.readDouble();
releaseDate = new Date(src.readLong()); long dateTime = src.readLong();
if(dateTime != -1)
releaseDate = new Date(dateTime);
id = src.readLong();
} }

View File

@@ -1,11 +1,16 @@
package net.headlezz.udacityproject1.tmdbapi; package net.headlezz.udacityproject1.tmdbapi;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Arrays;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
* Wrapper class for list of movies * Wrapper class for list of movies
*/ */
public class MovieList { public class MovieList implements Parcelable {
public MovieList(List<Movie> movieList) { public MovieList(List<Movie> movieList) {
this.movies = movieList; this.movies = movieList;
@@ -15,4 +20,29 @@ public class MovieList {
public List<Movie> getList() { return movies; } public List<Movie> getList() { return movies; }
/***** Parcelable implementation ******/
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeList(movies);
}
public static final Parcelable.Creator<MovieList> CREATOR = new Parcelable.Creator<MovieList>() {
public MovieList createFromParcel(Parcel src) {
return new MovieList(src);
}
public MovieList[] newArray(int size) {
return new MovieList[size];
}
};
private MovieList(Parcel src) {
movies = src.readArrayList(Movie.class.getClassLoader());
}
} }

View File

@@ -0,0 +1,44 @@
package net.headlezz.udacityproject1.tmdbapi;
import android.os.Parcel;
import android.os.Parcelable;
public class Review implements Parcelable {
String author;
String content;
public String getAuthor() {
return author;
}
public String getContent() {
return content;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(author);
parcel.writeString(content);
}
public static final Parcelable.Creator<Review> CREATOR = new Parcelable.Creator<Review>() {
public Review createFromParcel(Parcel src) {
return new Review(src);
}
public Review[] newArray(int size) {
return new Review[size];
}
};
Review(Parcel in) {
this.author = in.readString();
this.content = in.readString();
}
}

View File

@@ -0,0 +1,43 @@
package net.headlezz.udacityproject1.tmdbapi;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
public class ReviewList implements Parcelable {
List<Review> reviews;
public ReviewList(List<Review> reviews) {
this.reviews = reviews;
}
public List<Review> getReviews() {
return reviews;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeList(reviews);
}
public static final Parcelable.Creator<ReviewList> CREATOR = new Parcelable.Creator<ReviewList>() {
public ReviewList createFromParcel(Parcel src) {
return new ReviewList(src);
}
public ReviewList[] newArray(int size) {
return new ReviewList[size];
}
};
private ReviewList(Parcel src) {
this.reviews = src.readArrayList(Review.class.getClassLoader());
}
}

View File

@@ -0,0 +1,23 @@
package net.headlezz.udacityproject1.tmdbapi;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class ReviewListDeserializer implements JsonDeserializer<ReviewList> {
@Override
public ReviewList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Gson gson = new Gson();
List<Review> reviews = new ArrayList<>();
for(JsonElement elem : json.getAsJsonObject().getAsJsonArray("results")) {
reviews.add(gson.fromJson(elem, Review.class));
}
return new ReviewList(reviews);
}
}

View File

@@ -6,11 +6,14 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.squareup.picasso.Picasso; import com.squareup.picasso.Picasso;
import net.headlezz.udacityproject1.R;
import retrofit.Call; import retrofit.Call;
import retrofit.GsonConverterFactory; import retrofit.GsonConverterFactory;
import retrofit.Retrofit; import retrofit.Retrofit;
import retrofit.http.GET; import retrofit.http.GET;
import retrofit.http.Headers; import retrofit.http.Headers;
import retrofit.http.Path;
import retrofit.http.Query; import retrofit.http.Query;
/** /**
@@ -37,6 +40,8 @@ public class TMDBApi {
private static TMDBApiService buildApiService() { private static TMDBApiService buildApiService() {
Gson gson = new GsonBuilder() Gson gson = new GsonBuilder()
.registerTypeAdapter(MovieList.class, new MovieListDeserializer()) .registerTypeAdapter(MovieList.class, new MovieListDeserializer())
.registerTypeAdapter(VideoList.class, new VideoListDeserializer())
.registerTypeAdapter(ReviewList.class, new ReviewListDeserializer())
.create(); .create();
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL) .baseUrl(BASE_URL)
@@ -46,7 +51,7 @@ public class TMDBApi {
} }
public static void loadPoster(ImageView imageView, Movie movie) { public static void loadPoster(ImageView imageView, Movie movie) {
Picasso.with(imageView.getContext()).load(IMAGE_BASE_URL + movie.getPosterPath()).into(imageView); Picasso.with(imageView.getContext()).load(IMAGE_BASE_URL + movie.getPosterPath()).error(R.drawable.poster_notfound).into(imageView);
} }
public static Call<MovieList> discoverMovies(int sortOrder, String apiKey) { public static Call<MovieList> discoverMovies(int sortOrder, String apiKey) {
@@ -61,6 +66,14 @@ public class TMDBApi {
return apiService.discoverMovies(sort, apiKey); return apiService.discoverMovies(sort, apiKey);
} }
public static Call<VideoList> getVideosForMovie(Movie movie, String apiKey) {
return apiService.getVideosForMovie(movie.getId(), apiKey);
}
public static Call<ReviewList> getReviewsForMovie(Movie movie, String apiKey) {
return apiService.getReviewsForMovie(movie.getId(), apiKey);
}
/** /**
* Api interface for retrofit * Api interface for retrofit
*/ */
@@ -68,6 +81,14 @@ public class TMDBApi {
@Headers("Accept: application/json") @Headers("Accept: application/json")
@GET("discover/movie") @GET("discover/movie")
Call<MovieList> discoverMovies(@Query("sort_by") String sortOrder, @Query("api_key") String apiKey); Call<MovieList> discoverMovies(@Query("sort_by") String sortOrder, @Query("api_key") String apiKey);
@Headers("Accept: application/json")
@GET("movie/{id}/videos")
Call<VideoList> getVideosForMovie(@Path("id") long id, @Query("api_key") String apiKey);
@Headers("Accept: application/json")
@GET("movie/{id}/reviews")
Call<ReviewList> getReviewsForMovie(@Path("id") long id, @Query("api_key") String apiKey);
} }
} }

View File

@@ -0,0 +1,45 @@
package net.headlezz.udacityproject1.tmdbapi;
import android.os.Parcel;
import android.os.Parcelable;
public class Video implements Parcelable {
private String name;
private String key;
public String getName() {
return name;
}
public String getKey() {
return key;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeString(key);
}
public static final Parcelable.Creator<Video> CREATOR = new Parcelable.Creator<Video>() {
public Video createFromParcel(Parcel src) {
return new Video(src);
}
public Video[] newArray(int size) {
return new Video[size];
}
};
private Video(Parcel src) {
name = src.readString();
key = src.readString();
}
}

View File

@@ -0,0 +1,56 @@
package net.headlezz.udacityproject1.tmdbapi;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
/**
* A list of trailers for a movie
*/
public class VideoList implements Parcelable {
private long id;
private List<Video> videos;
public long getId() {
return id;
}
public List<Video> getVideos() {
return videos;
}
public VideoList(long id, List<Video> videos) {
this.id = id;
this.videos = videos;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(id);
parcel.writeList(videos);
}
public static final Parcelable.Creator<VideoList> CREATOR = new Parcelable.Creator<VideoList>() {
public VideoList createFromParcel(Parcel src) {
return new VideoList(src);
}
public VideoList[] newArray(int size) {
return new VideoList[size];
}
};
private VideoList(Parcel src) {
id = src.readLong();
videos = src.readArrayList(Video.class.getClassLoader());
}
}

View File

@@ -0,0 +1,25 @@
package net.headlezz.udacityproject1.tmdbapi;
import com.google.gson.Gson;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class VideoListDeserializer implements JsonDeserializer<VideoList> {
@Override
public VideoList deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Gson gson = new Gson();
List<Video> videos = new ArrayList<>();
for(JsonElement elem : json.getAsJsonObject().getAsJsonArray("results")) {
if(elem.getAsJsonObject().get("site").getAsString().equals("YouTube")) // filter non youtube videos out
videos.add(gson.fromJson(elem, Video.class));
}
long id = json.getAsJsonObject().get("id").getAsLong();
return new VideoList(id, videos);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 717 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/colorAccent"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
tools:ignore="InconsistentLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<android.support.v7.widget.Toolbar android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false">
<FrameLayout
android:layout_weight="1"
android:id="@+id/fragmentHolder"
android:layout_width="0dp"
android:layout_height="match_parent" />
<FrameLayout
android:layout_weight="1"
android:id="@+id/fragmentHolderDetails"
android:layout_width="0dp"
android:layout_height="match_parent">
<TextView
android:id="@+id/main_details_placeHolderText"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/details_frag_placeholder_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -57,5 +57,39 @@
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." /> tools:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." />
<View
android:layout_marginBottom="8dp"
android:layout_marginTop="8dp"
android:background="@color/colorAccent"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/movie_details_tvOverview"
android:id="@+id/view" />
<LinearLayout
android:orientation="vertical"
android:id="@+id/movie_details_trailerHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/view"
android:layout_alignParentStart="true">
<TextView
android:visibility="gone"
android:text="@string/details_no_movies_found"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<Button
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reviews"
android:id="@+id/movie_details_btReviews"
android:layout_alignBottom="@+id/movie_details_ivPoster"
android:layout_alignStart="@+id/movie_details_tvRating" />
</RelativeLayout> </RelativeLayout>
</ScrollView> </ScrollView>

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:paddingTop="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:id="@+id/review_tvReviewUser"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:text="Username" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="200dp"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/review_tvReview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." />
</ScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageButton
android:id="@+id/review_btPrevious"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="left"
android:background="@android:color/transparent"
android:padding="12dp"
android:src="@drawable/ic_action_arrow_left"
android:tint="@android:color/white" />
<TextView
android:id="@+id/review_tvPosition"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge"
tools:text="5/10" />
<ImageButton
android:id="@+id/review_btNext"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="right"
android:background="@android:color/transparent"
android:padding="12dp"
android:src="@drawable/ic_action_arrow_right"
android:tint="@color/colorAccent" />
</FrameLayout>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="@drawable/orange_ripple">
<ImageView
android:tint="@android:color/white"
android:layout_width="50dp"
android:layout_height="50dp"
android:id="@+id/imageView"
android:src="@drawable/filmstrip" />
<TextView
android:id="@+id/trailer_item_tvName"
android:layout_marginLeft="16dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_gravity="center_vertical"
tools:text="This is a trailer title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:icon="@drawable/ic_action_share" android:id="@+id/action_share" android:title="@string/details_action_share" app:showAsAction="ifRoom" />
<item android:id="@+id/action_favorite" android:title="@string/details_action_favorite" app:showAsAction="ifRoom" />
</menu>

View File

@@ -1,6 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
<item android:id="@+id/action_sort_popularity" android:title="Sort by popularity" app:showAsAction="never" /> <item android:id="@+id/action_sort_popularity" android:title="@string/list_action_popular" app:showAsAction="never" />
<item android:id="@+id/action_sort_rating" android:title="Sort by av. rating" app:showAsAction="never" /> <item android:id="@+id/action_sort_rating" android:title="@string/list_action_rating" app:showAsAction="never" />
<item android:id="@+id/action_sort_favorites" android:title="@string/list_action_favorites" app:showAsAction="never" />
</menu> </menu>

View File

@@ -0,0 +1,3 @@
<resources>
<bool name="master_detail">true</bool>
</resources>

View File

@@ -1,6 +0,0 @@
<resources>
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
(such as screen margins) for screens with more than 820dp of available width. This
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
<dimen name="activity_horizontal_margin">64dp</dimen>
</resources>

View File

@@ -1,6 +1,3 @@
<resources> <resources>
<!-- Default screen margins, per the Android Design guidelines. --> <bool name="master_detail">false</bool>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources> </resources>

View File

@@ -3,4 +3,17 @@
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="movie_release_date">Release: %1$s</string> <string name="movie_release_date">Release: %1$s</string>
<string name="movie_rating">Rating: %1$s/10.0</string> <string name="movie_rating">Rating: %1$s/10.0</string>
<string name="details_action_share">Share</string>
<string name="details_action_favorite">Add/Remove Favorite</string>
<string name="list_action_popular">Popular movies</string>
<string name="list_action_rating">Highest av. rating</string>
<string name="list_action_favorites">Favorites</string>
<string name="details_no_movies_found">No videos found.</string>
<string name="details_frag_placeholder_text">Please select a movie</string>
<string name="movie_download_review_progress">Downloading...</string>
<string name="movie_download_review_not_found">No reviews found</string>
<string name="movie_download_review_fail">Review download failed.</string>
<string name="movie_share_fail">Youtube app not found.</string>
<string name="movie_share_no_trailer_found">Nothing to share. :(</string>
<string name="movie_share_title">Check out this movie!</string>
</resources> </resources>

1
project/build.scala Normal file
View File

@@ -0,0 +1 @@
object Build extends android.GradleBuild

2
project/plugins.sbt Normal file
View File

@@ -0,0 +1,2 @@
addSbtPlugin("com.hanhuy.sbt" % "android-gradle-build" % "1.1.6")
addSbtPlugin("com.hanhuy.sbt" % "android-protify" % "1.1.12")