/*
* Copyright 2014 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.iosched.videolibrary;
import android.app.Activity;
import android.app.Fragment;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.util.Pair;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
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 com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.archframework.ModelWithLoaderManager;
import com.google.samples.apps.iosched.archframework.PresenterImpl;
import com.google.samples.apps.iosched.archframework.UpdatableView;
import com.google.samples.apps.iosched.injection.ModelProvider;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.ui.widget.DrawShadowFrameLayout;
import com.google.samples.apps.iosched.ui.widget.recyclerview.ItemMarginDecoration;
import com.google.samples.apps.iosched.ui.widget.recyclerview.UpdatableAdapter;
import com.google.samples.apps.iosched.util.AccountUtils;
import com.google.samples.apps.iosched.util.AnalyticsHelper;
import com.google.samples.apps.iosched.util.ImageLoader;
import com.google.samples.apps.iosched.util.UIUtils;
import com.google.samples.apps.iosched.videolibrary.VideoLibraryModel.VideoLibraryQueryEnum;
import com.google.samples.apps.iosched.videolibrary.VideoLibraryModel.VideoLibraryUserActionEnum;
import com.google.samples.apps.iosched.videolibrary.data.VideoTrack;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* This Fragment displays all the videos of past Google I/O sessions in the form of a card for each
* topics and a card for new videos of the current year.
*/
public class VideoLibraryFragment extends Fragment
implements UpdatableView<VideoLibraryModel, VideoLibraryQueryEnum,
VideoLibraryUserActionEnum> {
private static final String TAG = makeLogTag(VideoLibraryFragment.class);
private static final String VIDEO_LIBRARY_ANALYTICS_CATEGORY = "Video Library";
private ImageLoader mImageLoader;
private RecyclerView mCardList = null;
private VideosAdapter mAdapter;
private View mEmptyView = null;
private List<UserActionListener> mListeners = new ArrayList<>();
private SharedPreferences.OnSharedPreferenceChangeListener mSettingsChangeListener =
new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (AccountUtils.PREF_ACTIVE_ACCOUNT.equals(key)) {
fireReloadUserVideosEvent();
}
}
};
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.video_library_frag, container, false);
mCardList = (RecyclerView) root.findViewById(R.id.videos_card_list);
mCardList.setHasFixedSize(true);
final int cardVerticalMargin = getResources().getDimensionPixelSize(R.dimen.spacing_normal);
mCardList.addItemDecoration(new ItemMarginDecoration(0, cardVerticalMargin,
0, cardVerticalMargin));
mEmptyView = root.findViewById(android.R.id.empty);
getActivity().overridePendingTransition(0, 0);
return root;
}
@Override
public void displayData(final VideoLibraryModel model,
final VideoLibraryQueryEnum query) {
if ((VideoLibraryModel.VideoLibraryQueryEnum.VIDEOS == query
|| VideoLibraryModel.VideoLibraryQueryEnum.MY_VIEWED_VIDEOS == query)
&& model.hasVideos()) {
if (mAdapter == null) {
mAdapter = new VideosAdapter(getActivity(), model, mImageLoader, mListeners);
mCardList.setAdapter(mAdapter);
} else {
mAdapter.update(model);
}
}
mEmptyView.setVisibility(
(mAdapter != null && mAdapter.getItemCount() > 0) ? View.GONE : View.VISIBLE);
}
@Override
public void displayErrorMessage(final VideoLibraryQueryEnum query) {
// No UI changes upon query error
}
@Override
public void displayUserActionResult(final VideoLibraryModel model,
final VideoLibraryUserActionEnum userAction, final boolean success) {
switch (userAction) {
case VIDEO_PLAYED:
case RELOAD_USER_VIDEOS:
displayData(model, VideoLibraryQueryEnum.MY_VIEWED_VIDEOS);
break;
}
}
@Override
public Uri getDataUri(final VideoLibraryQueryEnum query) {
switch (query) {
case VIDEOS:
return ScheduleContract.Videos.CONTENT_URI;
case MY_VIEWED_VIDEOS:
return ScheduleContract.MyViewedVideos.CONTENT_URI;
default:
return Uri.EMPTY;
}
}
@Override
public Context getContext() {
return getActivity();
}
@Override
public void addListener(UserActionListener toAdd) {
mListeners.add(toAdd);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mImageLoader = new ImageLoader(getActivity(), android.R.color.transparent);
initPresenter();
}
private void initPresenter() {
VideoLibraryModel model = ModelProvider
.provideVideoLibraryModel(getDataUri(VideoLibraryQueryEnum.VIDEOS),
getDataUri(VideoLibraryQueryEnum.MY_VIEWED_VIDEOS),
getDataUri(VideoLibraryQueryEnum.FILTERS), getActivity(),
getLoaderManager());
PresenterImpl presenter =
new PresenterImpl(model, this, VideoLibraryUserActionEnum.values(),
VideoLibraryQueryEnum.values());
presenter.loadInitialQueries();
}
@Override
public void onResume() {
super.onResume();
getActivity().invalidateOptionsMenu();
final DrawShadowFrameLayout drawShadowFrameLayout =
(DrawShadowFrameLayout) getActivity().findViewById(R.id.main_content);
if (drawShadowFrameLayout != null) {
// Configure video fragment's top clearance to take our overlaid Toolbar into account.
drawShadowFrameLayout.setShadowTopOffset(UIUtils.calculateActionBarSize(getActivity()));
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// Register preference change listeners
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
sp.registerOnSharedPreferenceChangeListener(mSettingsChangeListener);
}
@Override
public void onDetach() {
super.onDetach();
if (mSettingsChangeListener != null) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
sp.unregisterOnSharedPreferenceChangeListener(mSettingsChangeListener);
}
}
/**
* Let all UserActionListener know that the user has changed.
*/
private void fireReloadUserVideosEvent() {
if (!isAdded()) {
return;
}
for (UserActionListener h1 : mListeners) {
Bundle args = new Bundle();
args.putInt(ModelWithLoaderManager.KEY_RUN_QUERY_ID,
VideoLibraryQueryEnum.MY_VIEWED_VIDEOS.getId());
h1.onUserAction(VideoLibraryUserActionEnum.RELOAD_USER_VIDEOS, args);
}
}
/**
* An adapter for providing data for the main list in this fragment. This shows a card per
* video track. Each card contains a scrolling list of videos within that track or can lead
* to a details screen showing an expanded view of the track.
*/
private static class VideosAdapter
extends UpdatableAdapter<VideoLibraryModel, VideoTrackViewHolder> {
// Immutable state
private final Activity mHost;
private final LayoutInflater mInflater;
private final ImageLoader mImageLoader;
private final List<UserActionListener> mListeners;
private final RecyclerView.RecycledViewPool mRecycledViewPool;
// State
private List<VideoTrack> mVideoTracks;
private SparseArrayCompat<VideoTrackAdapter> mTrackVideosAdapters;
private SparseArrayCompat<Parcelable> mTrackVideosState;
VideosAdapter(@NonNull Activity activity,
@NonNull VideoLibraryModel model,
@NonNull ImageLoader imageLoader,
@NonNull List<UserActionListener> listeners) {
mHost = activity;
mInflater = LayoutInflater.from(activity);
mImageLoader = imageLoader;
mListeners = listeners;
mRecycledViewPool = new RecyclerView.RecycledViewPool();
mVideoTracks = processVideos(model);
setupVideoTrackAdapters();
}
@Override
public VideoTrackViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
final VideoTrackViewHolder holder = new VideoTrackViewHolder(
mInflater.inflate(R.layout.explore_io_track_card, parent, false));
holder.videos.setHasFixedSize(true);
holder.videos.setRecycledViewPool(mRecycledViewPool);
ViewCompat.setImportantForAccessibility(
holder.videos, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
holder.header.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
final int position = holder.getAdapterPosition();
if (position == RecyclerView.NO_POSITION) return;
final VideoTrack videoTrack = mVideoTracks.get(position);
// ANALYTICS EVENT: Click on the "More" button of a card in the Video Library
// Contains: The clicked header's label
AnalyticsHelper.sendEvent(VIDEO_LIBRARY_ANALYTICS_CATEGORY, "morebutton",
videoTrack.getTrack());
// Start the Filtered Video Library intent.
final Intent filtered = new Intent(mHost, VideoLibraryFilteredActivity.class);
if (videoTrack.getTrackId() == VideoLibraryModel.TRACK_ID_KEYNOTES) {
filtered.putExtra(VideoLibraryFilteredActivity.KEY_FILTER_TOPIC,
VideoLibraryModel.KEYNOTES_TOPIC);
} else if (videoTrack.getTrackId() == VideoLibraryModel.TRACK_ID_NEW) {
filtered.putExtra(VideoLibraryFilteredActivity.KEY_FILTER_YEAR,
Calendar.getInstance().get(Calendar.YEAR));
} else {
filtered.putExtra(VideoLibraryFilteredActivity.KEY_FILTER_TOPIC,
videoTrack.getTrack());
}
final ActivityOptionsCompat options =
ActivityOptionsCompat.makeSceneTransitionAnimation(mHost,
Pair.create((View) holder.headerImage, mHost.getString(
R.string.transition_track_header)),
Pair.create(holder.itemView, mHost.getString(
R.string.transition_track_background)));
ActivityCompat.startActivity(mHost, filtered, options.toBundle());
}
});
return holder;
}
@Override
public void onBindViewHolder(final VideoTrackViewHolder holder, final int position) {
final VideoTrack videoTrack = mVideoTracks.get(position);
holder.title.setText(videoTrack.getTrack());
holder.header.setContentDescription(videoTrack.getTrack());
if (videoTrack.getTrackImageUrl() != null) {
holder.headerImage.setScaleType(ImageView.ScaleType.CENTER_CROP);
mImageLoader.loadImage(videoTrack.getTrackImageUrl(), holder.headerImage);
} else {
holder.headerImage.setScaleType(ImageView.ScaleType.FIT_CENTER);
holder.headerImage.setImageResource(R.drawable.ic_hash_io_16_monochrome);
}
holder.videos.setAdapter(mTrackVideosAdapters.get(videoTrack.getTrackId()));
holder.videos.getLayoutManager().onRestoreInstanceState(
mTrackVideosState.get(videoTrack.getTrackId()));
}
@Override
public void onViewRecycled(final VideoTrackViewHolder holder) {
// Cache the scroll position of the video list so that we can restore it in onBind
final int position = holder.getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
final VideoTrack videoTrack = mVideoTracks.get(position);
mTrackVideosState.put(videoTrack.getTrackId(),
holder.videos.getLayoutManager().onSaveInstanceState());
}
super.onViewRecycled(holder);
}
@Override
public int getItemCount() {
return mVideoTracks.size();
}
@Override
public void update(@NonNull final VideoLibraryModel updatedData) {
// Attempt to update our model in-place to keep scroll position etc
final List<VideoTrack> newVideos = processVideos(updatedData);
boolean changed = false;
if (newVideos.size() != mVideoTracks.size()) {
changed = true;
} else {
for (int i = 0; i < newVideos.size(); i++) {
final VideoTrack newVideoTrack = newVideos.get(i);
final VideoTrack oldVideoTrack = mVideoTracks.get(i);
if (newVideoTrack.equals(oldVideoTrack)) {
mTrackVideosAdapters.get(newVideoTrack.getTrackId())
.update(newVideoTrack.getVideos());
} else {
changed = true;
break;
}
}
}
if (changed) {
// Couldn't do an in-place update, do a full refresh
mVideoTracks = newVideos;
setupVideoTrackAdapters();
notifyDataSetChanged();
}
}
/**
* Process the data for use; we get given a flat list of videos. Group them by track with
* special handling of keynotes and new talks from this year. We assume that the provided
* data is already sorted by track.
* @param model
*/
private List<VideoTrack> processVideos(VideoLibraryModel model) {
final List<VideoTrack> data = new ArrayList<>();
final VideoTrack keynoteVideos = model.getKeynoteVideos();
if (keynoteVideos != null) {
data.add(keynoteVideos);
}
final VideoTrack currentYearVideos = model.getCurrentYearVideos();
if (currentYearVideos != null) {
data.add(currentYearVideos);
}
final List<VideoTrack> videos = model.getVideos();
if (videos != null && !videos.isEmpty()) {
data.addAll(videos);
}
return data;
}
/**
* Loop over {@link #mVideoTracks} and create adaptor & state objects for each track.
*/
private void setupVideoTrackAdapters() {
mTrackVideosAdapters = new SparseArrayCompat<>(mVideoTracks.size());
mTrackVideosState = new SparseArrayCompat<>(mVideoTracks.size());
for (final VideoTrack videoTrack : mVideoTracks) {
mTrackVideosAdapters.put(videoTrack.getTrackId(),
VideoTrackAdapter.createHorizontal(mHost,
videoTrack.getVideos(), mImageLoader, mListeners));
}
}
}
private static class VideoTrackViewHolder extends RecyclerView.ViewHolder {
final ViewGroup header;
final TextView title;
final RecyclerView videos;
final ImageView headerImage;
public VideoTrackViewHolder(final View itemView) {
super(itemView);
header = (ViewGroup) itemView.findViewById(R.id.header);
title = (TextView) itemView.findViewById(R.id.title);
videos = (RecyclerView) itemView.findViewById(R.id.sessions);
headerImage = (ImageView) itemView.findViewById(R.id.header_image);
}
}
}