/* * Copyright (C) 2016 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.android.apps.santatracker.map.cardstream; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Typeface; import android.os.Build; import android.support.v4.content.res.ResourcesCompat; import android.support.v7.widget.RecyclerView; import android.text.Html; import android.text.Spanned; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import com.bumptech.glide.Glide; import com.google.android.apps.santatracker.R; import com.google.android.apps.santatracker.data.Destination; import com.google.android.apps.santatracker.data.StreamEntry; import com.google.android.apps.santatracker.util.SantaLog; import com.google.android.gms.maps.model.LatLng; import com.google.android.youtube.player.YouTubeInitializationResult; import com.google.android.youtube.player.YouTubeThumbnailLoader; import com.google.android.youtube.player.YouTubeThumbnailView; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; public class CardAdapter extends RecyclerView.Adapter<CardViewHolder> { private final String mYouTubeApiDeveloperKey; private final Context mContext; private boolean mDisableDestinationPhoto = true; private final Typeface mTypefaceLabel; private final Typeface mTypefaceBody; private final CardAdapterListener mListener; private final DestinationCardKeyListener mDestinationCardListener; private final long mDashboardId; private TvFocusAnimator mFocusHighlight; public static final int DASHBOARD_POSITION = 0; /** * The list of cards. Note that the cards always need to be sorted by timestamps in the * descending order. */ private final ArrayList<TrackerCard> mCards = new ArrayList<>(); private ThumbnailListener mThumbnailListener; private Map<YouTubeThumbnailView, YouTubeThumbnailLoader> mThumbnailViewToLoader = new HashMap<>(); private static final String TAG = "CardAdaptor"; private final boolean mIsTv; public CardAdapter(Context context, CardAdapterListener listener) { this(context, listener, null, false); } public CardAdapter(Context context, CardAdapterListener listener, DestinationCardKeyListener destCardListener, boolean isTv) { mContext = context; mTypefaceLabel = Typeface.createFromAsset(context.getAssets(), context.getResources().getString(R.string.typeface_roboto_black)); mTypefaceBody = Typeface.createFromAsset(context.getAssets(), context.getResources().getString(R.string.typeface_roboto_light)); mListener = listener; mDestinationCardListener = destCardListener; TrackerCard.Dashboard dashboard = TrackerCard.Dashboard.getInstance(); mDashboardId = dashboard.id; mCards.add(DASHBOARD_POSITION, dashboard); mThumbnailListener = new ThumbnailListener(); mYouTubeApiDeveloperKey = context.getString(R.string.config_maps_api_key); mIsTv = isTv; setHasStableIds(true); } @Override public CardViewHolder onCreateViewHolder(ViewGroup parent, int type) { CardViewHolder holder; switch (type) { case TrackerCard.TYPE_DASHBOARD: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_dashboard, parent, false); holder = new DashboardViewHolder(view); break; } case TrackerCard.TYPE_FACTOID: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_factoid, parent, false); holder = new FactoidViewHolder(view); break; } case TrackerCard.TYPE_DESTINATION: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_destination, parent, false); holder = new DestinationViewHolder(view); break; } case TrackerCard.TYPE_PHOTO: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_photo, parent, false); holder = new PhotoViewHolder(view); break; } case TrackerCard.TYPE_MOVIE: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_movie, parent, false); holder = new MovieViewHolder(view); break; } case TrackerCard.TYPE_STATUS: { View view = LayoutInflater.from(mContext) .inflate(R.layout.item_card_update, parent, false); holder = new UpdateViewHolder(view); break; } default: { throw new IllegalArgumentException("Unexpected type of card."); } } holder.setTypefaces(mTypefaceLabel, mTypefaceBody); setupTvCardIfNecessary(holder); return holder; } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private void setupTvCardIfNecessary(CardViewHolder holder) { if (mIsTv) { if (mFocusHighlight == null) { mFocusHighlight = new TvFocusAnimator(); } final Resources res = holder.itemView.getResources(); TvFocusAnimator.FocusChangeListener listener = new TvFocusAnimator.FocusChangeListener(mFocusHighlight); holder.itemView.setFocusable(true); holder.itemView.setFocusableInTouchMode(false); holder.itemView.setOnFocusChangeListener(listener); holder.itemView.setElevation(res.getDimensionPixelOffset(R.dimen.toolbar_elevation)); mFocusHighlight.onInitializeView(holder.itemView); if (holder.itemView.getBackground() == null) { holder.itemView.setBackground(ResourcesCompat.getDrawable(res, R.drawable.tv_tracker_card_selector, holder.itemView.getContext().getTheme())); } } } @Override public void onBindViewHolder(CardViewHolder holder, int position) { TrackerCard card = mCards.get(position); if (card instanceof TrackerCard.Dashboard) { ((DashboardViewHolder) holder).location.setSelected(true); } else if (card instanceof TrackerCard.DestinationCard) { TrackerCard.DestinationCard destination = (TrackerCard.DestinationCard) card; DestinationViewHolder h = (DestinationViewHolder) holder; Context context = h.itemView.getContext(); h.city.setText(destination.city); if (destination.region == null) { h.region.setVisibility(View.GONE); } else { h.region.setText(destination.region); h.region.setVisibility(View.VISIBLE); } if (destination.url != null && destination.attributionHtml != null) { // Image Glide.with(context).load(destination.url).into(h.image); // Attribution Spanned attribution; if (Build.VERSION.SDK_INT >= 24) { attribution = Html.fromHtml(destination.attributionHtml, 0); } else { //noinspection deprecation attribution = Html.fromHtml(destination.attributionHtml); } h.copyright.setText(attribution); } h.arrival.setText(DashboardFormats.formatTime(context, destination.timestamp)); if (destination.hasWeather) { h.weatherLabel.setVisibility(View.VISIBLE); h.weather.setVisibility(View.VISIBLE); h.weather.setText(context.getString(R.string.weather_format, String.valueOf((int) destination.tempC), String.valueOf((int) destination.tempF))); } else { h.weatherLabel.setVisibility(View.GONE); h.weather.setVisibility(View.GONE); } if (mIsTv) { h.streetView.setVisibility(View.GONE); if (destination.position != null) { h.card.setTag(destination.position); h.card.setOnKeyListener(mMoveToDestinationListener); h.card.setClickable(true); } else { h.card.setTag(null); h.card.setOnKeyListener(null); h.card.setClickable(false); } } else { if (destination.streetView != null) { h.streetView.setVisibility(View.VISIBLE); h.streetView.setOnClickListener(mShowStreetViewListener); h.streetView.setTag(destination.streetView); } else if (h.streetView != null) { h.streetView.setVisibility(View.GONE); } } } else if (card instanceof TrackerCard.FactoidCard) { TrackerCard.FactoidCard factoid = (TrackerCard.FactoidCard) card; FactoidViewHolder h = (FactoidViewHolder) holder; h.body.setText(factoid.factoid); } else if (card instanceof TrackerCard.PhotoCard) { TrackerCard.PhotoCard photo = (TrackerCard.PhotoCard) card; PhotoViewHolder h = (PhotoViewHolder) holder; Glide.with(h.image.getContext()).load(photo.imageUrl).into(h.image); } else if (card instanceof TrackerCard.MovieCard) { if (mThumbnailViewToLoader == null || mThumbnailListener == null) { return; } TrackerCard.MovieCard movie = (TrackerCard.MovieCard) card; MovieViewHolder h = (MovieViewHolder) holder; h.thumbnail.setTag(movie.youtubeId); if (mThumbnailViewToLoader.containsKey(h.thumbnail)) { final YouTubeThumbnailLoader loader = mThumbnailViewToLoader.get(h.thumbnail); if (loader != null) { loader.setVideo(movie.youtubeId); } } else { h.thumbnail.setImageDrawable(null); h.thumbnail.initialize(mYouTubeApiDeveloperKey, mThumbnailListener); mThumbnailViewToLoader.put(h.thumbnail, null); } h.play.setTag(movie.youtubeId); h.play.setOnClickListener(mPlayVideoListener); if (mIsTv) { h.card.setTag(movie.youtubeId); h.card.setOnClickListener(mPlayVideoListener); } } else if (card instanceof TrackerCard.StatusCard) { TrackerCard.StatusCard status = (TrackerCard.StatusCard) card; UpdateViewHolder h = (UpdateViewHolder) holder; h.content.setText(status.status); } } @Override public void onDetachedFromRecyclerView(RecyclerView recyclerView) { release(); super.onDetachedFromRecyclerView(recyclerView); } @Override public long getItemId(int position) { return mCards.get(position).id; } @Override public int getItemCount() { return mCards.size(); } @Override public int getItemViewType(int position) { return mCards.get(position).getType(); } public long getDashboardId() { return mDashboardId; } /** * Find the right index to insert a new card with the specified {@code timestamp}. Note that * this method assumes that {@link #mCards} are sorted by timestamps in the descending order. * * @param timestamp The unix time. * @return The index to insert a new card in {@link #mCards}. */ private int findCardIndexByTimestamp(long timestamp) { // This is basically a binary search that doesn't search for a specific value but an index. int head = 0; int tail = mCards.size(); while (head < tail) { int needle = (head + tail) / 2; long needleTimestamp = mCards.get(needle).timestamp; if (timestamp < needleTimestamp) { head = needle + 1; } else { tail = needle; } } return head; } private int addCard(TrackerCard card) { final int index = findCardIndexByTimestamp(card.timestamp); // Replace a old duplicated card if it exists. if (index < mCards.size() && mCards.get(index).equals(card)) { mCards.remove(index); mCards.add(index, card); notifyItemChanged(index); } else { mCards.add(index, card); notifyItemInserted(index); } return index; } public int addDestination(boolean fromUser, Destination destination, boolean showStreetView) { String url = null; String copyright = null; if (!mDisableDestinationPhoto && destination.photos != null && destination.photos.length > 0) { url = destination.photos[0].url; copyright = destination.photos[0].attributionHTML; } Destination.StreetView streetView = null; if (showStreetView && destination.streetView != null && destination.streetView.id != null && !destination.streetView.id.isEmpty()) { streetView = destination.streetView; } boolean hasWeather = destination.weather != null; double tempC = 0, tempF = 0; if (hasWeather) { tempC = destination.weather.tempC; tempF = destination.weather.tempF; } return addCard(new TrackerCard.DestinationCard(destination.arrival, destination.position, fromUser, destination.city, destination.region, url, copyright, streetView, hasWeather, tempC, tempF)); } public TrackerCard addStreamEntry(StreamEntry entry) { TrackerCard card = null; if (entry.didYouKnow != null) { // Did you know card card = new TrackerCard.FactoidCard(entry.timestamp, entry.didYouKnow); addCard(card); } else if (entry.santaStatus != null) { // Status card card = new TrackerCard.StatusCard(entry.timestamp, entry.santaStatus); addCard(card); } else if (entry.image != null) { // Image card card = new TrackerCard.PhotoCard(entry.timestamp, entry.image, entry.caption); addCard(card); } else if (entry.video != null) { // Video card card = new TrackerCard.MovieCard(entry.timestamp, entry.video); addCard(card); } return card; } public void setNextLocation(String nextLocation) { TrackerCard.Dashboard.getInstance().nextDestination = nextLocation; } public void setDestinationPhotoDisabled(boolean disablePhoto) { mDisableDestinationPhoto = disablePhoto; } private final class ThumbnailListener implements YouTubeThumbnailView.OnInitializedListener, YouTubeThumbnailLoader.OnThumbnailLoadedListener { @Override public void onInitializationSuccess( YouTubeThumbnailView view, YouTubeThumbnailLoader loader) { loader.setOnThumbnailLoadedListener(this); view.setScaleType(ImageView.ScaleType.CENTER_CROP); loader.setVideo((String) view.getTag()); mThumbnailViewToLoader.put(view, loader); } @Override public void onInitializationFailure( YouTubeThumbnailView view, YouTubeInitializationResult loader) { SantaLog.e(TAG, "Failed to initialize YouTubeThumbnailView."); view.setImageResource(R.drawable.big_play_button); view.setScaleType(ImageView.ScaleType.CENTER); } @Override public void onThumbnailLoaded(YouTubeThumbnailView view, String videoId) { } @Override public void onThumbnailError(YouTubeThumbnailView view, YouTubeThumbnailLoader.ErrorReason errorReason) { Log.e(TAG, "Failed to load YouTubThumbnail"); SantaLog.e(TAG, errorReason.toString()); view.setImageResource(R.drawable.big_play_button); } } public interface DestinationCardKeyListener { void onJumpToDestination(LatLng destination); void onFinish(); boolean onMoveBy(KeyEvent event); } public interface CardAdapterListener { void onOpenStreetView(Destination.StreetView streetView); void onPlayVideo(String youtubeId); } private View.OnClickListener mShowStreetViewListener = new View.OnClickListener() { @Override public void onClick(View v) { mListener.onOpenStreetView((Destination.StreetView) v.getTag()); } }; private View.OnClickListener mPlayVideoListener = new View.OnClickListener() { @Override public void onClick(View v) { mListener.onPlayVideo((String) v.getTag()); } }; private View.OnKeyListener mMoveToDestinationListener = new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (mDestinationCardListener == null) { return false; } if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A) { switch (event.getAction()) { case KeyEvent.ACTION_DOWN: mDestinationCardListener.onJumpToDestination((LatLng) v.getTag()); break; case KeyEvent.ACTION_UP: mDestinationCardListener.onFinish(); break; } return false; } // When a DPAD is pressed, fire an 'onMove' event. switch (keyCode) { case KeyEvent.KEYCODE_DPAD_UP: //fall through case KeyEvent.KEYCODE_DPAD_DOWN: //fall through case KeyEvent.KEYCODE_DPAD_LEFT: //fall through case KeyEvent.KEYCODE_DPAD_RIGHT: return mDestinationCardListener.onMoveBy(event); } return false; } }; public void release() { if (mThumbnailViewToLoader != null) { for (YouTubeThumbnailLoader loader : mThumbnailViewToLoader.values()) { if (loader != null) { loader.release(); } } mThumbnailViewToLoader.clear(); mThumbnailViewToLoader = null; } mThumbnailListener = null; } }