package com.battlelancer.seriesguide.adapters; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.support.v4.util.LongSparseArray; import android.support.v4.widget.CursorAdapter; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.battlelancer.seriesguide.R; import com.battlelancer.seriesguide.SgApp; import com.battlelancer.seriesguide.adapters.model.HeaderData; import com.battlelancer.seriesguide.enums.EpisodeFlags; import com.battlelancer.seriesguide.provider.SeriesGuideContract; import com.battlelancer.seriesguide.provider.SeriesGuideContract.Episodes; import com.battlelancer.seriesguide.provider.SeriesGuideDatabase; import com.battlelancer.seriesguide.settings.DisplaySettings; import com.battlelancer.seriesguide.thetvdbapi.TvdbImageTools; import com.battlelancer.seriesguide.ui.CalendarFragment; import com.battlelancer.seriesguide.util.EpisodeTools; import com.battlelancer.seriesguide.util.TextTools; import com.battlelancer.seriesguide.util.TimeTools; import com.battlelancer.seriesguide.widgets.WatchedBox; import com.tonicartos.widget.stickygridheaders.StickyGridHeadersBaseAdapter; import com.uwetrottmann.androidutils.CheatSheet; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; /** * Adapter for {@link CalendarFragment} with optimizations for image loading for smoother * scrolling. */ public class CalendarAdapter extends CursorAdapter implements StickyGridHeadersBaseAdapter { private final SgApp app; private LayoutInflater mLayoutInflater; private List<HeaderData> mHeaders; private boolean mIsShowingHeaders; private Calendar mCalendar; public CalendarAdapter(Activity activity) { super(activity, null, 0); app = SgApp.from(activity); mLayoutInflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); mCalendar = Calendar.getInstance(); } /** * Whether to show episodes grouped by day with header. Disable headers for larger data sets as * calculating them is expensive. Make sure to reload the data afterwards. */ public void setIsShowingHeaders(boolean isShowingHeaders) { mIsShowingHeaders = isShowingHeaders; } @Override public void bindView(View view, final Context context, Cursor cursor) { ViewHolder viewHolder = (ViewHolder) view.getTag(); // watched box // save rowid to hand over to OnClick event listener final int showTvdbId = cursor.getInt(Query.SHOW_ID); final int season = cursor.getInt(Query.SEASON); final int episodeTvdbId = cursor.getInt(Query._ID); final int episode = cursor.getInt(Query.NUMBER); viewHolder.watchedBox.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { WatchedBox box = (WatchedBox) v; // disable button, will be re-enabled on data reload once action completes box.setEnabled(false); EpisodeTools.episodeWatched(app, showTvdbId, episodeTvdbId, season, episode, EpisodeTools.isWatched(box.getEpisodeFlag()) ? EpisodeFlags.UNWATCHED : EpisodeFlags.WATCHED ); } }); int episodeFlag = cursor.getInt(Query.WATCHED); viewHolder.watchedBox.setEpisodeFlag(episodeFlag); viewHolder.watchedBox.setEnabled(true); boolean watched = EpisodeTools.isWatched(episodeFlag); viewHolder.watchedBox.setContentDescription( context.getString(watched ? R.string.action_unwatched : R.string.action_watched)); CheatSheet.setup(viewHolder.watchedBox, watched ? R.string.action_unwatched : R.string.action_watched ); // show title viewHolder.show.setText(cursor.getString(Query.SHOW_TITLE)); // episode number and title if (EpisodeTools.isUnwatched(episodeFlag) && DisplaySettings.preventSpoilers(context)) { // just show the number viewHolder.episode.setText(TextTools.getEpisodeNumber(context, season, episode)); } else { // show number and title viewHolder.episode.setText(TextTools.getNextEpisodeString(context, season, episode, cursor.getString(Query.TITLE))); } // timestamp, absolute time and network StringBuilder releaseInfo = new StringBuilder(); long releaseTime = cursor.getLong(Query.RELEASE_TIME_MS); if (releaseTime != -1) { Date actualRelease = TimeTools.applyUserOffset(context, releaseTime); // timestamp boolean displayExactDate = DisplaySettings.isDisplayExactDate(context); viewHolder.timestamp.setText(displayExactDate ? TimeTools.formatToLocalDateShort(context, actualRelease) : TimeTools.formatToLocalRelativeTime(context, actualRelease)); // "10:00 PM / Network", as left aligned, exactly mirrored from show list releaseInfo.append(TimeTools.formatToLocalTime(context, actualRelease)); } else { viewHolder.timestamp.setText(null); } final String network = cursor.getString(Query.SHOW_NETWORK); if (!TextUtils.isEmpty(network)) { releaseInfo.append(" / ").append(network); } viewHolder.info.setText(releaseInfo); // collected indicator boolean isCollected = EpisodeTools.isCollected( cursor.getInt(Query.COLLECTED)); viewHolder.collected.setVisibility(isCollected ? View.VISIBLE : View.GONE); // set poster TvdbImageTools.loadShowPosterResizeSmallCrop(context, viewHolder.poster, TvdbImageTools.smallSizeUrl(cursor.getString(Query.SHOW_POSTER_PATH))); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View v = mLayoutInflater.inflate(R.layout.item_calendar, parent, false); ViewHolder viewHolder = new ViewHolder(v); v.setTag(viewHolder); return v; } private long getHeaderId(int position) { Object obj = getItem(position); if (obj != null) { /* * Map all episodes releasing the same day to the same id (which * equals the time midnight of their release day). */ @SuppressWarnings("resource") Cursor item = (Cursor) obj; return getHeaderTime(item); } return 0; } private long getHeaderTime(Cursor item) { long releaseTime = item.getLong(Query.RELEASE_TIME_MS); Date actualRelease = TimeTools.applyUserOffset(mContext, releaseTime); mCalendar.setTime(actualRelease); // not midnight because upcoming->recent is delayed 1 hour // so header would display wrong relative time close to midnight mCalendar.set(Calendar.HOUR_OF_DAY, 1); mCalendar.set(Calendar.MINUTE, 0); mCalendar.set(Calendar.SECOND, 0); mCalendar.set(Calendar.MILLISECOND, 0); return mCalendar.getTimeInMillis(); } @Override public int getCountForHeader(int position) { if (mHeaders != null) { return mHeaders.get(position).getCount(); } return 0; } @Override public int getNumHeaders() { if (mHeaders != null) { return mHeaders.size(); } return 0; } @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { // get header position for item position position = mHeaders.get(position).getRefPosition(); Object obj = getItem(position); if (obj == null) { return null; } HeaderViewHolder holder; if (convertView == null) { convertView = mLayoutInflater.inflate(R.layout.item_grid_header, parent, false); holder = new HeaderViewHolder(); holder.day = (TextView) convertView.findViewById(R.id.textViewGridHeader); convertView.setTag(holder); } else { holder = (HeaderViewHolder) convertView.getTag(); } @SuppressWarnings("resource") Cursor item = (Cursor) obj; long headerTime = getHeaderTime(item); // display headers like "Mon in 3 days", also "today" when applicable holder.day.setText( TimeTools.formatToLocalDayAndRelativeTime(mContext, new Date(headerTime))); return convertView; } @Override public void notifyDataSetChanged() { // re-create headers before letting notifyDataSetChanged reach the AdapterView mHeaders = generateHeaderList(); super.notifyDataSetChanged(); } @Override public void notifyDataSetInvalidated() { // remove headers before letting notifyDataSetChanged reach the AdapterView mHeaders = null; super.notifyDataSetInvalidated(); } protected List<HeaderData> generateHeaderList() { int count = getCount(); if (count == 0 || !mIsShowingHeaders) { return null; } // pre-size to 30 as we display 30 days == headers at most LongSparseArray<HeaderData> mapping = new LongSparseArray<>(30); List<HeaderData> headers = new ArrayList<>(); for (int position = 0; position < count; position++) { long headerId = getHeaderId(position); HeaderData headerData = mapping.get(headerId); if (headerData == null) { headerData = new HeaderData(position); headers.add(headerData); } headerData.incrementCount(); mapping.put(headerId, headerData); } return headers; } public interface Query { String[] PROJECTION = new String[] { SeriesGuideDatabase.Tables.EPISODES + "." + Episodes._ID, Episodes.TITLE, Episodes.NUMBER, Episodes.SEASON, Episodes.FIRSTAIREDMS, Episodes.WATCHED, Episodes.COLLECTED, SeriesGuideContract.Shows.REF_SHOW_ID, SeriesGuideContract.Shows.TITLE, SeriesGuideContract.Shows.NETWORK, SeriesGuideContract.Shows.POSTER }; String QUERY_UPCOMING = Episodes.FIRSTAIREDMS + ">=? AND " + Episodes.FIRSTAIREDMS + "<? AND " + SeriesGuideContract.Shows.SELECTION_NO_HIDDEN; String QUERY_RECENT = Episodes.SELECTION_HAS_RELEASE_DATE + " AND " + Episodes.FIRSTAIREDMS + "<? AND " + Episodes.FIRSTAIREDMS + ">? AND " + SeriesGuideContract.Shows.SELECTION_NO_HIDDEN; String SORTING_UPCOMING = Episodes.FIRSTAIREDMS + " ASC," + SeriesGuideContract.Shows.SORT_TITLE + "," + Episodes.NUMBER + " ASC"; String SORTING_RECENT = Episodes.FIRSTAIREDMS + " DESC," + SeriesGuideContract.Shows.SORT_TITLE + "," + Episodes.NUMBER + " DESC"; int _ID = 0; int TITLE = 1; int NUMBER = 2; int SEASON = 3; int RELEASE_TIME_MS = 4; int WATCHED = 5; int COLLECTED = 6; int SHOW_ID = 7; int SHOW_TITLE = 8; int SHOW_NETWORK = 9; int SHOW_POSTER_PATH = 10; } static class ViewHolder { public TextView show; public TextView episode; public View collected; public WatchedBox watchedBox; public TextView info; public TextView timestamp; public ImageView poster; public ViewHolder(View v) { show = (TextView) v.findViewById(R.id.textViewActivityShow); episode = (TextView) v.findViewById(R.id.textViewActivityEpisode); collected = v.findViewById(R.id.imageViewActivityCollected); watchedBox = (WatchedBox) v.findViewById(R.id.watchedBoxActivity); info = (TextView) v.findViewById(R.id.textViewActivityInfo); timestamp = (TextView) v.findViewById(R.id.textViewActivityTimestamp); poster = (ImageView) v.findViewById(R.id.imageViewActivityPoster); } } static class HeaderViewHolder { public TextView day; } }