aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr/ui/browse
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/dvr/ui/browse')
-rw-r--r--src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java134
-rw-r--r--src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java120
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java207
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java299
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java92
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java35
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java634
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java98
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java344
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java83
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java34
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java29
-rw-r--r--src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java88
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java170
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java179
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingCardView.java264
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java87
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java97
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java174
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java369
-rw-r--r--src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java233
21 files changed, 3770 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
new file mode 100644
index 00000000..38a78f5d
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/ActionPresenterSelector.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+// This class is adapted from Leanback's library, which does not support action icon with one-line
+// label. This class modified its getPresenter method to support the above situation.
+class ActionPresenterSelector extends PresenterSelector {
+ private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
+ private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
+ private final Presenter[] mPresenters = new Presenter[] {
+ mOneLineActionPresenter, mTwoLineActionPresenter};
+
+ @Override
+ public Presenter getPresenter(Object item) {
+ Action action = (Action) item;
+ if (TextUtils.isEmpty(action.getLabel2()) && action.getIcon() == null) {
+ return mOneLineActionPresenter;
+ } else {
+ return mTwoLineActionPresenter;
+ }
+ }
+
+ @Override
+ public Presenter[] getPresenters() {
+ return mPresenters;
+ }
+
+ static class ActionViewHolder extends Presenter.ViewHolder {
+ Action mAction;
+ Button mButton;
+ int mLayoutDirection;
+
+ public ActionViewHolder(View view, int layoutDirection) {
+ super(view);
+ mButton = (Button) view.findViewById(R.id.lb_action_button);
+ mLayoutDirection = layoutDirection;
+ }
+ }
+
+ class OneLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_1_line, parent, false);
+ return new ActionViewHolder(v, parent.getLayoutDirection());
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mAction = action;
+ vh.mButton.setText(action.getLabel1());
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ((ActionViewHolder) viewHolder).mAction = null;
+ }
+ }
+
+ class TwoLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_2_lines, parent, false);
+ return new ActionViewHolder(v, parent.getLayoutDirection());
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ Drawable icon = action.getIcon();
+ vh.mAction = action;
+
+ if (icon != null) {
+ final int startPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_start);
+ final int endPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_end);
+ vh.view.setPaddingRelative(startPadding, 0, endPadding, 0);
+ } else {
+ final int padding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal);
+ vh.view.setPaddingRelative(padding, 0, padding, 0);
+ }
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null);
+
+ CharSequence line1 = action.getLabel1();
+ CharSequence line2 = action.getLabel2();
+ if (TextUtils.isEmpty(line1)) {
+ vh.mButton.setText(line2);
+ } else if (TextUtils.isEmpty(line2)) {
+ vh.mButton.setText(line1);
+ } else {
+ vh.mButton.setText(line1 + "\n" + line2);
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mButton.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, null, null);
+ vh.view.setPadding(0, 0, 0, 0);
+ vh.mAction = null;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
new file mode 100644
index 00000000..c8f6a03f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/CurrentRecordingDetailsFragment.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dialog.HalfSizedDialogFragment;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrStopRecordingFragment;
+import com.android.tv.dvr.ui.DvrUiHelper;
+
+/**
+ * {@link RecordingDetailsFragment} for current recording in DVR.
+ */
+public class CurrentRecordingDetailsFragment extends RecordingDetailsFragment {
+ private static final int ACTION_STOP_RECORDING = 1;
+
+ private DvrDataManager mDvrDataManger;
+ private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener =
+ new DvrDataManager.ScheduledRecordingListener() {
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... schedules) { }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == getRecording().getId()) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (schedule.getId() == getRecording().getId()
+ && schedule.getState()
+ != ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mDvrDataManger = TvApplication.getSingletons(context).getDvrDataManager();
+ mDvrDataManger.addScheduledRecordingListener(mScheduledRecordingListener);
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ adapter.set(ACTION_STOP_RECORDING, new Action(ACTION_STOP_RECORDING,
+ res.getString(R.string.epg_dvr_dialog_message_stop_recording), null,
+ res.getDrawable(R.drawable.lb_ic_stop)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_STOP_RECORDING) {
+ DvrUiHelper.showStopRecordingDialog(getActivity(),
+ getRecording().getChannelId(),
+ DvrStopRecordingFragment.REASON_USER_STOP,
+ new HalfSizedDialogFragment.OnActionClickListener() {
+ @Override
+ public void onActionClick(long actionId) {
+ if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
+ DvrManager dvrManager =
+ TvApplication.getSingletons(getContext())
+ .getDvrManager();
+ dvrManager.stopRecording(getRecording());
+ getActivity().finish();
+ }
+ }
+ });
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onDetach() {
+ if (mDvrDataManger != null) {
+ mDvrDataManger.removeScheduledRecordingListener(mScheduledRecordingListener);
+ }
+ super.onDetach();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
new file mode 100644
index 00000000..b43d1f12
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.media.tv.TvContract;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.Channel;
+
+/**
+ * A class for details content.
+ */
+class DetailsContent {
+ /** Constant for invalid time. */
+ public static final long INVALID_TIME = -1;
+
+ private CharSequence mTitle;
+ private long mStartTimeUtcMillis;
+ private long mEndTimeUtcMillis;
+ private String mDescription;
+ private String mLogoImageUri;
+ private String mBackgroundImageUri;
+
+ private DetailsContent() { }
+
+ /**
+ * Returns title.
+ */
+ public CharSequence getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns start time.
+ */
+ public long getStartTimeUtcMillis() {
+ return mStartTimeUtcMillis;
+ }
+
+ /**
+ * Returns end time.
+ */
+ public long getEndTimeUtcMillis() {
+ return mEndTimeUtcMillis;
+ }
+
+ /**
+ * Returns description.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns Logo image URI as a String.
+ */
+ public String getLogoImageUri() {
+ return mLogoImageUri;
+ }
+
+ /**
+ * Returns background image URI as a String.
+ */
+ public String getBackgroundImageUri() {
+ return mBackgroundImageUri;
+ }
+
+ /**
+ * Copies other details content.
+ */
+ public void copyFrom(DetailsContent other) {
+ if (this == other) {
+ return;
+ }
+ mTitle = other.mTitle;
+ mStartTimeUtcMillis = other.mStartTimeUtcMillis;
+ mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+ mDescription = other.mDescription;
+ mLogoImageUri = other.mLogoImageUri;
+ mBackgroundImageUri = other.mBackgroundImageUri;
+ }
+
+ /**
+ * A class for building details content.
+ */
+ public static final class Builder {
+ private final DetailsContent mDetailsContent;
+
+ public Builder() {
+ mDetailsContent = new DetailsContent();
+ mDetailsContent.mStartTimeUtcMillis = INVALID_TIME;
+ mDetailsContent.mEndTimeUtcMillis = INVALID_TIME;
+ }
+
+ /**
+ * Sets title.
+ */
+ public Builder setTitle(CharSequence title) {
+ mDetailsContent.mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets start time.
+ */
+ public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
+ mDetailsContent.mStartTimeUtcMillis = startTimeUtcMillis;
+ return this;
+ }
+
+ /**
+ * Sets end time.
+ */
+ public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
+ mDetailsContent.mEndTimeUtcMillis = endTimeUtcMillis;
+ return this;
+ }
+
+ /**
+ * Sets description.
+ */
+ public Builder setDescription(String description) {
+ mDetailsContent.mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets logo image URI as a String.
+ */
+ public Builder setLogoImageUri(String logoImageUri) {
+ mDetailsContent.mLogoImageUri = logoImageUri;
+ return this;
+ }
+
+ /**
+ * Sets background image URI as a String.
+ */
+ public Builder setBackgroundImageUri(String backgroundImageUri) {
+ mDetailsContent.mBackgroundImageUri = backgroundImageUri;
+ return this;
+ }
+
+ /**
+ * Sets background image and logo image URI from program and channel.
+ */
+ public Builder setImageUris(@Nullable BaseProgram program, @Nullable Channel channel) {
+ if (program != null) {
+ return setImageUris(program.getPosterArtUri(), program.getThumbnailUri(), channel);
+ } else {
+ return setImageUris(null, null, channel);
+ }
+ }
+
+ /**
+ * Sets background image and logo image URI and channel is used for fallback images.
+ */
+ public Builder setImageUris(@Nullable String posterArtUri,
+ @Nullable String thumbnailUri, @Nullable Channel channel) {
+ mDetailsContent.mLogoImageUri = null;
+ mDetailsContent.mBackgroundImageUri = null;
+ if (!TextUtils.isEmpty(posterArtUri) && !TextUtils.isEmpty(thumbnailUri)) {
+ mDetailsContent.mLogoImageUri = posterArtUri;
+ mDetailsContent.mBackgroundImageUri = thumbnailUri;
+ } else if (!TextUtils.isEmpty(posterArtUri)) {
+ // thumbnailUri is empty
+ mDetailsContent.mLogoImageUri = posterArtUri;
+ mDetailsContent.mBackgroundImageUri = posterArtUri;
+ } else if (!TextUtils.isEmpty(thumbnailUri)) {
+ // posterArtUri is empty
+ mDetailsContent.mLogoImageUri = thumbnailUri;
+ mDetailsContent.mBackgroundImageUri = thumbnailUri;
+ }
+ if (TextUtils.isEmpty(mDetailsContent.mLogoImageUri) && channel != null) {
+ String channelLogoUri = TvContract.buildChannelLogoUri(channel.getId())
+ .toString();
+ mDetailsContent.mLogoImageUri = channelLogoUri;
+ mDetailsContent.mBackgroundImageUri = channelLogoUri;
+ }
+ return this;
+ }
+
+ /**
+ * Builds details content.
+ */
+ public DetailsContent build() {
+ DetailsContent detailsContent = new DetailsContent();
+ detailsContent.copyFrom(mDetailsContent);
+ return detailsContent;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
new file mode 100644
index 00000000..a2e3fe16
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContentPresenter.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.support.v17.leanback.widget.Presenter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.ui.ViewUtils;
+import com.android.tv.util.Utils;
+
+/**
+ * An {@link Presenter} for rendering a detailed description of an DVR item.
+ * Typically this Presenter will be used in a
+ * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}.
+ * Most codes of this class is originated from
+ * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
+ * The latter class are re-used to provide a customized version of
+ * {@link android.support.v17.leanback.widget.DetailsOverviewRow}.
+ */
+class DetailsContentPresenter extends Presenter {
+ /**
+ * The ViewHolder for the {@link DetailsContentPresenter}.
+ */
+ public static class ViewHolder extends Presenter.ViewHolder {
+ final TextView mTitle;
+ final TextView mSubtitle;
+ final LinearLayout mDescriptionContainer;
+ final TextView mBody;
+ final TextView mReadMoreView;
+ final int mTitleMargin;
+ final int mUnderTitleBaselineMargin;
+ final int mUnderSubtitleBaselineMargin;
+ final int mTitleLineSpacing;
+ final int mBodyLineSpacing;
+ final int mBodyMaxLines;
+ final int mBodyMinLines;
+ final FontMetricsInt mTitleFontMetricsInt;
+ final FontMetricsInt mSubtitleFontMetricsInt;
+ final FontMetricsInt mBodyFontMetricsInt;
+ final int mTitleMaxLines;
+
+ private Activity mActivity;
+ private boolean mFullTextMode;
+ private int mFullTextAnimationDuration;
+ private boolean mIsListeningToPreDraw;
+
+ private ViewTreeObserver.OnPreDrawListener mPreDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (mSubtitle.getVisibility() == View.VISIBLE
+ && mSubtitle.getTop() > view.getHeight()
+ && mTitle.getLineCount() > 1) {
+ mTitle.setMaxLines(mTitle.getLineCount() - 1);
+ return false;
+ }
+ final int bodyLines = mBody.getLineCount();
+ final int maxLines = mFullTextMode ? bodyLines :
+ (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines);
+ if (bodyLines > maxLines) {
+ mReadMoreView.setVisibility(View.VISIBLE);
+ mDescriptionContainer.setFocusable(true);
+ mDescriptionContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mFullTextMode = true;
+ mReadMoreView.setVisibility(View.GONE);
+ mDescriptionContainer.setFocusable(false);
+ mDescriptionContainer.setOnClickListener(null);
+ mBody.setMaxLines(bodyLines);
+ // Minus 1 from line difference to eliminate the space
+ // originally occupied by "READ MORE"
+ showFullText((bodyLines - maxLines - 1) * mBodyLineSpacing);
+ }
+ });
+ }
+ if (mBody.getMaxLines() != maxLines) {
+ mBody.setMaxLines(maxLines);
+ return false;
+ } else {
+ removePreDrawListener();
+ return true;
+ }
+ }
+ };
+
+ public ViewHolder(final View view) {
+ super(view);
+ view.addOnAttachStateChangeListener(
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ // In case predraw listener was removed in detach, make sure
+ // we have the proper layout.
+ addPreDrawListener();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ removePreDrawListener();
+ }
+ });
+ mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
+ mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
+ mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
+ mDescriptionContainer =
+ (LinearLayout) view.findViewById(R.id.dvr_details_description_container);
+ mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more);
+
+ FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle);
+ final int titleAscent = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_baseline);
+ // Ascent is negative
+ mTitleMargin = titleAscent + titleFontMetricsInt.ascent;
+
+ mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_title_baseline_margin);
+ mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_subtitle_baseline_margin);
+
+ mTitleLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_line_spacing);
+ mBodyLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_body_line_spacing);
+
+ mBodyMaxLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_max_lines);
+ mBodyMinLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_min_lines);
+ mTitleMaxLines = mTitle.getMaxLines();
+
+ mTitleFontMetricsInt = getFontMetricsInt(mTitle);
+ mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle);
+ mBodyFontMetricsInt = getFontMetricsInt(mBody);
+ }
+
+ void addPreDrawListener() {
+ if (!mIsListeningToPreDraw) {
+ mIsListeningToPreDraw = true;
+ view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
+ }
+ }
+
+ void removePreDrawListener() {
+ if (mIsListeningToPreDraw) {
+ view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
+ mIsListeningToPreDraw = false;
+ }
+ }
+
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ public TextView getSubtitle() {
+ return mSubtitle;
+ }
+
+ public TextView getBody() {
+ return mBody;
+ }
+
+ private FontMetricsInt getFontMetricsInt(TextView textView) {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTextSize(textView.getTextSize());
+ paint.setTypeface(textView.getTypeface());
+ return paint.getFontMetricsInt();
+ }
+
+ private void showFullText(int heightDiff) {
+ final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame);
+ int nowHeight = ViewUtils.getLayoutHeight(detailsFrame);
+ Animator expandAnimator = ViewUtils.createHeightAnimator(
+ detailsFrame, nowHeight, nowHeight + heightDiff);
+ expandAnimator.setDuration(mFullTextAnimationDuration);
+ Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+ 0f, -(heightDiff / 2)));
+ shiftAnimator.setDuration(mFullTextAnimationDuration);
+ AnimatorSet fullTextAnimator = new AnimatorSet();
+ fullTextAnimator.playTogether(expandAnimator, shiftAnimator);
+ fullTextAnimator.start();
+ }
+ }
+
+ private final Activity mActivity;
+ private final int mFullTextAnimationDuration;
+
+ public DetailsContentPresenter(Activity activity) {
+ super();
+ mActivity = activity;
+ mFullTextAnimationDuration = mActivity.getResources()
+ .getInteger(R.integer.dvr_details_full_text_animation_duration);
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.dvr_details_description, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ final ViewHolder vh = (ViewHolder) viewHolder;
+ final DetailsContent detailsContent = (DetailsContent) item;
+
+ vh.mActivity = mActivity;
+ vh.mFullTextAnimationDuration = mFullTextAnimationDuration;
+
+ boolean hasTitle = true;
+ if (TextUtils.isEmpty(detailsContent.getTitle())) {
+ vh.mTitle.setVisibility(View.GONE);
+ hasTitle = false;
+ } else {
+ vh.mTitle.setText(detailsContent.getTitle());
+ vh.mTitle.setVisibility(View.VISIBLE);
+ vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight()
+ + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier());
+ vh.mTitle.setMaxLines(vh.mTitleMaxLines);
+ }
+ setTopMargin(vh.mTitle, vh.mTitleMargin);
+
+ boolean hasSubtitle = true;
+ if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME
+ && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) {
+ vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(),
+ detailsContent.getStartTimeUtcMillis(),
+ detailsContent.getEndTimeUtcMillis(), false));
+ vh.mSubtitle.setVisibility(View.VISIBLE);
+ if (hasTitle) {
+ setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin
+ + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent);
+ } else {
+ setTopMargin(vh.mSubtitle, 0);
+ }
+ } else {
+ vh.mSubtitle.setVisibility(View.GONE);
+ hasSubtitle = false;
+ }
+
+ if (TextUtils.isEmpty(detailsContent.getDescription())) {
+ vh.mBody.setVisibility(View.GONE);
+ } else {
+ vh.mBody.setText(detailsContent.getDescription());
+ vh.mBody.setVisibility(View.VISIBLE);
+ vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight()
+ + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier());
+ if (hasSubtitle) {
+ setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin
+ + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent
+ - vh.mBody.getPaddingTop());
+ } else if (hasTitle) {
+ setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin
+ + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent
+ - vh.mBody.getPaddingTop());
+ } else {
+ setTopMargin(vh.mDescriptionContainer, 0);
+ }
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { }
+
+ private void setTopMargin(View view, int topMargin) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
+ lp.topMargin = topMargin;
+ view.setLayoutParams(lp);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
new file mode 100644
index 00000000..82fe9ce3
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DetailsViewBackgroundHelper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.v17.leanback.app.BackgroundManager;
+
+/**
+ * The Background Helper.
+ */
+class DetailsViewBackgroundHelper {
+ // Background delay serves to avoid kicking off expensive bitmap loading
+ // in case multiple backgrounds are set in quick succession.
+ private static final int SET_BACKGROUND_DELAY_MS = 100;
+
+ private final BackgroundManager mBackgroundManager;
+
+ class LoadBackgroundRunnable implements Runnable {
+ final Drawable mBackGround;
+
+ LoadBackgroundRunnable(Drawable background) {
+ mBackGround = background;
+ }
+
+ @Override
+ public void run() {
+ if (!mBackgroundManager.isAttached()) {
+ return;
+ }
+ if (mBackGround instanceof BitmapDrawable) {
+ mBackgroundManager.setBitmap(((BitmapDrawable) mBackGround).getBitmap());
+ }
+ mRunnable = null;
+ }
+ }
+
+ private LoadBackgroundRunnable mRunnable;
+
+ private final Handler mHandler = new Handler();
+
+ public DetailsViewBackgroundHelper(Activity activity) {
+ mBackgroundManager = BackgroundManager.getInstance(activity);
+ mBackgroundManager.attach(activity.getWindow());
+ }
+
+ /**
+ * Sets the given image to background.
+ */
+ public void setBackground(Drawable background) {
+ if (mRunnable != null) {
+ mHandler.removeCallbacks(mRunnable);
+ }
+ mRunnable = new LoadBackgroundRunnable(background);
+ mHandler.postDelayed(mRunnable, SET_BACKGROUND_DELAY_MS);
+ }
+
+ /**
+ * Sets the background color.
+ */
+ public void setBackgroundColor(int color) {
+ if (mBackgroundManager.isAttached()) {
+ mBackgroundManager.setColor(color);
+ }
+ }
+
+ /**
+ * Sets the background scrim.
+ */
+ public void setScrim(int color) {
+ if (mBackgroundManager.isAttached()) {
+ mBackgroundManager.setDimLayer(new ColorDrawable(color));
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
new file mode 100644
index 00000000..2b3dcb25
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseActivity.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+
+/**
+ * {@link android.app.Activity} for DVR UI.
+ */
+public class DvrBrowseActivity extends Activity {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dvr_main);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
new file mode 100644
index 00000000..803d1017
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -0,0 +1,634 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v17.leanback.app.BrowseFragment;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.TitleViewAdapter;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.GenreItems;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrDataManager.OnDvrScheduleLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
+import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrDataManager.SeriesRecordingListener;
+import com.android.tv.dvr.DvrScheduleManager;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * {@link BrowseFragment} for DVR functions.
+ */
+public class DvrBrowseFragment extends BrowseFragment implements
+ RecordedProgramListener, ScheduledRecordingListener, SeriesRecordingListener,
+ OnDvrScheduleLoadFinishedListener, OnRecordedProgramLoadFinishedListener {
+ private static final String TAG = "DvrBrowseFragment";
+ private static final boolean DEBUG = false;
+
+ private static final int MAX_RECENT_ITEM_COUNT = 10;
+ private static final int MAX_SCHEDULED_ITEM_COUNT = 4;
+
+ private RecordedProgramAdapter mRecentAdapter;
+ private ScheduleAdapter mScheduleAdapter;
+ private SeriesAdapter mSeriesAdapter;
+ private RecordedProgramAdapter[] mGenreAdapters =
+ new RecordedProgramAdapter[GenreItems.getGenreCount() + 1];
+ private ListRow mRecentRow;
+ private ListRow mSeriesRow;
+ private ListRow[] mGenreRows = new ListRow[GenreItems.getGenreCount() + 1];
+ private List<String> mGenreLabels;
+ private DvrDataManager mDvrDataManager;
+ private DvrScheduleManager mDvrScheudleManager;
+ private ArrayObjectAdapter mRowsAdapter;
+ private ClassPresenterSelector mPresenterSelector;
+ private final HashMap<String, RecordedProgram> mSeriesId2LatestProgram = new HashMap<>();
+ private final Handler mHandler = new Handler();
+ private final OnGlobalFocusChangeListener mOnGlobalFocusChangeListener =
+ new OnGlobalFocusChangeListener() {
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (oldFocus instanceof RecordingCardView) {
+ ((RecordingCardView) oldFocus).expandTitle(false, true);
+ }
+ if (newFocus instanceof RecordingCardView) {
+ // If the header transition is ongoing, expand cards immediately without
+ // animation to make a smooth transition.
+ ((RecordingCardView) newFocus).expandTitle(true, !isInHeadersTransition());
+ }
+ }
+ };
+
+ private final Comparator<Object> RECORDED_PROGRAM_COMPARATOR = new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ if (lhs instanceof SeriesRecording) {
+ lhs = mSeriesId2LatestProgram.get(((SeriesRecording) lhs).getSeriesId());
+ }
+ if (rhs instanceof SeriesRecording) {
+ rhs = mSeriesId2LatestProgram.get(((SeriesRecording) rhs).getSeriesId());
+ }
+ if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR.reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+ } else {
+ return -1;
+ }
+ } else if (rhs instanceof RecordedProgram) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ private final Comparator<Object> SCHEDULE_COMPARATOR = new Comparator<Object>() {
+ @Override
+ public int compare(Object lhs, Object rhs) {
+ if (lhs instanceof ScheduledRecording) {
+ if (rhs instanceof ScheduledRecording) {
+ return ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+ } else {
+ return -1;
+ }
+ } else if (rhs instanceof ScheduledRecording) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ };
+
+ private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener =
+ new DvrScheduleManager.OnConflictStateChangeListener() {
+ @Override
+ public void onConflictStateChange(boolean conflict, ScheduledRecording... schedules) {
+ if (mScheduleAdapter != null) {
+ for (ScheduledRecording schedule : schedules) {
+ onScheduledRecordingConflictStatusChanged(schedule);
+ }
+ }
+ }
+ };
+
+ private final Runnable mUpdateRowsRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateRows();
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ Context context = getContext();
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrScheudleManager = singletons.getDvrScheduleManager();
+ mPresenterSelector = new ClassPresenterSelector()
+ .addClassPresenter(ScheduledRecording.class,
+ new ScheduledRecordingPresenter(context))
+ .addClassPresenter(RecordedProgram.class, new RecordedProgramPresenter(context))
+ .addClassPresenter(SeriesRecording.class, new SeriesRecordingPresenter(context))
+ .addClassPresenter(FullScheduleCardHolder.class,
+ new FullSchedulesCardPresenter(context));
+ mGenreLabels = new ArrayList<>(Arrays.asList(GenreItems.getLabels(context)));
+ mGenreLabels.add(getString(R.string.dvr_main_others));
+ prepareUiElements();
+ if (!startBrowseIfDvrInitialized()) {
+ if (!mDvrDataManager.isDvrScheduleLoadFinished()) {
+ mDvrDataManager.addDvrScheduleLoadFinishedListener(this);
+ }
+ if (!mDvrDataManager.isRecordedProgramLoadFinished()) {
+ mDvrDataManager.addRecordedProgramLoadFinishedListener(this);
+ }
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ view.getViewTreeObserver().addOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ }
+
+ @Override
+ public void onDestroyView() {
+ getView().getViewTreeObserver()
+ .removeOnGlobalFocusChangeListener(mOnGlobalFocusChangeListener);
+ super.onDestroyView();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ mHandler.removeCallbacks(mUpdateRowsRunnable);
+ mDvrScheudleManager.removeOnConflictStateChangeListener(mOnConflictStateChangeListener);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrDataManager.removeSeriesRecordingListener(this);
+ mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ mRowsAdapter.clear();
+ mSeriesId2LatestProgram.clear();
+ for (Presenter presenter : mPresenterSelector.getPresenters()) {
+ if (presenter instanceof DvrItemPresenter) {
+ ((DvrItemPresenter) presenter).unbindAllViewHolders();
+ }
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onDvrScheduleLoadFinished() {
+ startBrowseIfDvrInitialized();
+ mDvrDataManager.removeDvrScheduleLoadFinishedListener(this);
+ }
+
+ @Override
+ public void onRecordedProgramLoadFinished() {
+ startBrowseIfDvrInitialized();
+ mDvrDataManager.removeRecordedProgramLoadFinishedListener(this);
+ }
+
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ handleRecordedProgramAdded(recordedProgram, true);
+ }
+ postUpdateRows();
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ handleRecordedProgramChanged(recordedProgram);
+ }
+ postUpdateRows();
+ }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ handleRecordedProgramRemoved(recordedProgram);
+ }
+ postUpdateRows();
+ }
+
+ // No need to call updateRows() during ScheduledRecordings' change because
+ // the row for ScheduledRecordings is always displayed.
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ if (needToShowScheduledRecording(scheduleRecording)) {
+ mScheduleAdapter.add(scheduleRecording);
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ mScheduleAdapter.remove(scheduleRecording);
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduleRecording : scheduledRecordings) {
+ if (needToShowScheduledRecording(scheduleRecording)) {
+ mScheduleAdapter.change(scheduleRecording);
+ } else {
+ mScheduleAdapter.removeWithId(scheduleRecording);
+ }
+ }
+ }
+
+ private void onScheduledRecordingConflictStatusChanged(ScheduledRecording... schedules) {
+ for (ScheduledRecording schedule : schedules) {
+ if (needToShowScheduledRecording(schedule)) {
+ if (mScheduleAdapter.contains(schedule)) {
+ mScheduleAdapter.change(schedule);
+ }
+ } else {
+ mScheduleAdapter.removeWithId(schedule);
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsAdded(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsRemoved(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ handleSeriesRecordingsChanged(Arrays.asList(seriesRecordings));
+ postUpdateRows();
+ }
+
+ // Workaround of b/29108300
+ @Override
+ public void showTitle(int flags) {
+ flags &= ~TitleViewAdapter.SEARCH_VIEW_VISIBLE;
+ super.showTitle(flags);
+ }
+
+ private void prepareUiElements() {
+ setBadgeDrawable(getActivity().getDrawable(R.drawable.ic_dvr_badge));
+ setHeadersState(HEADERS_ENABLED);
+ setHeadersTransitionOnBackEnabled(false);
+ setBrandColor(getResources().getColor(R.color.program_guide_side_panel_background, null));
+ mRowsAdapter = new ArrayObjectAdapter(new DvrListRowPresenter(getContext()));
+ setAdapter(mRowsAdapter);
+ prepareEntranceTransition();
+ }
+
+ private boolean startBrowseIfDvrInitialized() {
+ if (mDvrDataManager.isInitialized()) {
+ // Setup rows
+ mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
+ mScheduleAdapter = new ScheduleAdapter(MAX_SCHEDULED_ITEM_COUNT);
+ mSeriesAdapter = new SeriesAdapter();
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ mGenreAdapters[i] = new RecordedProgramAdapter();
+ }
+ // Schedule Recordings.
+ List<ScheduledRecording> schedules = mDvrDataManager.getAllScheduledRecordings();
+ onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
+ mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
+ // Recorded Programs.
+ for (RecordedProgram recordedProgram : mDvrDataManager.getRecordedPrograms()) {
+ handleRecordedProgramAdded(recordedProgram, false);
+ }
+ // Series Recordings. Series recordings should be added after recorded programs, because
+ // we build series recordings' latest program information while adding recorded programs.
+ List<SeriesRecording> recordings = mDvrDataManager.getSeriesRecordings();
+ handleSeriesRecordingsAdded(recordings);
+ mRecentRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_recent)), mRecentAdapter);
+ mRowsAdapter.add(new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_scheduled)), mScheduleAdapter));
+ mSeriesRow = new ListRow(new HeaderItem(
+ getString(R.string.dvr_main_series)), mSeriesAdapter);
+ updateRows();
+ // Initialize listeners
+ mDvrDataManager.addRecordedProgramListener(this);
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mDvrScheudleManager.addOnConflictStateChangeListener(mOnConflictStateChangeListener);
+ startEntranceTransition();
+ return true;
+ }
+ return false;
+ }
+
+ private void handleRecordedProgramAdded(RecordedProgram recordedProgram,
+ boolean updateSeriesRecording) {
+ mRecentAdapter.add(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ SeriesRecording seriesRecording = null;
+ if (seriesId != null) {
+ seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
+ if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .compare(latestProgram, recordedProgram) < 0) {
+ mSeriesId2LatestProgram.put(seriesId, recordedProgram);
+ if (updateSeriesRecording && seriesRecording != null) {
+ onSeriesRecordingChanged(seriesRecording);
+ }
+ }
+ }
+ if (seriesRecording == null) {
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(recordedProgram.getCanonicalGenres())) {
+ adapter.add(recordedProgram);
+ }
+ }
+ }
+
+ private void handleRecordedProgramRemoved(RecordedProgram recordedProgram) {
+ mRecentAdapter.remove(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ if (seriesId != null) {
+ SeriesRecording seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram =
+ mSeriesId2LatestProgram.get(recordedProgram.getSeriesId());
+ if (latestProgram != null && latestProgram.getId() == recordedProgram.getId()) {
+ if (seriesRecording != null) {
+ updateLatestRecordedProgram(seriesRecording);
+ onSeriesRecordingChanged(seriesRecording);
+ }
+ }
+ }
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(recordedProgram.getCanonicalGenres())) {
+ adapter.remove(recordedProgram);
+ }
+ }
+
+ private void handleRecordedProgramChanged(RecordedProgram recordedProgram) {
+ mRecentAdapter.change(recordedProgram);
+ String seriesId = recordedProgram.getSeriesId();
+ SeriesRecording seriesRecording = null;
+ if (seriesId != null) {
+ seriesRecording = mDvrDataManager.getSeriesRecording(seriesId);
+ RecordedProgram latestProgram = mSeriesId2LatestProgram.get(seriesId);
+ if (latestProgram == null || RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .compare(latestProgram, recordedProgram) <= 0) {
+ mSeriesId2LatestProgram.put(seriesId, recordedProgram);
+ if (seriesRecording != null) {
+ onSeriesRecordingChanged(seriesRecording);
+ }
+ } else if (latestProgram.getId() == recordedProgram.getId()) {
+ if (seriesRecording != null) {
+ updateLatestRecordedProgram(seriesRecording);
+ onSeriesRecordingChanged(seriesRecording);
+ }
+ }
+ }
+ if (seriesRecording == null) {
+ updateGenreAdapters(getGenreAdapters(
+ recordedProgram.getCanonicalGenres()), recordedProgram);
+ } else {
+ updateGenreAdapters(new ArrayList<>(), recordedProgram);
+ }
+ }
+
+ private void handleSeriesRecordingsAdded(List<SeriesRecording> seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ mSeriesAdapter.add(seriesRecording);
+ if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
+ adapter.add(seriesRecording);
+ }
+ }
+ }
+ }
+
+ private void handleSeriesRecordingsRemoved(List<SeriesRecording> seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ mSeriesAdapter.remove(seriesRecording);
+ for (RecordedProgramAdapter adapter
+ : getGenreAdapters(seriesRecording.getCanonicalGenreIds())) {
+ adapter.remove(seriesRecording);
+ }
+ }
+ }
+
+ private void handleSeriesRecordingsChanged(List<SeriesRecording> seriesRecordings) {
+ for (SeriesRecording seriesRecording : seriesRecordings) {
+ mSeriesAdapter.change(seriesRecording);
+ if (mSeriesId2LatestProgram.get(seriesRecording.getSeriesId()) != null) {
+ updateGenreAdapters(getGenreAdapters(
+ seriesRecording.getCanonicalGenreIds()), seriesRecording);
+ } else {
+ // Remove series recording from all genre rows if it has no recorded program
+ updateGenreAdapters(new ArrayList<>(), seriesRecording);
+ }
+ }
+ }
+
+ private List<RecordedProgramAdapter> getGenreAdapters(String[] genres) {
+ List<RecordedProgramAdapter> result = new ArrayList<>();
+ if (genres == null || genres.length == 0) {
+ result.add(mGenreAdapters[mGenreAdapters.length - 1]);
+ } else {
+ for (String genre : genres) {
+ int genreId = GenreItems.getId(genre);
+ if(genreId >= mGenreAdapters.length) {
+ Log.d(TAG, "Wrong Genre ID: " + genreId);
+ } else {
+ result.add(mGenreAdapters[genreId]);
+ }
+ }
+ }
+ return result;
+ }
+
+ private List<RecordedProgramAdapter> getGenreAdapters(int[] genreIds) {
+ List<RecordedProgramAdapter> result = new ArrayList<>();
+ if (genreIds == null || genreIds.length == 0) {
+ result.add(mGenreAdapters[mGenreAdapters.length - 1]);
+ } else {
+ for (int genreId : genreIds) {
+ if(genreId >= mGenreAdapters.length) {
+ Log.d(TAG, "Wrong Genre ID: " + genreId);
+ } else {
+ result.add(mGenreAdapters[genreId]);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void updateGenreAdapters(List<RecordedProgramAdapter> adapters, Object r) {
+ for (RecordedProgramAdapter adapter : mGenreAdapters) {
+ if (adapters.contains(adapter)) {
+ adapter.change(r);
+ } else {
+ adapter.remove(r);
+ }
+ }
+ }
+
+ private void postUpdateRows() {
+ mHandler.removeCallbacks(mUpdateRowsRunnable);
+ mHandler.post(mUpdateRowsRunnable);
+ }
+
+ private void updateRows() {
+ int visibleRowsCount = 1; // Schedule's Row will never be empty
+ if (mRecentAdapter.isEmpty()) {
+ mRowsAdapter.remove(mRecentRow);
+ } else {
+ if (mRowsAdapter.indexOf(mRecentRow) < 0) {
+ mRowsAdapter.add(0, mRecentRow);
+ }
+ visibleRowsCount++;
+ }
+ if (mSeriesAdapter.isEmpty()) {
+ mRowsAdapter.remove(mSeriesRow);
+ } else {
+ if (mRowsAdapter.indexOf(mSeriesRow) < 0) {
+ mRowsAdapter.add(visibleRowsCount, mSeriesRow);
+ }
+ visibleRowsCount++;
+ }
+ for (int i = 0; i < mGenreAdapters.length; i++) {
+ RecordedProgramAdapter adapter = mGenreAdapters[i];
+ if (adapter != null) {
+ if (adapter.isEmpty()) {
+ mRowsAdapter.remove(mGenreRows[i]);
+ } else {
+ if (mGenreRows[i] == null || mRowsAdapter.indexOf(mGenreRows[i]) < 0) {
+ mGenreRows[i] = new ListRow(new HeaderItem(mGenreLabels.get(i)), adapter);
+ mRowsAdapter.add(visibleRowsCount, mGenreRows[i]);
+ }
+ visibleRowsCount++;
+ }
+ }
+ }
+ }
+
+ private boolean needToShowScheduledRecording(ScheduledRecording recording) {
+ int state = recording.getState();
+ return state == ScheduledRecording.STATE_RECORDING_IN_PROGRESS
+ || state == ScheduledRecording.STATE_RECORDING_NOT_STARTED;
+ }
+
+ private void updateLatestRecordedProgram(SeriesRecording seriesRecording) {
+ RecordedProgram latestProgram = null;
+ for (RecordedProgram program :
+ mDvrDataManager.getRecordedPrograms(seriesRecording.getId())) {
+ if (latestProgram == null || RecordedProgram
+ .START_TIME_THEN_ID_COMPARATOR.compare(latestProgram, program) < 0) {
+ latestProgram = program;
+ }
+ }
+ mSeriesId2LatestProgram.put(seriesRecording.getSeriesId(), latestProgram);
+ }
+
+ private class ScheduleAdapter extends SortedArrayAdapter<Object> {
+ ScheduleAdapter(int maxItemCount) {
+ super(mPresenterSelector, SCHEDULE_COMPARATOR, maxItemCount);
+ }
+
+ @Override
+ public long getId(Object item) {
+ if (item instanceof ScheduledRecording) {
+ return ((ScheduledRecording) item).getId();
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ private class SeriesAdapter extends SortedArrayAdapter<SeriesRecording> {
+ SeriesAdapter() {
+ super(mPresenterSelector, new Comparator<SeriesRecording>() {
+ @Override
+ public int compare(SeriesRecording lhs, SeriesRecording rhs) {
+ if (lhs.isStopped() && !rhs.isStopped()) {
+ return 1;
+ } else if (!lhs.isStopped() && rhs.isStopped()) {
+ return -1;
+ }
+ return SeriesRecording.PRIORITY_COMPARATOR.compare(lhs, rhs);
+ }
+ });
+ }
+
+ @Override
+ public long getId(SeriesRecording item) {
+ return item.getId();
+ }
+ }
+
+ private class RecordedProgramAdapter extends SortedArrayAdapter<Object> {
+ RecordedProgramAdapter() {
+ this(Integer.MAX_VALUE);
+ }
+
+ RecordedProgramAdapter(int maxItemCount) {
+ super(mPresenterSelector, RECORDED_PROGRAM_COMPARATOR, maxItemCount);
+ }
+
+ @Override
+ public long getId(Object item) {
+ // We takes the inverse number for the ID of recorded programs to make the ID stable.
+ if (item instanceof SeriesRecording) {
+ return ((SeriesRecording) item).getId();
+ } else if (item instanceof RecordedProgram) {
+ return -((RecordedProgram) item).getId() - 1;
+ } else {
+ return -1;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
new file mode 100644
index 00000000..30c81e83
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+
+/**
+ * Activity to show details view in DVR.
+ */
+public class DvrDetailsActivity extends Activity {
+ /**
+ * Name of record id added to the Intent.
+ */
+ public static final String RECORDING_ID = "record_id";
+
+ /**
+ * Name of flag added to the Intent to determine if details view should hide "View schedule"
+ * button.
+ */
+ public static final String HIDE_VIEW_SCHEDULE = "hide_view_schedule";
+
+ /**
+ * Name of details view's type added to the intent.
+ */
+ public static final String DETAILS_VIEW_TYPE = "details_view_type";
+
+ /**
+ * Name of shared element between activities.
+ */
+ public static final String SHARED_ELEMENT_NAME = "shared_element";
+
+ /**
+ * CURRENT_RECORDING_VIEW refers to Current Recordings in DVR.
+ */
+ public static final int CURRENT_RECORDING_VIEW = 1;
+
+ /**
+ * SCHEDULED_RECORDING_VIEW refers to Scheduled Recordings in DVR.
+ */
+ public static final int SCHEDULED_RECORDING_VIEW = 2;
+
+ /**
+ * RECORDED_PROGRAM_VIEW refers to Recorded programs in DVR.
+ */
+ public static final int RECORDED_PROGRAM_VIEW = 3;
+
+ /**
+ * SERIES_RECORDING_VIEW refers to series recording in DVR.
+ */
+ public static final int SERIES_RECORDING_VIEW = 4;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ TvApplication.setCurrentRunningProcess(this, true);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dvr_details);
+ long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
+ int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
+ boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+ if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) {
+ Bundle args = new Bundle();
+ args.putLong(RECORDING_ID, recordId);
+ DetailsFragment detailsFragment = null;
+ if (detailsViewType == CURRENT_RECORDING_VIEW) {
+ detailsFragment = new CurrentRecordingDetailsFragment();
+ } else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
+ args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
+ detailsFragment = new ScheduledRecordingDetailsFragment();
+ } else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
+ detailsFragment = new RecordedProgramDetailsFragment();
+ } else if (detailsViewType == SERIES_RECORDING_VIEW) {
+ detailsFragment = new SeriesRecordingDetailsFragment();
+ }
+ detailsFragment.setArguments(args);
+ getFragmentManager().beginTransaction()
+ .replace(R.id.dvr_details_view_frame, detailsFragment).commit();
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
new file mode 100644
index 00000000..4d3698ef
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsFragment.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvInputManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dialog.PinDialogFragment;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
+import com.android.tv.parental.ParentalControlSettings;
+import com.android.tv.util.ImageLoader;
+import com.android.tv.util.ToastUtils;
+import com.android.tv.util.Utils;
+
+import java.io.File;
+
+abstract class DvrDetailsFragment extends DetailsFragment {
+ private static final int LOAD_LOGO_IMAGE = 1;
+ private static final int LOAD_BACKGROUND_IMAGE = 2;
+
+ protected DetailsViewBackgroundHelper mBackgroundHelper;
+ private ArrayObjectAdapter mRowsAdapter;
+ private DetailsOverviewRow mDetailsOverview;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!onLoadRecordingDetails(getArguments())) {
+ getActivity().finish();
+ return;
+ }
+ mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
+ setupAdapter();
+ onCreateInternal();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ // TODO: remove the workaround of b/30401180.
+ VerticalGridView container = (VerticalGridView) getActivity()
+ .findViewById(R.id.container_list);
+ // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout.
+ container.setItemAlignmentOffset(0);
+ container.setWindowAlignmentOffset(
+ getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top));
+ }
+
+ private void setupAdapter() {
+ DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter(
+ new DetailsContentPresenter(getActivity()));
+ rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background,
+ null));
+ rowPresenter.setSharedElementEnterTransition(getActivity(),
+ DvrDetailsActivity.SHARED_ELEMENT_NAME);
+ rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
+ mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
+ setAdapter(mRowsAdapter);
+ }
+
+ /**
+ * Returns details views' rows adapter.
+ */
+ protected ArrayObjectAdapter getRowsAdapter() {
+ return mRowsAdapter;
+ }
+
+ /**
+ * Sets details overview.
+ */
+ protected void setDetailsOverviewRow(DetailsContent detailsContent) {
+ mDetailsOverview = new DetailsOverviewRow(detailsContent);
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ mRowsAdapter.add(mDetailsOverview);
+ onLoadLogoAndBackgroundImages(detailsContent);
+ }
+
+ /**
+ * Creates and returns presenter selector will be used by rows adaptor.
+ */
+ protected PresenterSelector onCreatePresenterSelector(
+ DetailsOverviewRowPresenter rowPresenter) {
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+ return presenterSelector;
+ }
+
+ /**
+ * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish
+ * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not
+ * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to
+ * do after the super class did onCreate, it should override this method and put the codes here.
+ */
+ protected void onCreateInternal() { }
+
+ /**
+ * Updates actions of details overview.
+ */
+ protected void updateActions() {
+ mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
+ }
+
+ /**
+ * Loads recording details according to the arguments the fragment got.
+ *
+ * @return false if cannot find valid recordings, else return true. If the return value
+ * is false, the detail activity and fragment will be ended.
+ */
+ abstract boolean onLoadRecordingDetails(Bundle args);
+
+ /**
+ * Creates actions users can interact with and their adaptor for this fragment.
+ */
+ abstract SparseArrayObjectAdapter onCreateActionsAdapter();
+
+ /**
+ * Creates actions listeners to implement the behavior of the fragment after users click some
+ * action buttons.
+ */
+ abstract OnActionClickedListener onCreateOnActionClickedListener();
+
+ /**
+ * Returns program title with episode number. If the program is null, returns channel name.
+ */
+ protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) {
+ String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext());
+ SpannableString title = titleWithEpisodeNumber == null ? null
+ : new SpannableString(titleWithEpisodeNumber);
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : getContext().getResources().getString(
+ R.string.no_program_information));
+ } else {
+ String programTitle = program.getTitle();
+ title.setSpan(new TextAppearanceSpan(getContext(),
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ return title;
+ }
+
+ /**
+ * Loads logo and background images for detail fragments.
+ */
+ protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) {
+ Drawable logoDrawable = null;
+ Drawable backgroundDrawable = null;
+ if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) {
+ logoDrawable = getContext().getResources()
+ .getDrawable(R.drawable.dvr_default_poster, null);
+ mDetailsOverview.setImageDrawable(logoDrawable);
+ }
+ if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) {
+ backgroundDrawable = getContext().getResources()
+ .getDrawable(R.drawable.dvr_default_poster, null);
+ mBackgroundHelper.setBackground(backgroundDrawable);
+ }
+ if (logoDrawable != null && backgroundDrawable != null) {
+ return;
+ }
+ if (logoDrawable == null && backgroundDrawable == null
+ && detailsContent.getLogoImageUri().equals(
+ detailsContent.getBackgroundImageUri())) {
+ ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(),
+ new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE,
+ getContext()));
+ return;
+ }
+ if (logoDrawable == null) {
+ int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width);
+ int imageHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_details_poster_height);
+ ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(),
+ imageWidth, imageHeight,
+ new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext()));
+ }
+ if (backgroundDrawable == null) {
+ ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(),
+ new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext()));
+ }
+ }
+
+ protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) {
+ if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) &&
+ !isDataUriAccessible(recordedProgram.getDataUri())) {
+ // Since cleaning RecordedProgram from forgotten storage will take some time,
+ // ignore playback until cleaning is finished.
+ ToastUtils.show(getContext(),
+ getContext().getResources().getString(R.string.dvr_toast_recording_deleted),
+ Toast.LENGTH_SHORT);
+ return;
+ }
+ ParentalControlSettings parental = TvApplication.getSingletons(getActivity())
+ .getTvInputManagerHelper().getParentalControlSettings();
+ if (!parental.isParentalControlsEnabled()) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ return;
+ }
+ ChannelDataManager channelDataManager =
+ TvApplication.getSingletons(getActivity()).getChannelDataManager();
+ Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId());
+ if (channel != null && channel.isLocked()) {
+ checkPinToPlay(recordedProgram, seekTimeMs);
+ return;
+ }
+ String ratingString = recordedProgram.getContentRating();
+ if (TextUtils.isEmpty(ratingString)) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ return;
+ }
+ String[] ratingList = ratingString.split(",");
+ TvContentRating[] programRatings = new TvContentRating[ratingList.length];
+ for (int i = 0; i < ratingList.length; i++) {
+ programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]);
+ }
+ TvContentRating blockRatings = parental.getBlockedRating(programRatings);
+ if (blockRatings != null) {
+ checkPinToPlay(recordedProgram, seekTimeMs);
+ } else {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, false);
+ }
+ }
+
+ private boolean isDataUriAccessible(Uri dataUri) {
+ if (dataUri == null || dataUri.getPath() == null) {
+ return false;
+ }
+ try {
+ File recordedProgramPath = new File(dataUri.getPath());
+ if (recordedProgramPath.exists()) {
+ return true;
+ }
+ } catch (SecurityException e) {
+ }
+ return false;
+ }
+
+ private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) {
+ new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
+ new PinDialogFragment.ResultListener() {
+ @Override
+ public void done(boolean success) {
+ if (success) {
+ launchPlaybackActivity(recordedProgram, seekTimeMs, true);
+ }
+ }
+ }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG);
+ }
+
+ private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs,
+ boolean pinChecked) {
+ Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class);
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId());
+ if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs);
+ }
+ intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked);
+ getActivity().startActivity(intent);
+ }
+
+ private static class MyImageLoaderCallback extends
+ ImageLoader.ImageLoaderCallback<DvrDetailsFragment> {
+ private final Context mContext;
+ private final int mLoadType;
+
+ public MyImageLoaderCallback(DvrDetailsFragment fragment,
+ int loadType, Context context) {
+ super(fragment);
+ mLoadType = loadType;
+ mContext = context;
+ }
+
+ @Override
+ public void onBitmapLoaded(DvrDetailsFragment fragment,
+ @Nullable Bitmap bitmap) {
+ Drawable drawable;
+ int loadType = mLoadType;
+ if (bitmap == null) {
+ Resources res = mContext.getResources();
+ drawable = res.getDrawable(R.drawable.dvr_default_poster, null);
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) {
+ loadType &= ~LOAD_BACKGROUND_IMAGE;
+ fragment.mBackgroundHelper.setBackgroundColor(
+ res.getColor(R.color.dvr_detail_default_background));
+ fragment.mBackgroundHelper.setScrim(
+ res.getColor(R.color.dvr_detail_default_background_scrim));
+ }
+ } else {
+ drawable = new BitmapDrawable(mContext.getResources(), bitmap);
+ }
+ if (!fragment.isDetached()) {
+ if ((loadType & LOAD_LOGO_IMAGE) != 0) {
+ fragment.mDetailsOverview.setImageDrawable(drawable);
+ }
+ if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) {
+ fragment.mBackgroundHelper.setBackground(drawable);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
new file mode 100644
index 00000000..317b6af3
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrItemPresenter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.app.Activity;
+import android.support.annotation.CallSuper;
+import android.support.v17.leanback.widget.Presenter;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.android.tv.dvr.ui.DvrUiHelper;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An abstract class to present DVR items in {@link RecordingCardView}, which is mainly used in
+ * {@link DvrBrowseFragment}. DVR items might include:
+ * {@link com.android.tv.dvr.data.ScheduledRecording},
+ * {@link com.android.tv.dvr.data.RecordedProgram}, and
+ * {@link com.android.tv.dvr.data.SeriesRecording}.
+ */
+public abstract class DvrItemPresenter extends Presenter {
+ private final Set<ViewHolder> mBoundViewHolders = new HashSet<>();
+ private final OnClickListener mOnClickListener = onCreateOnClickListener();
+
+ @Override
+ @CallSuper
+ public void onBindViewHolder(ViewHolder viewHolder, Object o) {
+ viewHolder.view.setTag(o);
+ viewHolder.view.setOnClickListener(mOnClickListener);
+ mBoundViewHolders.add(viewHolder);
+ }
+
+ @Override
+ @CallSuper
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ mBoundViewHolders.remove(viewHolder);
+ viewHolder.view.setTag(null);
+ viewHolder.view.setOnClickListener(null);
+ }
+
+ /**
+ * Unbinds all bound view holders.
+ */
+ public void unbindAllViewHolders() {
+ // When browse fragments are destroyed, RecyclerView would not call presenters'
+ // onUnbindViewHolder(). We should handle it by ourselves to prevent resources leaks.
+ for (ViewHolder viewHolder : new HashSet<>(mBoundViewHolders)) {
+ onUnbindViewHolder(viewHolder);
+ }
+ }
+
+ /**
+ * Creates {@link OnClickListener} for DVR library's card views.
+ */
+ protected OnClickListener onCreateOnClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (view instanceof RecordingCardView) {
+ RecordingCardView v = (RecordingCardView) view;
+ DvrUiHelper.startDetailsActivity((Activity) v.getContext(),
+ v.getTag(), v.getImageView(), false);
+ }
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
new file mode 100644
index 00000000..37a72eaf
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/DvrListRowPresenter.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.support.v17.leanback.widget.ListRowPresenter;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+
+/** A list row presenter to display expand/fold card views list. */
+public class DvrListRowPresenter extends ListRowPresenter {
+ public DvrListRowPresenter(Context context) {
+ super();
+ setRowHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+ setExpandedRowHeight(
+ context.getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_expanded_row_height));
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
new file mode 100644
index 00000000..311137a9
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/FullScheduleCardHolder.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+/**
+ * Special object for schedule preview;
+ */
+final class FullScheduleCardHolder {
+ /**
+ * Full schedule card holder.
+ */
+ static final FullScheduleCardHolder FULL_SCHEDULE_CARD_HOLDER = new FullScheduleCardHolder();
+
+ private FullScheduleCardHolder() { }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
new file mode 100644
index 00000000..6d4763d4
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/FullSchedulesCardPresenter.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.util.Utils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
+ */
+class FullSchedulesCardPresenter extends DvrItemPresenter {
+ private Context mContext;
+ private final Drawable mIconDrawable;
+ private final String mCardTitleText;
+
+ public FullSchedulesCardPresenter(Context context) {
+ mContext = context;
+ mIconDrawable = mContext.getDrawable(R.drawable.dvr_full_schedule);
+ mCardTitleText = mContext.getString(R.string.dvr_full_schedule_card_view_title);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder vh, Object o) {
+ final RecordingCardView cardView = (RecordingCardView) vh.view;
+
+ cardView.setImage(mIconDrawable);
+ cardView.setTitle(mCardTitleText);
+ List<ScheduledRecording> scheduledRecordings = TvApplication.getSingletons(mContext)
+ .getDvrDataManager().getAvailableScheduledRecordings();
+ int fullDays = 0;
+ if (!scheduledRecordings.isEmpty()) {
+ fullDays = Utils.computeDateDifference(System.currentTimeMillis(),
+ Collections.max(scheduledRecordings, ScheduledRecording.START_TIME_COMPARATOR)
+ .getStartTimeMs()) + 1;
+ }
+ cardView.setContent(mContext.getResources().getQuantityString(
+ R.plurals.dvr_full_schedule_card_view_content, fullDays, fullDays), null);
+ super.onBindViewHolder(vh, o);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder vh) {
+ ((RecordingCardView) vh.view).reset();
+ super.onUnbindViewHolder(vh);
+ }
+
+ @Override
+ protected View.OnClickListener onCreateOnClickListener() {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DvrUiHelper.startSchedulesActivity(mContext, null);
+ }
+ };
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
new file mode 100644
index 00000000..fe9b9de5
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramDetailsFragment.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.res.Resources;
+import android.media.tv.TvInputManager;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
+
+/**
+ * {@link android.support.v17.leanback.app.DetailsFragment} for recorded program in DVR.
+ */
+public class RecordedProgramDetailsFragment extends DvrDetailsFragment
+ implements DvrDataManager.RecordedProgramListener {
+ private static final int ACTION_RESUME_PLAYING = 1;
+ private static final int ACTION_PLAY_FROM_BEGINNING = 2;
+ private static final int ACTION_DELETE_RECORDING = 3;
+
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
+
+ private RecordedProgram mRecordedProgram;
+ private DetailsContent mDetailsContent;
+ private boolean mPaused;
+ private DvrDataManager mDvrDataManager;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mDvrDataManager = TvApplication.getSingletons(getContext()).getDvrDataManager();
+ mDvrDataManager.addRecordedProgramListener(this);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onCreateInternal() {
+ mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
+ .getDvrWatchedPositionManager();
+ setDetailsOverviewRow(mDetailsContent);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateActions();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
+ }
+
+ @Override
+ public void onDestroy() {
+ mDvrDataManager.removeRecordedProgramListener(this);
+ super.onDestroy();
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ long recordedProgramId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ mRecordedProgram = mDvrDataManager.getRecordedProgram(recordedProgramId);
+ if (mRecordedProgram == null) {
+ // notify super class to end activity before initializing anything
+ return false;
+ }
+ mDetailsContent = createDetailsContent();
+ return true;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mRecordedProgram.getChannelId());
+ String description = TextUtils.isEmpty(mRecordedProgram.getLongDescription())
+ ? mRecordedProgram.getDescription() : mRecordedProgram.getLongDescription();
+ return new DetailsContent.Builder()
+ .setTitle(getTitleFromProgram(mRecordedProgram, channel))
+ .setStartTimeUtcMillis(mRecordedProgram.getStartTimeUtcMillis())
+ .setEndTimeUtcMillis(mRecordedProgram.getEndTimeUtcMillis())
+ .setDescription(description)
+ .setImageUris(mRecordedProgram, channel)
+ .build();
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (mDvrWatchedPositionManager.getWatchedStatus(mRecordedProgram)
+ == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ adapter.set(ACTION_RESUME_PLAYING, new Action(ACTION_RESUME_PLAYING,
+ res.getString(R.string.dvr_detail_resume_play), null,
+ res.getDrawable(R.drawable.lb_ic_play)));
+ adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_play_from_beginning), null,
+ res.getDrawable(R.drawable.lb_ic_replay)));
+ } else {
+ adapter.set(ACTION_PLAY_FROM_BEGINNING, new Action(ACTION_PLAY_FROM_BEGINNING,
+ res.getString(R.string.dvr_detail_watch), null,
+ res.getDrawable(R.drawable.lb_ic_play)));
+ }
+ adapter.set(ACTION_DELETE_RECORDING, new Action(ACTION_DELETE_RECORDING,
+ res.getString(R.string.dvr_detail_delete), null,
+ res.getDrawable(R.drawable.ic_delete_32dp)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_PLAY_FROM_BEGINNING) {
+ startPlayback(mRecordedProgram, TvInputManager.TIME_SHIFT_INVALID_TIME);
+ } else if (action.getId() == ACTION_RESUME_PLAYING) {
+ startPlayback(mRecordedProgram, mDvrWatchedPositionManager
+ .getWatchedPosition(mRecordedProgram.getId()));
+ } else if (action.getId() == ACTION_DELETE_RECORDING) {
+ DvrManager dvrManager = TvApplication
+ .getSingletons(getActivity()).getDvrManager();
+ dvrManager.removeRecordedProgram(mRecordedProgram);
+ getActivity().finish();
+ }
+ }
+ };
+ }
+
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) { }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (recordedProgram.getId() == mRecordedProgram.getId()) {
+ getActivity().finish();
+ }
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
new file mode 100644
index 00000000..ee978797
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordedProgramPresenter.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.view.ViewGroup;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.util.Utils;
+
+/**
+ * Presents a {@link RecordedProgram} in the {@link DvrBrowseFragment}.
+ */
+public class RecordedProgramPresenter extends DvrItemPresenter {
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private final Context mContext;
+ private String mTodayString;
+ private String mYesterdayString;
+ private final int mProgressBarColor;
+ private final boolean mShowEpisodeTitle;
+ private final boolean mExpandTitleWhenFocused;
+
+ private static final class RecordedProgramViewHolder extends ViewHolder
+ implements WatchedPositionChangedListener {
+ private RecordedProgram mProgram;
+
+ RecordedProgramViewHolder(RecordingCardView view, int progressColor) {
+ super(view);
+ view.setProgressBarColor(progressColor);
+ }
+
+ private void setProgram(RecordedProgram program) {
+ mProgram = program;
+ }
+
+ private void setProgressBar(long watchedPositionMs) {
+ ((RecordingCardView) view).setProgressBar(
+ (watchedPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) ? null
+ : Math.min(100, (int) (100.0f * watchedPositionMs
+ / mProgram.getDurationMillis())));
+ }
+
+ @Override
+ public void onWatchedPositionChanged(long programId, long positionMs) {
+ if (programId == mProgram.getId()) {
+ setProgressBar(positionMs);
+ }
+ }
+ }
+
+ public RecordedProgramPresenter(Context context, boolean showEpisodeTitle,
+ boolean expandTitleWhenFocused) {
+ mContext = context;
+ mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager();
+ mTodayString = mContext.getString(R.string.dvr_date_today);
+ mYesterdayString = mContext.getString(R.string.dvr_date_yesterday);
+ mDvrWatchedPositionManager =
+ TvApplication.getSingletons(mContext).getDvrWatchedPositionManager();
+ mProgressBarColor = mContext.getResources()
+ .getColor(R.color.play_controls_progress_bar_watched);
+ mShowEpisodeTitle = showEpisodeTitle;
+ mExpandTitleWhenFocused = expandTitleWhenFocused;
+ }
+
+ public RecordedProgramPresenter(Context context) {
+ this(context, false, false);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ RecordingCardView view = new RecordingCardView(mContext, mExpandTitleWhenFocused);
+ return new RecordedProgramViewHolder(view, mProgressBarColor);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder viewHolder, Object o) {
+ final RecordedProgram program = (RecordedProgram) o;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ Channel channel = mChannelDataManager.getChannel(program.getChannelId());
+ String titleString = mShowEpisodeTitle ? program.getEpisodeDisplayTitle(mContext)
+ : program.getTitleWithEpisodeNumber(mContext);
+ SpannableString title = titleString == null ? null : new SpannableString(titleString);
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : mContext.getResources().getString(R.string.no_program_information));
+ } else if (!mShowEpisodeTitle) {
+ // TODO: Some translation may add delimiters in-between program titles, we should use
+ // a more robust way to get the span range.
+ String programTitle = program.getTitle();
+ title.setSpan(new TextAppearanceSpan(mContext,
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ cardView.setTitle(title);
+ String imageUri = null;
+ boolean isChannelLogo = false;
+ if (program.getPosterArtUri() != null) {
+ imageUri = program.getPosterArtUri();
+ } else if (program.getThumbnailUri() != null) {
+ imageUri = program.getThumbnailUri();
+ } else if (channel != null) {
+ imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString();
+ isChannelLogo = true;
+ }
+ cardView.setImageUri(imageUri, isChannelLogo);
+ int durationMinutes = Math.max(1, Utils.getRoundOffMinsFromMs(program.getDurationMillis()));
+ String durationString = getContext().getResources().getQuantityString(
+ R.plurals.dvr_program_duration, durationMinutes, durationMinutes);
+ cardView.setContent(getDescription(program), durationString);
+ if (viewHolder instanceof RecordedProgramViewHolder) {
+ RecordedProgramViewHolder cardViewHolder = (RecordedProgramViewHolder) viewHolder;
+ cardViewHolder.setProgram(program);
+ mDvrWatchedPositionManager.addListener(cardViewHolder, program.getId());
+ cardViewHolder
+ .setProgressBar(mDvrWatchedPositionManager.getWatchedPosition(program.getId()));
+ }
+ super.onBindViewHolder(viewHolder, o);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ if (viewHolder instanceof RecordedProgramViewHolder) {
+ mDvrWatchedPositionManager.removeListener((RecordedProgramViewHolder) viewHolder,
+ ((RecordedProgramViewHolder) viewHolder).mProgram.getId());
+ }
+ ((RecordingCardView) viewHolder.view).reset();
+ super.onUnbindViewHolder(viewHolder);
+ }
+
+ /**
+ * Returns description would be used in its card view.
+ */
+ protected String getDescription(RecordedProgram recording) {
+ int dateDifference = Utils.computeDateDifference(recording.getStartTimeUtcMillis(),
+ System.currentTimeMillis());
+ if (dateDifference == 0) {
+ return mTodayString;
+ } else if (dateDifference == 1) {
+ return mYesterdayString;
+ } else {
+ return Utils.getDurationString(mContext, recording.getStartTimeUtcMillis(),
+ recording.getStartTimeUtcMillis(), false, true, false, 0);
+ }
+ }
+
+ /**
+ * Returns context.
+ */
+ protected Context getContext() {
+ return mContext;
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingCardView.java b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
new file mode 100644
index 00000000..7b0a8cb9
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordingCardView.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.v17.leanback.widget.BaseCardView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.tv.R;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.ui.ViewUtils;
+import com.android.tv.util.ImageLoader;
+
+/**
+ * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording}
+ * or {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
+ */
+public class RecordingCardView extends BaseCardView {
+ // This value should be the same with
+ // android.support.v17.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
+ private final static int ANIMATION_DURATION = 150;
+ private final ImageView mImageView;
+ private final int mImageWidth;
+ private final int mImageHeight;
+ private String mImageUri;
+ private final TextView mMajorContentView;
+ private final TextView mMinorContentView;
+ private final ProgressBar mProgressBar;
+ private final View mAffiliatedIconContainer;
+ private final ImageView mAffiliatedIcon;
+ private final Drawable mDefaultImage;
+ private final FrameLayout mTitleArea;
+ private final TextView mFoldedTitleView;
+ private final TextView mExpandedTitleView;
+ private final ValueAnimator mExpandTitleAnimator;
+ private final int mFoldedTitleHeight;
+ private final int mExpandedTitleHeight;
+ private final boolean mExpandTitleWhenFocused;
+ private boolean mExpanded;
+
+ public RecordingCardView(Context context) {
+ this(context, false);
+ }
+
+ public RecordingCardView(Context context, boolean expandTitleWhenFocused) {
+ this(context, context.getResources().getDimensionPixelSize(
+ R.dimen.dvr_library_card_image_layout_width), context.getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height),
+ expandTitleWhenFocused);
+ }
+
+ public RecordingCardView(Context context, int imageWidth, int imageHeight,
+ boolean expandTitleWhenFocused) {
+ super(context);
+ //TODO(dvr): move these to the layout XML.
+ setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
+ setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS);
+ setFocusable(true);
+ setFocusableInTouchMode(true);
+ mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null);
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ inflater.inflate(R.layout.dvr_recording_card_view, this);
+ mImageView = (ImageView) findViewById(R.id.image);
+ mImageWidth = imageWidth;
+ mImageHeight = imageHeight;
+ mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
+ mAffiliatedIconContainer = findViewById(R.id.affiliated_icon_container);
+ mAffiliatedIcon = (ImageView) findViewById(R.id.affiliated_icon);
+ mMajorContentView = (TextView) findViewById(R.id.content_major);
+ mMinorContentView = (TextView) findViewById(R.id.content_minor);
+ mTitleArea = (FrameLayout) findViewById(R.id.title_area);
+ mFoldedTitleView = (TextView) findViewById(R.id.title_one_line);
+ mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines);
+ mFoldedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height);
+ mExpandedTitleHeight = getResources()
+ .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height);
+ mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION);
+ mExpandTitleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float value = (Float) valueAnimator.getAnimatedValue();
+ mExpandedTitleView.setAlpha(value);
+ mFoldedTitleView.setAlpha(1.0f - value);
+ ViewUtils.setLayoutHeight(mTitleArea, (int) (mFoldedTitleHeight
+ + (mExpandedTitleHeight - mFoldedTitleHeight) * value));
+ }
+ });
+ mExpandTitleWhenFocused = expandTitleWhenFocused;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ if (mExpandTitleWhenFocused) {
+ if (gainFocus) {
+ expandTitle(true, true);
+ } else {
+ expandTitle(false, true);
+ }
+ }
+ }
+
+ /**
+ * Expands/folds the title area to show program title with two/one lines.
+ *
+ * @param expand {@code true} to expand the title area, or {@code false} to fold it.
+ * @param withAnimation {@code true} to expand/fold with animation.
+ */
+ public void expandTitle(boolean expand, boolean withAnimation) {
+ if (expand != mExpanded && mFoldedTitleView.getLayout().getEllipsisCount(0) > 0) {
+ if (withAnimation) {
+ if (expand) {
+ mExpandTitleAnimator.start();
+ } else {
+ mExpandTitleAnimator.reverse();
+ }
+ } else {
+ if (expand) {
+ mFoldedTitleView.setAlpha(0.0f);
+ mExpandedTitleView.setAlpha(1.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight);
+ } else {
+ mFoldedTitleView.setAlpha(1.0f);
+ mExpandedTitleView.setAlpha(0.0f);
+ ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight);
+ }
+ }
+ mExpanded = expand;
+ }
+ }
+
+ void setTitle(CharSequence title) {
+ mFoldedTitleView.setText(title);
+ mExpandedTitleView.setText(title);
+ }
+
+ void setContent(CharSequence majorContent, CharSequence minorContent) {
+ if (!TextUtils.isEmpty(majorContent)) {
+ mMajorContentView.setText(majorContent);
+ mMajorContentView.setVisibility(View.VISIBLE);
+ } else {
+ mMajorContentView.setVisibility(View.GONE);
+ }
+ if (!TextUtils.isEmpty(minorContent)) {
+ mMinorContentView.setText(minorContent);
+ mMinorContentView.setVisibility(View.VISIBLE);
+ } else {
+ mMinorContentView.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Sets progress bar. If progress is {@code null}, hides progress bar.
+ */
+ void setProgressBar(Integer progress) {
+ if (progress == null) {
+ mProgressBar.setVisibility(View.GONE);
+ } else {
+ mProgressBar.setProgress(progress);
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Sets the color of progress bar.
+ */
+ void setProgressBarColor(int color) {
+ mProgressBar.getProgressDrawable().setTint(color);
+ }
+
+ void setImageUri(String uri, boolean isChannelLogo) {
+ if (isChannelLogo) {
+ mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+ } else {
+ mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ }
+ mImageUri = uri;
+ if (TextUtils.isEmpty(uri)) {
+ mImageView.setImageDrawable(mDefaultImage);
+ } else {
+ ImageLoader.loadBitmap(getContext(), uri, mImageWidth, mImageHeight,
+ new RecordingCardImageLoaderCallback(this, uri));
+ }
+ }
+
+ /**
+ * Set image to card view.
+ */
+ public void setImage(Drawable image) {
+ if (image != null) {
+ mImageView.setImageDrawable(image);
+ }
+ }
+
+ public void setAffiliatedIcon(int imageResId) {
+ if (imageResId > 0) {
+ mAffiliatedIconContainer.setVisibility(View.VISIBLE);
+ mAffiliatedIcon.setImageResource(imageResId);
+ } else {
+ mAffiliatedIconContainer.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Returns image view.
+ */
+ public ImageView getImageView() {
+ return mImageView;
+ }
+
+ private static class RecordingCardImageLoaderCallback
+ extends ImageLoader.ImageLoaderCallback<RecordingCardView> {
+ private final String mUri;
+
+ RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) {
+ super(referent);
+ mUri = uri;
+ }
+
+ @Override
+ public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) {
+ if (bitmap == null || !mUri.equals(view.mImageUri)) {
+ view.mImageView.setImageDrawable(view.mDefaultImage);
+ } else {
+ view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap));
+ }
+ }
+ }
+
+ public void reset() {
+ mFoldedTitleView.setText(null);
+ mExpandedTitleView.setText(null);
+ setContent(null, null);
+ mImageView.setImageDrawable(mDefaultImage);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
new file mode 100644
index 00000000..a877e05f
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.data.ScheduledRecording;
+
+/**
+ * {@link DetailsFragment} for recordings in DVR.
+ */
+abstract class RecordingDetailsFragment extends DvrDetailsFragment {
+ private ScheduledRecording mRecording;
+
+ @Override
+ protected void onCreateInternal() {
+ setDetailsOverviewRow(createDetailsContent());
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ long scheduledRecordingId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ mRecording = TvApplication.getSingletons(getContext()).getDvrDataManager()
+ .getScheduledRecording(scheduledRecordingId);
+ return mRecording != null;
+ }
+
+ /**
+ * Returns {@link ScheduledRecording} for the current fragment.
+ */
+ public ScheduledRecording getRecording() {
+ return mRecording;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mRecording.getChannelId());
+ SpannableString title = mRecording.getProgramTitleWithEpisodeNumber(getContext()) == null ?
+ null : new SpannableString(mRecording
+ .getProgramTitleWithEpisodeNumber(getContext()));
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : getContext().getResources().getString(
+ R.string.no_program_information));
+ } else {
+ String programTitle = mRecording.getProgramTitle();
+ title.setSpan(new TextAppearanceSpan(getContext(),
+ R.style.text_appearance_card_view_episode_number), programTitle == null ? 0
+ : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ String description = !TextUtils.isEmpty(mRecording.getProgramDescription()) ?
+ mRecording.getProgramDescription() : mRecording.getProgramLongDescription();
+ if (TextUtils.isEmpty(description)) {
+ description = channel != null ? channel.getDescription() : null;
+ }
+ return new DetailsContent.Builder()
+ .setTitle(title)
+ .setStartTimeUtcMillis(mRecording.getStartTimeMs())
+ .setEndTimeUtcMillis(mRecording.getEndTimeMs())
+ .setDescription(description)
+ .setImageUris(mRecording.getProgramPosterArtUri(),
+ mRecording.getProgramThumbnailUri(), channel)
+ .build();
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
new file mode 100644
index 00000000..eb0f4f0d
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.ui.DvrUiHelper;
+
+/**
+ * {@link RecordingDetailsFragment} for scheduled recording in DVR.
+ */
+public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment {
+ private static final int ACTION_VIEW_SCHEDULE = 1;
+ private static final int ACTION_CANCEL = 2;
+
+ private DvrManager mDvrManager;
+ private Action mScheduleAction;
+ private boolean mHideViewSchedule;
+
+ @Override
+ public void onCreate(Bundle savedInstance) {
+ mDvrManager = TvApplication.getSingletons(getContext()).getDvrManager();
+ mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
+ super.onCreate(savedInstance);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mScheduleAction != null) {
+ mScheduleAction.setIcon(getResources().getDrawable(getScheduleIconId()));
+ }
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ SparseArrayObjectAdapter adapter =
+ new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ if (!mHideViewSchedule) {
+ mScheduleAction = new Action(ACTION_VIEW_SCHEDULE,
+ res.getString(R.string.dvr_detail_view_schedule), null,
+ res.getDrawable(getScheduleIconId()));
+ adapter.set(ACTION_VIEW_SCHEDULE, mScheduleAction);
+ }
+ adapter.set(ACTION_CANCEL, new Action(ACTION_CANCEL,
+ res.getString(R.string.epg_dvr_dialog_message_remove_recording_schedule), null,
+ res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
+ return adapter;
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ long actionId = action.getId();
+ if (actionId == ACTION_VIEW_SCHEDULE) {
+ DvrUiHelper.startSchedulesActivity(getContext(), getRecording());
+ } else if (actionId == ACTION_CANCEL) {
+ mDvrManager.removeScheduledRecording(getRecording());
+ getActivity().finish();
+ }
+ }
+ };
+ }
+
+ private int getScheduleIconId() {
+ if (mDvrManager.isConflicting(getRecording())) {
+ return R.drawable.ic_warning_white_32dp;
+ } else {
+ return R.drawable.ic_schedule_32dp;
+ }
+ }
+}
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
new file mode 100644
index 00000000..efc8785a
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.os.Handler;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.view.ViewGroup;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.util.Utils;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Presents a {@link ScheduledRecording} in the {@link DvrBrowseFragment}.
+ */
+class ScheduledRecordingPresenter extends DvrItemPresenter {
+ private static final long PROGRESS_UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
+
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrManager mDvrManager;
+ private final Context mContext;
+ private final int mProgressBarColor;
+
+ private static final class ScheduledRecordingViewHolder extends ViewHolder {
+ private final Handler mHandler = new Handler();
+ private ScheduledRecording mScheduledRecording;
+ private final Runnable mProgressBarUpdater = new Runnable() {
+ @Override
+ public void run() {
+ updateProgressBar();
+ mHandler.postDelayed(this, PROGRESS_UPDATE_INTERVAL_MS);
+ }
+ };
+
+ ScheduledRecordingViewHolder(RecordingCardView view, int progressBarColor) {
+ super(view);
+ view.setProgressBarColor(progressBarColor);
+ }
+
+ private void updateProgressBar() {
+ if (mScheduledRecording == null) {
+ return;
+ }
+ int recordingState = mScheduledRecording.getState();
+ RecordingCardView cardView = (RecordingCardView) view;
+ if (recordingState == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
+ cardView.setProgressBar(Math.max(0, Math.min((int) (100 *
+ (System.currentTimeMillis() - mScheduledRecording.getStartTimeMs())
+ / mScheduledRecording.getDuration()), 100)));
+ } else if (recordingState == ScheduledRecording.STATE_RECORDING_FINISHED) {
+ cardView.setProgressBar(100);
+ } else {
+ // Hides progress bar.
+ cardView.setProgressBar(null);
+ }
+ }
+
+ private void startUpdateProgressBar() {
+ mHandler.post(mProgressBarUpdater);
+ }
+
+ private void stopUpdateProgressBar() {
+ mHandler.removeCallbacks(mProgressBarUpdater);
+ }
+ }
+
+ public ScheduledRecordingPresenter(Context context) {
+ mContext = context;
+ ApplicationSingletons singletons = TvApplication.getSingletons(mContext);
+ mChannelDataManager = singletons.getChannelDataManager();
+ mDvrManager = singletons.getDvrManager();
+ mProgressBarColor = mContext.getResources()
+ .getColor(R.color.play_controls_recording_icon_color_on_focus);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ RecordingCardView view = new RecordingCardView(mContext);
+ return new ScheduledRecordingViewHolder(view, mProgressBarColor);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ final ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ final ScheduledRecording recording = (ScheduledRecording) o;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ final Context context = viewHolder.view.getContext();
+
+ setTitleAndImage(cardView, recording);
+ int dateDifference = Utils.computeDateDifference(System.currentTimeMillis(),
+ recording.getStartTimeMs());
+ if (dateDifference <= 0) {
+ cardView.setContent(mContext.getString(R.string.dvr_date_today_time,
+ Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0)), null);
+ } else if (dateDifference == 1) {
+ cardView.setContent(mContext.getString(R.string.dvr_date_tomorrow_time,
+ Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getEndTimeMs(), false, false, true, 0)), null);
+ } else {
+ cardView.setContent(Utils.getDurationString(context, recording.getStartTimeMs(),
+ recording.getStartTimeMs(), false, true, false, 0), null);
+ }
+ if (mDvrManager.isConflicting(recording)) {
+ cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
+ } else {
+ cardView.setAffiliatedIcon(0);
+ }
+ viewHolder.updateProgressBar();
+ viewHolder.mScheduledRecording = recording;
+ viewHolder.startUpdateProgressBar();
+ super.onBindViewHolder(viewHolder, o);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder baseHolder) {
+ ScheduledRecordingViewHolder viewHolder = (ScheduledRecordingViewHolder) baseHolder;
+ viewHolder.stopUpdateProgressBar();
+ viewHolder.mScheduledRecording = null;
+ ((RecordingCardView) viewHolder.view).reset();
+ super.onUnbindViewHolder(viewHolder);
+ }
+
+ private void setTitleAndImage(RecordingCardView cardView, ScheduledRecording recording) {
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+ SpannableString title = recording.getProgramTitleWithEpisodeNumber(mContext) == null ?
+ null : new SpannableString(recording.getProgramTitleWithEpisodeNumber(mContext));
+ if (TextUtils.isEmpty(title)) {
+ title = new SpannableString(channel != null ? channel.getDisplayName()
+ : mContext.getResources().getString(R.string.no_program_information));
+ } else {
+ String programTitle = recording.getProgramTitle();
+ title.setSpan(new TextAppearanceSpan(mContext,
+ R.style.text_appearance_card_view_episode_number),
+ programTitle == null ? 0 : programTitle.length(), title.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ String imageUri = recording.getProgramPosterArtUri();
+ boolean isChannelLogo = false;
+ if (TextUtils.isEmpty(imageUri)) {
+ imageUri = channel != null ?
+ TvContract.buildChannelLogoUri(channel.getId()).toString() : null;
+ isChannelLogo = true;
+ }
+ cardView.setTitle(title);
+ cardView.setImageUri(imageUri, isChannelLogo);
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
new file mode 100644
index 00000000..f7b60b50
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingDetailsFragment.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.media.tv.TvInputManager;
+import android.os.Bundle;
+import android.support.v17.leanback.app.DetailsFragment;
+import android.support.v17.leanback.widget.Action;
+import android.support.v17.leanback.widget.ArrayObjectAdapter;
+import android.support.v17.leanback.widget.ClassPresenterSelector;
+import android.support.v17.leanback.widget.DetailsOverviewRow;
+import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
+import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnActionClickedListener;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
+import android.text.TextUtils;
+
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.BaseProgram;
+import com.android.tv.data.Channel;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.SeriesRecording;
+import com.android.tv.dvr.ui.DvrUiHelper;
+import com.android.tv.dvr.ui.SortedArrayAdapter;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * {@link DetailsFragment} for series recording in DVR.
+ */
+public class SeriesRecordingDetailsFragment extends DvrDetailsFragment implements
+ DvrDataManager.SeriesRecordingListener, DvrDataManager.RecordedProgramListener {
+ private static final int ACTION_WATCH = 1;
+ private static final int ACTION_SERIES_SCHEDULES = 2;
+ private static final int ACTION_DELETE = 3;
+
+ private DvrWatchedPositionManager mDvrWatchedPositionManager;
+ private DvrDataManager mDvrDataManager;
+
+ private SeriesRecording mSeries;
+ // NOTICE: mRecordedPrograms should only be used in creating details fragments.
+ // After fragments are created, it should be cleared to save resources.
+ private List<RecordedProgram> mRecordedPrograms;
+ private RecordedProgram mRecommendRecordedProgram;
+ private DetailsContent mDetailsContent;
+ private int mSeasonRowCount;
+ private SparseArrayObjectAdapter mActionsAdapter;
+ private Action mDeleteAction;
+
+ private boolean mPaused;
+ private long mInitialPlaybackPositionMs;
+ private String mWatchLabel;
+ private String mResumeLabel;
+ private Drawable mWatchDrawable;
+ private RecordedProgramPresenter mRecordedProgramPresenter;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ mDvrDataManager = TvApplication.getSingletons(getActivity()).getDvrDataManager();
+ mWatchLabel = getString(R.string.dvr_detail_watch);
+ mResumeLabel = getString(R.string.dvr_detail_series_resume);
+ mWatchDrawable = getResources().getDrawable(R.drawable.lb_ic_play, null);
+ mRecordedProgramPresenter = new RecordedProgramPresenter(getContext(), true, true);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mDvrWatchedPositionManager = TvApplication.getSingletons(getActivity())
+ .getDvrWatchedPositionManager();
+ setDetailsOverviewRow(mDetailsContent);
+ setupRecordedProgramsRow();
+ mDvrDataManager.addSeriesRecordingListener(this);
+ mDvrDataManager.addRecordedProgramListener(this);
+ mRecordedPrograms = null;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mPaused) {
+ updateWatchAction();
+ mPaused = false;
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPaused = true;
+ }
+
+ private void updateWatchAction() {
+ List<RecordedProgram> programs = mDvrDataManager.getRecordedPrograms(mSeries.getId());
+ Collections.sort(programs, RecordedProgram.EPISODE_COMPARATOR);
+ mRecommendRecordedProgram = getRecommendProgram(programs);
+ if (mRecommendRecordedProgram == null) {
+ mActionsAdapter.clear(ACTION_WATCH);
+ } else {
+ String episodeStatus;
+ if(mDvrWatchedPositionManager.getWatchedStatus(mRecommendRecordedProgram)
+ == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ episodeStatus = mResumeLabel;
+ mInitialPlaybackPositionMs = mDvrWatchedPositionManager
+ .getWatchedPosition(mRecommendRecordedProgram.getId());
+ } else {
+ episodeStatus = mWatchLabel;
+ mInitialPlaybackPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
+ }
+ String episodeDisplayNumber = mRecommendRecordedProgram.getEpisodeDisplayNumber(
+ getContext());
+ mActionsAdapter.set(ACTION_WATCH, new Action(ACTION_WATCH,
+ episodeStatus, episodeDisplayNumber, mWatchDrawable));
+ }
+ }
+
+ @Override
+ protected boolean onLoadRecordingDetails(Bundle args) {
+ long recordId = args.getLong(DvrDetailsActivity.RECORDING_ID);
+ mSeries = TvApplication.getSingletons(getActivity()).getDvrDataManager()
+ .getSeriesRecording(recordId);
+ if (mSeries == null) {
+ return false;
+ }
+ mRecordedPrograms = mDvrDataManager.getRecordedPrograms(mSeries.getId());
+ Collections.sort(mRecordedPrograms, RecordedProgram.SEASON_REVERSED_EPISODE_COMPARATOR);
+ mDetailsContent = createDetailsContent();
+ return true;
+ }
+
+ @Override
+ protected PresenterSelector onCreatePresenterSelector(
+ DetailsOverviewRowPresenter rowPresenter) {
+ ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
+ presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
+ presenterSelector.addClassPresenter(ListRow.class, new DvrListRowPresenter(getContext()));
+ return presenterSelector;
+ }
+
+ private DetailsContent createDetailsContent() {
+ Channel channel = TvApplication.getSingletons(getContext()).getChannelDataManager()
+ .getChannel(mSeries.getChannelId());
+ String description = TextUtils.isEmpty(mSeries.getLongDescription())
+ ? mSeries.getDescription() : mSeries.getLongDescription();
+ return new DetailsContent.Builder()
+ .setTitle(mSeries.getTitle())
+ .setDescription(description)
+ .setImageUris(mSeries.getPosterUri(), mSeries.getPhotoUri(), channel)
+ .build();
+ }
+
+ @Override
+ protected SparseArrayObjectAdapter onCreateActionsAdapter() {
+ mActionsAdapter = new SparseArrayObjectAdapter(new ActionPresenterSelector());
+ Resources res = getResources();
+ updateWatchAction();
+ mActionsAdapter.set(ACTION_SERIES_SCHEDULES, new Action(ACTION_SERIES_SCHEDULES,
+ getString(R.string.dvr_detail_view_schedule), null,
+ res.getDrawable(R.drawable.ic_schedule_32dp, null)));
+ mDeleteAction = new Action(ACTION_DELETE,
+ getString(R.string.dvr_detail_series_delete), null,
+ res.getDrawable(R.drawable.ic_delete_32dp, null));
+ if (!mRecordedPrograms.isEmpty()) {
+ mActionsAdapter.set(ACTION_DELETE, mDeleteAction);
+ }
+ return mActionsAdapter;
+ }
+
+ private void setupRecordedProgramsRow() {
+ for (RecordedProgram program : mRecordedPrograms) {
+ addProgram(program);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mDvrDataManager.removeSeriesRecordingListener(this);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ if (mSeries != null) {
+ mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeries.getId());
+ }
+ mRecordedProgramPresenter.unbindAllViewHolders();
+ }
+
+ @Override
+ protected OnActionClickedListener onCreateOnActionClickedListener() {
+ return new OnActionClickedListener() {
+ @Override
+ public void onActionClicked(Action action) {
+ if (action.getId() == ACTION_WATCH) {
+ startPlayback(mRecommendRecordedProgram, mInitialPlaybackPositionMs);
+ } else if (action.getId() == ACTION_SERIES_SCHEDULES) {
+ DvrUiHelper.startSchedulesActivityForSeries(getContext(), mSeries);
+ } else if (action.getId() == ACTION_DELETE) {
+ DvrUiHelper.startSeriesDeletionActivity(getContext(), mSeries.getId());
+ }
+ }
+ };
+ }
+
+ /**
+ * The programs are sorted by season number and episode number.
+ */
+ private RecordedProgram getRecommendProgram(List<RecordedProgram> programs) {
+ for (int i = programs.size() - 1 ; i >= 0 ; i--) {
+ RecordedProgram program = programs.get(i);
+ int watchedStatus = mDvrWatchedPositionManager.getWatchedStatus(program);
+ if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_NEW) {
+ continue;
+ }
+ if (watchedStatus == DvrWatchedPositionManager.DVR_WATCHED_STATUS_WATCHING) {
+ return program;
+ }
+ if (i == programs.size() - 1) {
+ return program;
+ } else {
+ return programs.get(i + 1);
+ }
+ }
+ return programs.isEmpty() ? null : programs.get(0);
+ }
+
+ @Override
+ public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) { }
+
+ @Override
+ public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (mSeries.getId() == series.getId()) {
+ mSeries = series;
+ }
+ }
+ }
+
+ @Override
+ public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) {
+ for (SeriesRecording series : seriesRecordings) {
+ if (series.getId() == mSeries.getId()) {
+ getActivity().finish();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
+ addProgram(recordedProgram);
+ if (mActionsAdapter.lookup(ACTION_DELETE) == null) {
+ mActionsAdapter.set(ACTION_DELETE, mDeleteAction);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ // Do nothing
+ }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(), mSeries.getSeriesId())) {
+ ListRow row = getSeasonRow(recordedProgram.getSeasonNumber(), false);
+ if (row != null) {
+ SeasonRowAdapter adapter = (SeasonRowAdapter) row.getAdapter();
+ adapter.remove(recordedProgram);
+ if (adapter.isEmpty()) {
+ getRowsAdapter().remove(row);
+ if (getRowsAdapter().size() == 1) {
+ // No season rows left. Only DetailsOverviewRow
+ mActionsAdapter.clear(ACTION_DELETE);
+ }
+ }
+ }
+ if (recordedProgram.getId() == mRecommendRecordedProgram.getId()) {
+ updateWatchAction();
+ }
+ }
+ }
+ }
+
+ private void addProgram(RecordedProgram program) {
+ String programSeasonNumber =
+ TextUtils.isEmpty(program.getSeasonNumber()) ? "" : program.getSeasonNumber();
+ getOrCreateSeasonRowAdapter(programSeasonNumber).add(program);
+ }
+
+ private SeasonRowAdapter getOrCreateSeasonRowAdapter(String seasonNumber) {
+ ListRow row = getSeasonRow(seasonNumber, true);
+ return (SeasonRowAdapter) row.getAdapter();
+ }
+
+ private ListRow getSeasonRow(String seasonNumber, boolean createNewRow) {
+ seasonNumber = TextUtils.isEmpty(seasonNumber) ? "" : seasonNumber;
+ ArrayObjectAdapter rowsAdaptor = getRowsAdapter();
+ for (int i = rowsAdaptor.size() - 1; i >= 0; i--) {
+ Object row = rowsAdaptor.get(i);
+ if (row instanceof ListRow) {
+ int compareResult = BaseProgram.numberCompare(seasonNumber,
+ ((SeasonRowAdapter) ((ListRow) row).getAdapter()).mSeasonNumber);
+ if (compareResult == 0) {
+ return (ListRow) row;
+ } else if (compareResult < 0) {
+ return createNewRow ? createNewSeasonRow(seasonNumber, i + 1) : null;
+ }
+ }
+ }
+ return createNewRow ? createNewSeasonRow(seasonNumber, rowsAdaptor.size()) : null;
+ }
+
+ private ListRow createNewSeasonRow(String seasonNumber, int position) {
+ String seasonTitle = seasonNumber.isEmpty() ? mSeries.getTitle()
+ : getString(R.string.dvr_detail_series_season_title, seasonNumber);
+ HeaderItem header = new HeaderItem(mSeasonRowCount++, seasonTitle);
+ ClassPresenterSelector selector = new ClassPresenterSelector();
+ selector.addClassPresenter(RecordedProgram.class, mRecordedProgramPresenter);
+ ListRow row = new ListRow(header, new SeasonRowAdapter(selector,
+ new Comparator<RecordedProgram>() {
+ @Override
+ public int compare(RecordedProgram lhs, RecordedProgram rhs) {
+ return BaseProgram.EPISODE_COMPARATOR.compare(lhs, rhs);
+ }
+ }, seasonNumber));
+ getRowsAdapter().add(position, row);
+ return row;
+ }
+
+ private class SeasonRowAdapter extends SortedArrayAdapter<RecordedProgram> {
+ private String mSeasonNumber;
+
+ SeasonRowAdapter(PresenterSelector selector, Comparator<RecordedProgram> comparator,
+ String seasonNumber) {
+ super(selector, comparator);
+ mSeasonNumber = seasonNumber;
+ }
+
+ @Override
+ public long getId(RecordedProgram program) {
+ return program.getId();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
new file mode 100644
index 00000000..af6ecc19
--- /dev/null
+++ b/src/com/android/tv/dvr/ui/browse/SeriesRecordingPresenter.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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.android.tv.dvr.ui.browse;
+
+import android.content.Context;
+import android.media.tv.TvContract;
+import android.media.tv.TvInputManager;
+import android.text.TextUtils;
+import android.view.ViewGroup;
+
+import com.android.tv.ApplicationSingletons;
+import com.android.tv.R;
+import com.android.tv.TvApplication;
+import com.android.tv.data.Channel;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
+import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
+import com.android.tv.dvr.DvrManager;
+import com.android.tv.dvr.DvrWatchedPositionManager;
+import com.android.tv.dvr.DvrWatchedPositionManager.WatchedPositionChangedListener;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
+import com.android.tv.dvr.data.SeriesRecording;
+
+import java.util.List;
+
+/**
+ * Presents a {@link SeriesRecording} in {@link DvrBrowseFragment}.
+ */
+class SeriesRecordingPresenter extends DvrItemPresenter {
+ private final ChannelDataManager mChannelDataManager;
+ private final DvrDataManager mDvrDataManager;
+ private final DvrManager mDvrManager;
+ private final DvrWatchedPositionManager mWatchedPositionManager;
+
+ private static final class SeriesRecordingViewHolder extends ViewHolder implements
+ WatchedPositionChangedListener, ScheduledRecordingListener, RecordedProgramListener {
+ private SeriesRecording mSeriesRecording;
+ private RecordingCardView mCardView;
+ private DvrDataManager mDvrDataManager;
+ private DvrManager mDvrManager;
+ private DvrWatchedPositionManager mWatchedPositionManager;
+
+ SeriesRecordingViewHolder(RecordingCardView view, DvrDataManager dvrDataManager,
+ DvrManager dvrManager, DvrWatchedPositionManager watchedPositionManager) {
+ super(view);
+ mCardView = view;
+ mDvrDataManager = dvrDataManager;
+ mDvrManager = dvrManager;
+ mWatchedPositionManager = watchedPositionManager;
+ }
+
+ @Override
+ public void onWatchedPositionChanged(long recordedProgramId, long positionMs) {
+ if (positionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.removeListener(this, recordedProgramId);
+ updateCardViewContent();
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduledRecording : scheduledRecordings) {
+ if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ updateCardViewContent();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ for (ScheduledRecording scheduledRecording : scheduledRecordings) {
+ if (scheduledRecording.getSeriesRecordingId() == mSeriesRecording.getId()) {
+ updateCardViewContent();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ boolean needToUpdateCardView = false;
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(),
+ mSeriesRecording.getSeriesId())) {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ needToUpdateCardView = true;
+ }
+ }
+ if (needToUpdateCardView) {
+ updateCardViewContent();
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ boolean needToUpdateCardView = false;
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (TextUtils.equals(recordedProgram.getSeriesId(),
+ mSeriesRecording.getSeriesId())) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.removeListener(this, recordedProgram.getId());
+ }
+ needToUpdateCardView = true;
+ }
+ }
+ if (needToUpdateCardView) {
+ updateCardViewContent();
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ // Do nothing
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ // Do nothing
+ }
+
+ public void onBound(SeriesRecording seriesRecording) {
+ mSeriesRecording = seriesRecording;
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addRecordedProgramListener(this);
+ for (RecordedProgram recordedProgram :
+ mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId())) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ mWatchedPositionManager.addListener(this, recordedProgram.getId());
+ }
+ }
+ updateCardViewContent();
+ }
+
+ public void onUnbound() {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ mWatchedPositionManager.removeListener(this);
+ }
+
+ private void updateCardViewContent() {
+ int count = 0;
+ int quantityStringID;
+ List<RecordedProgram> recordedPrograms =
+ mDvrDataManager.getRecordedPrograms(mSeriesRecording.getId());
+ if (recordedPrograms.size() == 0) {
+ count = mDvrManager.getAvailableScheduledRecording(mSeriesRecording.getId()).size();
+ quantityStringID = R.plurals.dvr_count_scheduled_recordings;
+ } else {
+ for (RecordedProgram recordedProgram : recordedPrograms) {
+ if (mWatchedPositionManager.getWatchedPosition(recordedProgram.getId())
+ == TvInputManager.TIME_SHIFT_INVALID_TIME) {
+ count++;
+ }
+ }
+ if (count == 0) {
+ count = recordedPrograms.size();
+ quantityStringID = R.plurals.dvr_count_recordings;
+ } else {
+ quantityStringID = R.plurals.dvr_count_new_recordings;
+ }
+ }
+ mCardView.setContent(mCardView.getResources()
+ .getQuantityString(quantityStringID, count, count), null);
+ }
+ }
+
+ public SeriesRecordingPresenter(Context context) {
+ ApplicationSingletons singletons = TvApplication.getSingletons(context);
+ mChannelDataManager = singletons.getChannelDataManager();
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrManager = singletons.getDvrManager();
+ mWatchedPositionManager = singletons.getDvrWatchedPositionManager();
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ Context context = parent.getContext();
+ RecordingCardView view = new RecordingCardView(context);
+ return new SeriesRecordingViewHolder(view, mDvrDataManager, mDvrManager,
+ mWatchedPositionManager);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder baseHolder, Object o) {
+ final SeriesRecordingViewHolder viewHolder = (SeriesRecordingViewHolder) baseHolder;
+ final SeriesRecording seriesRecording = (SeriesRecording) o;
+ final RecordingCardView cardView = (RecordingCardView) viewHolder.view;
+ viewHolder.onBound(seriesRecording);
+ setTitleAndImage(cardView, seriesRecording);
+ super.onBindViewHolder(baseHolder, o);
+ }
+
+ @Override
+ public void onUnbindViewHolder(ViewHolder viewHolder) {
+ ((RecordingCardView) viewHolder.view).reset();
+ ((SeriesRecordingViewHolder) viewHolder).onUnbound();
+ super.onUnbindViewHolder(viewHolder);
+ }
+
+ private void setTitleAndImage(RecordingCardView cardView, SeriesRecording recording) {
+ cardView.setTitle(recording.getTitle());
+ if (recording.getPosterUri() != null) {
+ cardView.setImageUri(recording.getPosterUri(), false);
+ } else {
+ Channel channel = mChannelDataManager.getChannel(recording.getChannelId());
+ String imageUri = null;
+ if (channel != null) {
+ imageUri = TvContract.buildChannelLogoUri(channel.getId()).toString();
+ }
+ cardView.setImageUri(imageUri, true);
+ }
+ }
+}