aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/android/tv/MainActivity.java43
-rw-r--r--src/com/android/tv/dvr/BaseDvrDataManager.java13
-rw-r--r--src/com/android/tv/dvr/DvrDataManagerImpl.java7
-rw-r--r--src/com/android/tv/dvr/WritableDvrDataManager.java8
-rw-r--r--src/com/android/tv/dvr/data/ScheduledRecording.java150
-rw-r--r--src/com/android/tv/dvr/provider/DvrContract.java53
-rw-r--r--src/com/android/tv/dvr/provider/DvrDatabaseHelper.java19
-rw-r--r--src/com/android/tv/dvr/recorder/InputTaskScheduler.java13
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingScheduler.java10
-rw-r--r--src/com/android/tv/dvr/recorder/RecordingTask.java61
-rw-r--r--src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java2
-rw-r--r--src/com/android/tv/dvr/ui/DvrUiHelper.java24
-rw-r--r--src/com/android/tv/dvr/ui/browse/DetailsContent.java33
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java80
-rw-r--r--src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java5
-rw-r--r--src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java4
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java16
-rw-r--r--src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java12
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java100
-rw-r--r--src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java237
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRow.java6
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java34
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java22
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java24
-rw-r--r--src/com/android/tv/menu/Menu.java44
-rw-r--r--src/com/android/tv/ui/ChannelBannerView.java61
-rw-r--r--src/com/android/tv/ui/TunableTvView.java36
-rw-r--r--src/com/android/tv/ui/TvOverlayManager.java11
-rw-r--r--src/com/android/tv/ui/hideable/AutoHideScheduler.java98
-rw-r--r--src/com/android/tv/ui/sidepanel/SideFragmentManager.java28
30 files changed, 1050 insertions, 204 deletions
diff --git a/src/com/android/tv/MainActivity.java b/src/com/android/tv/MainActivity.java
index b5c0b28d..94a86cce 100644
--- a/src/com/android/tv/MainActivity.java
+++ b/src/com/android/tv/MainActivity.java
@@ -436,6 +436,8 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
@Override
protected void onCreate(Bundle savedInstanceState) {
+ mAccessibilityManager =
+ (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
TvSingletons tvSingletons = TvSingletons.getSingletons(this);
mPerformanceMonitor = tvSingletons.getPerformanceMonitor();
TimerEvent timer = mPerformanceMonitor.startTimer();
@@ -486,6 +488,9 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
new OnUnhandledInputEventListener() {
@Override
public boolean onUnhandledInputEvent(InputEvent event) {
+ if (DEBUG) {
+ Log.d(TAG, "onUnhandledInputEvent " + event);
+ }
if (isKeyEventBlocked()) {
return true;
}
@@ -506,6 +511,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return false;
}
});
+ mTvView.setOnTalkBackDpadKeyListener(keycode -> handleUpDownKeys(keycode, null));
long channelId = Utils.getLastWatchedChannelId(this);
String inputId = Utils.getLastWatchedTunerInputId(this);
if (!isPassthroughInput
@@ -648,6 +654,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
selectInputView,
sceneContainer,
mSearchFragment);
+ mAccessibilityManager.addAccessibilityStateChangeListener(mOverlayManager);
mAudioManagerHelper = new AudioManagerHelper(this, mTvView);
Intent nowPlayingIntent = new Intent(this, MainActivity.class);
@@ -661,8 +668,6 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
return;
}
- mAccessibilityManager =
- (AccessibilityManager) getSystemService(Context.ACCESSIBILITY_SERVICE);
mSendConfigInfoRecurringRunner =
new RecurringRunner(
this,
@@ -2044,6 +2049,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
}
}
if (mOverlayManager != null) {
+ mAccessibilityManager.removeAccessibilityStateChangeListener(mOverlayManager);
mOverlayManager.release();
}
mMemoryManageables.clear();
@@ -2094,32 +2100,43 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
if (!mChannelTuner.areAllChannelsLoaded()) {
return false;
}
+ if (handleUpDownKeys(keyCode, event)) {
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private boolean handleUpDownKeys(int keyCode, @Nullable KeyEvent event) {
if (!mChannelTuner.isCurrentChannelPassthrough()) {
switch (keyCode) {
case KeyEvent.KEYCODE_CHANNEL_UP:
case KeyEvent.KEYCODE_DPAD_UP:
- if (event.getRepeatCount() == 0
+ if ((event == null || event.getRepeatCount() == 0)
&& mChannelTuner.getBrowsableChannelCount() > 0) {
// message sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ if (event != null) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_CHANNEL_UP_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ }
moveToAdjacentChannel(true, false);
mTracker.sendChannelUp();
}
return true;
case KeyEvent.KEYCODE_CHANNEL_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN:
- if (event.getRepeatCount() == 0
+ if ((event == null || event.getRepeatCount() == 0)
&& mChannelTuner.getBrowsableChannelCount() > 0) {
// message sending should be done before moving channel, because we use the
// existence of message to decide if users are switching channel.
- mHandler.sendMessageDelayed(
- mHandler.obtainMessage(
- MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
- CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ if (event != null) {
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(
+ MSG_CHANNEL_DOWN_PRESSED, System.currentTimeMillis()),
+ CHANNEL_CHANGE_INITIAL_DELAY_MILLIS);
+ }
moveToAdjacentChannel(false, false);
mTracker.sendChannelDown();
}
@@ -2127,7 +2144,7 @@ public class MainActivity extends Activity implements OnActionClickListener, OnP
default: // fall out
}
}
- return super.onKeyDown(keyCode, event);
+ return false;
}
@Override
diff --git a/src/com/android/tv/dvr/BaseDvrDataManager.java b/src/com/android/tv/dvr/BaseDvrDataManager.java
index 4a04de4e..b8bffa18 100644
--- a/src/com/android/tv/dvr/BaseDvrDataManager.java
+++ b/src/com/android/tv/dvr/BaseDvrDataManager.java
@@ -258,6 +258,19 @@ public abstract class BaseDvrDataManager implements WritableDvrDataManager {
}
@Override
+ public void changeState(
+ ScheduledRecording scheduledRecording, @RecordingState int newState, int reason) {
+ if (scheduledRecording.getState() != newState) {
+ ScheduledRecording.Builder builder =
+ ScheduledRecording.buildFrom(scheduledRecording).setState(newState);
+ if (newState == ScheduledRecording.STATE_RECORDING_FAILED) {
+ builder.setFailedReason(reason);
+ }
+ updateScheduledRecording(builder.build());
+ }
+ }
+
+ @Override
public Collection<ScheduledRecording> getDeletedSchedules() {
return mDeletedScheduleMap.values();
}
diff --git a/src/com/android/tv/dvr/DvrDataManagerImpl.java b/src/com/android/tv/dvr/DvrDataManagerImpl.java
index c74aa208..2b4ecbf5 100644
--- a/src/com/android/tv/dvr/DvrDataManagerImpl.java
+++ b/src/com/android/tv/dvr/DvrDataManagerImpl.java
@@ -228,6 +228,9 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
protected void onPostExecute(List<ScheduledRecording> result) {
mPendingTasks.remove(this);
long maxId = 0;
+ int reasonNotStarted =
+ ScheduledRecording
+ .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
List<ScheduledRecording> toUpdate = new ArrayList<>();
List<ScheduledRecording> toDelete = new ArrayList<>();
for (ScheduledRecording r : result) {
@@ -244,11 +247,14 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
switch (r.getState()) {
case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
+ int reason =
+ ScheduledRecording.FAILED_REASON_NOT_FINISHED;
toUpdate.add(
ScheduledRecording.buildFrom(r)
.setState(
ScheduledRecording
.STATE_RECORDING_FAILED)
+ .setFailedReason(reason)
.build());
} else {
toUpdate.add(
@@ -266,6 +272,7 @@ public class DvrDataManagerImpl extends BaseDvrDataManager {
.setState(
ScheduledRecording
.STATE_RECORDING_FAILED)
+ .setFailedReason(reasonNotStarted)
.build());
}
break;
diff --git a/src/com/android/tv/dvr/WritableDvrDataManager.java b/src/com/android/tv/dvr/WritableDvrDataManager.java
index 059b0a6d..1b505e80 100644
--- a/src/com/android/tv/dvr/WritableDvrDataManager.java
+++ b/src/com/android/tv/dvr/WritableDvrDataManager.java
@@ -57,6 +57,14 @@ public interface WritableDvrDataManager extends DvrDataManager {
void changeState(ScheduledRecording scheduledRecording, @RecordingState int newState);
/**
+ * Changes the state of the recording.
+ *
+ * @param reason the reason of this change
+ */
+ void changeState(
+ ScheduledRecording scheduledRecording, @RecordingState int newState, int reason);
+
+ /**
* Remove all the records related to the input.
*
* <p>Note that this should be called after the input was removed.
diff --git a/src/com/android/tv/dvr/data/ScheduledRecording.java b/src/com/android/tv/dvr/data/ScheduledRecording.java
index bc569d96..7c2d12d9 100644
--- a/src/com/android/tv/dvr/data/ScheduledRecording.java
+++ b/src/com/android/tv/dvr/data/ScheduledRecording.java
@@ -24,6 +24,7 @@ import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Range;
import com.android.tv.R;
@@ -144,7 +145,8 @@ public final class ScheduledRecording implements Parcelable {
.setProgramLongDescription(p.getLongDescription())
.setProgramPosterArtUri(p.getPosterArtUri())
.setProgramThumbnailUri(p.getThumbnailUri())
- .setState(STATE_RECORDING_FINISHED);
+ .setState(STATE_RECORDING_FINISHED)
+ .setRecordedProgramId(p.getId());
}
public static final class Builder {
@@ -166,6 +168,8 @@ public final class ScheduledRecording implements Parcelable {
private String mProgramThumbnailUri;
private @RecordingState int mState;
private long mSeriesRecordingId = ID_NOT_SET;
+ private Long mRecodedProgramId;
+ private Integer mFailedReason;
private Builder() {}
@@ -259,6 +263,16 @@ public final class ScheduledRecording implements Parcelable {
return this;
}
+ public Builder setRecordedProgramId(Long recordedProgramId) {
+ mRecodedProgramId = recordedProgramId;
+ return this;
+ }
+
+ public Builder setFailedReason(Integer reason) {
+ mFailedReason = reason;
+ return this;
+ }
+
public ScheduledRecording build() {
return new ScheduledRecording(
mId,
@@ -278,7 +292,9 @@ public final class ScheduledRecording implements Parcelable {
mProgramPosterArtUri,
mProgramThumbnailUri,
mState,
- mSeriesRecordingId);
+ mSeriesRecordingId,
+ mRecodedProgramId,
+ mFailedReason);
}
}
@@ -302,6 +318,7 @@ public final class ScheduledRecording implements Parcelable {
.setProgramPosterArtUri(orig.getProgramPosterArtUri())
.setProgramThumbnailUri(orig.getProgramThumbnailUri())
.setState(orig.mState)
+ .setFailedReason(orig.getFailedReason())
.setType(orig.mType);
}
@@ -325,6 +342,36 @@ public final class ScheduledRecording implements Parcelable {
public static final int STATE_RECORDING_DELETED = 5;
public static final int STATE_RECORDING_CANCELED = 6;
+ /** The reasons of failed recordings */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FAILED_REASON_OTHER,
+ FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED,
+ FAILED_REASON_NOT_FINISHED,
+ FAILED_REASON_SCHEDULER_STOPPED,
+ FAILED_REASON_INVALID_CHANNEL,
+ FAILED_REASON_MESSAGE_NOT_SENT,
+ FAILED_REASON_CONNECTION_FAILED,
+ FAILED_REASON_RESOURCE_BUSY,
+ FAILED_REASON_INPUT_UNAVAILABLE,
+ FAILED_REASON_INPUT_DVR_UNSUPPORTED,
+ FAILED_REASON_INSUFFICIENT_SPACE
+ })
+ public @interface RecordingFailedReason {}
+
+ public static final int FAILED_REASON_OTHER = 0;
+ public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
+ public static final int FAILED_REASON_NOT_FINISHED = 2;
+ public static final int FAILED_REASON_SCHEDULER_STOPPED = 3;
+ public static final int FAILED_REASON_INVALID_CHANNEL = 4;
+ public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5;
+ public static final int FAILED_REASON_CONNECTION_FAILED = 6;
+ public static final int FAILED_REASON_RESOURCE_BUSY = 7;
+ // For the following reasons, show advice to users
+ public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8;
+ public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9;
+ public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10;
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_TIMED, TYPE_PROGRAM})
public @interface RecordingType {}
@@ -358,6 +405,7 @@ public final class ScheduledRecording implements Parcelable {
Schedules.COLUMN_PROGRAM_POST_ART_URI,
Schedules.COLUMN_PROGRAM_THUMBNAIL_URI,
Schedules.COLUMN_STATE,
+ Schedules.COLUMN_FAILED_REASON,
Schedules.COLUMN_SERIES_RECORDING_ID
};
@@ -382,6 +430,7 @@ public final class ScheduledRecording implements Parcelable {
.setProgramPosterArtUri(c.getString(++index))
.setProgramThumbnailUri(c.getString(++index))
.setState(recordingState(c.getString(++index)))
+ .setFailedReason(recordingFailedReason(c.getString(++index)))
.setSeriesRecordingId(c.getLong(++index))
.build();
}
@@ -406,6 +455,7 @@ public final class ScheduledRecording implements Parcelable {
values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri());
values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri());
values.put(Schedules.COLUMN_STATE, recordingState(r.getState()));
+ values.put(Schedules.COLUMN_FAILED_REASON, recordingFailedReason(r.getFailedReason()));
values.put(Schedules.COLUMN_TYPE, recordingType(r.getType()));
if (r.getSeriesRecordingId() != ID_NOT_SET) {
values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId());
@@ -434,6 +484,7 @@ public final class ScheduledRecording implements Parcelable {
.setProgramPosterArtUri(in.readString())
.setProgramThumbnailUri(in.readString())
.setState(in.readInt())
+ .setFailedReason(recordingFailedReason(in.readString()))
.setSeriesRecordingId(in.readLong())
.build();
}
@@ -480,6 +531,8 @@ public final class ScheduledRecording implements Parcelable {
private final String mProgramThumbnailUri;
@RecordingState private final int mState;
private final long mSeriesRecordingId;
+ private final Long mRecordedProgramId;
+ private final Integer mFailedReason;
private ScheduledRecording(
long id,
@@ -499,7 +552,9 @@ public final class ScheduledRecording implements Parcelable {
String programPosterArtUri,
String programThumbnailUri,
@RecordingState int state,
- long seriesRecordingId) {
+ long seriesRecordingId,
+ Long recordedProgramId,
+ Integer failedReason) {
mId = id;
mPriority = priority;
mInputId = inputId;
@@ -518,6 +573,8 @@ public final class ScheduledRecording implements Parcelable {
mProgramThumbnailUri = programThumbnailUri;
mState = state;
mSeriesRecordingId = seriesRecordingId;
+ mRecordedProgramId = recordedProgramId;
+ mFailedReason = failedReason;
}
/**
@@ -615,6 +672,18 @@ public final class ScheduledRecording implements Parcelable {
return mSeriesRecordingId;
}
+ /** Returns the ID of the corresponding {@link RecordedProgram}. */
+ @Nullable
+ public Long getRecordedProgramId() {
+ return mRecordedProgramId;
+ }
+
+ /** Returns the failed reason of the {@link ScheduledRecording}. */
+ @Nullable @RecordingFailedReason
+ public Integer getFailedReason() {
+ return mFailedReason;
+ }
+
public long getId() {
return mId;
}
@@ -743,6 +812,76 @@ public final class ScheduledRecording implements Parcelable {
}
}
+ /**
+ * Converts a string to a failed reason integer, defaulting to {@link
+ * #FAILED_REASON_OTHER}.
+ */
+ private static Integer recordingFailedReason(String reason) {
+ if (TextUtils.isEmpty(reason)) {
+ return null;
+ }
+ switch (reason) {
+ case Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
+ case Schedules.FAILED_REASON_NOT_FINISHED:
+ return FAILED_REASON_NOT_FINISHED;
+ case Schedules.FAILED_REASON_SCHEDULER_STOPPED:
+ return FAILED_REASON_SCHEDULER_STOPPED;
+ case Schedules.FAILED_REASON_INVALID_CHANNEL:
+ return FAILED_REASON_INVALID_CHANNEL;
+ case Schedules.FAILED_REASON_MESSAGE_NOT_SENT:
+ return FAILED_REASON_MESSAGE_NOT_SENT;
+ case Schedules.FAILED_REASON_CONNECTION_FAILED:
+ return FAILED_REASON_CONNECTION_FAILED;
+ case Schedules.FAILED_REASON_RESOURCE_BUSY:
+ return FAILED_REASON_RESOURCE_BUSY;
+ case Schedules.FAILED_REASON_INPUT_UNAVAILABLE:
+ return FAILED_REASON_INPUT_UNAVAILABLE;
+ case Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return FAILED_REASON_INPUT_DVR_UNSUPPORTED;
+ case Schedules.FAILED_REASON_INSUFFICIENT_SPACE:
+ return FAILED_REASON_INSUFFICIENT_SPACE;
+ case Schedules.FAILED_REASON_OTHER:
+ default:
+ return FAILED_REASON_OTHER;
+ }
+ }
+
+ /**
+ * Converts a failed reason integer to string, defaulting to {@link
+ * Schedules#FAILED_REASON_OTHER}.
+ */
+ private static String recordingFailedReason(Integer reason) {
+ if (reason == null) {
+ return null;
+ }
+ switch (reason) {
+ case FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
+ return Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
+ case FAILED_REASON_NOT_FINISHED:
+ return Schedules.FAILED_REASON_NOT_FINISHED;
+ case FAILED_REASON_SCHEDULER_STOPPED:
+ return Schedules.FAILED_REASON_SCHEDULER_STOPPED;
+ case FAILED_REASON_INVALID_CHANNEL:
+ return Schedules.FAILED_REASON_INVALID_CHANNEL;
+ case FAILED_REASON_MESSAGE_NOT_SENT:
+ return Schedules.FAILED_REASON_MESSAGE_NOT_SENT;
+ case FAILED_REASON_CONNECTION_FAILED:
+ return Schedules.FAILED_REASON_CONNECTION_FAILED;
+ case FAILED_REASON_RESOURCE_BUSY:
+ return Schedules.FAILED_REASON_RESOURCE_BUSY;
+ case FAILED_REASON_INPUT_UNAVAILABLE:
+ return Schedules.FAILED_REASON_INPUT_UNAVAILABLE;
+ case FAILED_REASON_INPUT_DVR_UNSUPPORTED:
+ return Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED;
+ case FAILED_REASON_INSUFFICIENT_SPACE:
+ return Schedules.FAILED_REASON_INSUFFICIENT_SPACE;
+ case FAILED_REASON_OTHER: // fall through
+ default:
+ return Schedules.FAILED_REASON_OTHER;
+ }
+ }
+
/** Checks if the {@code period} overlaps with the recording time. */
public boolean isOverLapping(Range<Long> period) {
return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower();
@@ -794,6 +933,8 @@ public final class ScheduledRecording implements Parcelable {
+ mProgramThumbnailUri
+ ",state="
+ mState
+ + ",failedReason="
+ + mFailedReason
+ ",priority="
+ mPriority
+ ",seriesRecordingId="
@@ -825,6 +966,7 @@ public final class ScheduledRecording implements Parcelable {
out.writeString(mProgramPosterArtUri);
out.writeString(mProgramThumbnailUri);
out.writeInt(mState);
+ out.writeString(recordingFailedReason(mFailedReason));
out.writeLong(mSeriesRecordingId);
}
@@ -865,6 +1007,7 @@ public final class ScheduledRecording implements Parcelable {
&& Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri())
&& Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri())
&& mState == r.mState
+ && Objects.equals(mFailedReason, r.mFailedReason)
&& mSeriesRecordingId == r.mSeriesRecordingId;
}
@@ -887,6 +1030,7 @@ public final class ScheduledRecording implements Parcelable {
mProgramPosterArtUri,
mProgramThumbnailUri,
mState,
+ mFailedReason,
mSeriesRecordingId);
}
diff --git a/src/com/android/tv/dvr/provider/DvrContract.java b/src/com/android/tv/dvr/provider/DvrContract.java
index f956ef0b..a5f2e2cd 100644
--- a/src/com/android/tv/dvr/provider/DvrContract.java
+++ b/src/com/android/tv/dvr/provider/DvrContract.java
@@ -55,6 +55,52 @@ public final class DvrContract {
/** The recording marked as canceled. */
public static final String STATE_RECORDING_CANCELED = "STATE_RECORDING_CANCELED";
+ /** The recording failed reason for other reasons */
+ public static final String FAILED_REASON_OTHER = "FAILED_REASON_OTHER";
+
+ /** The recording failed because the program ended before recording started. */
+ public static final String FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED =
+ "FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED";
+
+ /** The recording failed because it was not finished successfully */
+ public static final String FAILED_REASON_NOT_FINISHED = "FAILED_REASON_NOT_FINISHED";
+
+ /** The recording failed because the channel ID was invalid */
+ public static final String FAILED_REASON_INVALID_CHANNEL = "FAILED_REASON_INVALID_CHANNEL";
+
+ /** The recording failed because the scheduler was stopped */
+ public static final String FAILED_REASON_SCHEDULER_STOPPED
+ = "FAILED_REASON_SCHEDULER_STOPPED";
+
+ /** The recording failed because some messages were not sent to the message queue */
+ public static final String FAILED_REASON_MESSAGE_NOT_SENT =
+ "FAILED_REASON_MESSAGE_NOT_SENT";
+
+ /**
+ * The recording failed because it was failed to establish a connection to the recording
+ * session for the corresponding TV input.
+ */
+ public static final String FAILED_REASON_CONNECTION_FAILED =
+ "FAILED_REASON_CONNECTION_FAILED";
+
+ /**
+ * The recording failed because a required recording resource was not able to be
+ * allocated.
+ */
+ public static final String FAILED_REASON_RESOURCE_BUSY = "FAILED_REASON_RESOURCE_BUSY";
+
+ /** The recording failed because the input was not available */
+ public static final String FAILED_REASON_INPUT_UNAVAILABLE =
+ "FAILED_REASON_INPUT_UNAVAILABLE";
+
+ /** The recording failed because the input doesn't support recording */
+ public static final String FAILED_REASON_INPUT_DVR_UNSUPPORTED =
+ "FAILED_REASON_INPUT_DVR_UNSUPPORTED";
+
+ /** The recording failed because the space was not sufficient */
+ public static final String FAILED_REASON_INSUFFICIENT_SPACE =
+ "FAILED_REASON_INSUFFICIENT_SPACE";
+
/**
* The priority of this recording.
*
@@ -195,6 +241,13 @@ public final class DvrContract {
public static final String COLUMN_STATE = "state";
/**
+ * The reason of failure of this recording if it's failed.
+ *
+ * <p>Type: TEXT
+ */
+ public static final String COLUMN_FAILED_REASON = "failed_reason";
+
+ /**
* The ID of the parent series recording.
*
* <p>Type: INTEGER (long)
diff --git a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
index 0fb96d1b..41e5a66a 100644
--- a/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
+++ b/src/com/android/tv/dvr/provider/DvrDatabaseHelper.java
@@ -36,7 +36,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "DvrDatabaseHelper";
private static final boolean DEBUG = false;
- private static final int DATABASE_VERSION = 17;
+ private static final int DATABASE_VERSION = 18;
private static final String DB_NAME = "dvr.db";
private static final String SQL_CREATE_SCHEDULES =
@@ -162,6 +162,7 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
new ColumnInfo(Schedules.COLUMN_PROGRAM_POST_ART_URI, SQL_DATA_TYPE_STRING),
new ColumnInfo(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, SQL_DATA_TYPE_STRING),
new ColumnInfo(Schedules.COLUMN_STATE, SQL_DATA_TYPE_STRING),
+ new ColumnInfo(Schedules.COLUMN_FAILED_REASON, SQL_DATA_TYPE_STRING),
new ColumnInfo(Schedules.COLUMN_SERIES_RECORDING_ID, SQL_DATA_TYPE_LONG)
};
@@ -254,11 +255,17 @@ public class DvrDatabaseHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES);
- db.execSQL(SQL_DROP_SCHEDULES);
- if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
- db.execSQL(SQL_DROP_SERIES_RECORDINGS);
- onCreate(db);
+ if (oldVersion < 17) {
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SCHEDULES);
+ db.execSQL(SQL_DROP_SCHEDULES);
+ if (DEBUG) Log.d(TAG, "Executing SQL: " + SQL_DROP_SERIES_RECORDINGS);
+ db.execSQL(SQL_DROP_SERIES_RECORDINGS);
+ onCreate(db);
+ }
+ if (oldVersion < 18) {
+ db.execSQL("ALTER TABLE " + Schedules.TABLE_NAME + " ADD COLUMN "
+ + Schedules.COLUMN_FAILED_REASON + " TEXT DEFAULT null;");
+ }
}
/** Handles the query request and returns a {@link Cursor}. */
diff --git a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
index 0f1ea3b0..1021b2bc 100644
--- a/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
+++ b/src/com/android/tv/dvr/recorder/InputTaskScheduler.java
@@ -278,7 +278,9 @@ public class InputTaskScheduler {
ScheduledRecording schedule = iter.next();
if (schedule.getEndTimeMs() - currentTimeMs
<= MIN_REMAIN_DURATION_PERCENT * schedule.getDuration()) {
- fail(schedule);
+ Log.e(TAG, "Error! Program ended before recording started:" + schedule);
+ fail(schedule,
+ ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
iter.remove();
}
}
@@ -389,7 +391,7 @@ public class InputTaskScheduler {
return candidate;
}
- private void fail(ScheduledRecording schedule) {
+ private void fail(ScheduledRecording schedule, int reason) {
// It's called when the scheduling has been failed without creating RecordingTask.
runOnMainHandler(
new Runnable() {
@@ -399,10 +401,11 @@ public class InputTaskScheduler {
mDataManager.getScheduledRecording(schedule.getId());
if (scheduleInManager != null) {
// The schedule should be updated based on the object from DataManager
- // in case
- // when it has been updated.
+ // in case when it has been updated.
mDataManager.changeState(
- scheduleInManager, ScheduledRecording.STATE_RECORDING_FAILED);
+ scheduleInManager,
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ reason);
}
}
});
diff --git a/src/com/android/tv/dvr/recorder/RecordingScheduler.java b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
index d631d84f..f309537d 100644
--- a/src/com/android/tv/dvr/recorder/RecordingScheduler.java
+++ b/src/com/android/tv/dvr/recorder/RecordingScheduler.java
@@ -280,12 +280,18 @@ public class RecordingScheduler extends TvInputCallback implements ScheduledReco
TvInputInfo input = Utils.getTvInputInfoForInputId(mContext, schedule.getInputId());
if (input == null) {
Log.e(TAG, "Can't find input for " + schedule);
- mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
+ mDataManager.changeState(
+ schedule,
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE);
return;
}
if (!input.canRecord() || input.getTunerCount() <= 0) {
Log.e(TAG, "TV input doesn't support recording: " + input);
- mDataManager.changeState(schedule, ScheduledRecording.STATE_RECORDING_FAILED);
+ mDataManager.changeState(
+ schedule,
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED);
return;
}
InputTaskScheduler inputTaskScheduler = mInputSchedulerMap.get(input.getId());
diff --git a/src/com/android/tv/dvr/recorder/RecordingTask.java b/src/com/android/tv/dvr/recorder/RecordingTask.java
index ff37f3f0..07a29e51 100644
--- a/src/com/android/tv/dvr/recorder/RecordingTask.java
+++ b/src/com/android/tv/dvr/recorder/RecordingTask.java
@@ -26,6 +26,7 @@ import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.util.Log;
@@ -194,7 +195,7 @@ public class RecordingTask extends RecordingCallback
public void onDisconnected(String inputId) {
if (DEBUG) Log.d(TAG, "onDisconnected(" + inputId + ")");
if (mRecordingSession != null && mState != State.FINISHED) {
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_NOT_FINISHED);
}
}
@@ -202,7 +203,7 @@ public class RecordingTask extends RecordingCallback
public void onConnectionFailed(String inputId) {
if (DEBUG) Log.d(TAG, "onConnectionFailed(" + inputId + ")");
if (mRecordingSession != null) {
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_CONNECTION_FAILED);
}
}
@@ -217,7 +218,7 @@ public class RecordingTask extends RecordingCallback
|| !sendEmptyMessageAtAbsoluteTime(
MSG_START_RECORDING,
mScheduledRecording.getStartTimeMs() - RECORDING_EARLY_START_OFFSET_MS)) {
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT);
}
}
@@ -249,6 +250,7 @@ public class RecordingTask extends RecordingCallback
if (mRecordingSession == null) {
return;
}
+ int error;
switch (reason) {
case TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE:
Log.i(TAG, "Insufficient space to record " + mScheduledRecording);
@@ -284,23 +286,28 @@ public class RecordingTask extends RecordingCallback
}
}
});
- // fall through
+ error = ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE;
+ break;
+ case TvInputManager.RECORDING_ERROR_RESOURCE_BUSY:
+ error = ScheduledRecording.FAILED_REASON_RESOURCE_BUSY;
+ break;
default:
- failAndQuit();
+ error = ScheduledRecording.FAILED_REASON_OTHER;
break;
}
+ failAndQuit(error);
}
private void handleInit() {
if (DEBUG) Log.d(TAG, "handleInit " + mScheduledRecording);
if (mScheduledRecording.getEndTimeMs() < mClock.currentTimeMillis()) {
Log.w(TAG, "End time already past, not recording " + mScheduledRecording);
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED);
return;
}
if (mChannel == null) {
Log.w(TAG, "Null channel for " + mScheduledRecording);
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_INVALID_CHANNEL);
return;
}
if (mChannel.getId() != mScheduledRecording.getChannelId()) {
@@ -310,7 +317,7 @@ public class RecordingTask extends RecordingCallback
+ mChannel
+ " does not match scheduled recording "
+ mScheduledRecording);
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_INVALID_CHANNEL);
return;
}
@@ -329,8 +336,14 @@ public class RecordingTask extends RecordingCallback
}
private void failAndQuit() {
+ failAndQuit(ScheduledRecording.FAILED_REASON_OTHER);
+ }
+
+ private void failAndQuit(Integer reason) {
if (DEBUG) Log.d(TAG, "failAndQuit");
- updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
+ updateRecordingState(
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ reason);
mState = State.ERROR;
sendRemove();
}
@@ -360,7 +373,7 @@ public class RecordingTask extends RecordingCallback
if (!sendEmptyMessageAtAbsoluteTime(
MSG_STOP_RECORDING, mScheduledRecording.getEndTimeMs())) {
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT);
}
}
@@ -380,7 +393,7 @@ public class RecordingTask extends RecordingCallback
if (mState == State.RECORDING_STARTED) {
mHandler.removeMessages(MSG_STOP_RECORDING);
if (!sendEmptyMessageAtAbsoluteTime(MSG_STOP_RECORDING, schedule.getEndTimeMs())) {
- failAndQuit();
+ failAndQuit(ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT);
}
}
}
@@ -435,7 +448,13 @@ public class RecordingTask extends RecordingCallback
}
private void updateRecordingState(@ScheduledRecording.RecordingState int state) {
- if (DEBUG) Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state);
+ updateRecordingState(state, null);
+ }
+ private void updateRecordingState(
+ @ScheduledRecording.RecordingState int state, @Nullable Integer reason) {
+ if (DEBUG) {
+ Log.d(TAG, "Updating the state of " + mScheduledRecording + " to " + state);
+ }
mScheduledRecording =
ScheduledRecording.buildFrom(mScheduledRecording).setState(state).build();
runOnMainThread(
@@ -449,11 +468,17 @@ public class RecordingTask extends RecordingCallback
removeRecordedProgram();
} else {
// Update the state based on the object in DataManager in case when it
- // has been
- // updated. mScheduledRecording will be updated from
+ // has been updated. mScheduledRecording will be updated from
// onScheduledRecordingStateChanged.
- mDataManager.updateScheduledRecording(
- ScheduledRecording.buildFrom(schedule).setState(state).build());
+ ScheduledRecording.Builder builder =
+ ScheduledRecording
+ .buildFrom(schedule)
+ .setState(state);
+ if (state == ScheduledRecording.STATE_RECORDING_FAILED
+ && reason != null) {
+ builder.setFailedReason(reason);
+ }
+ mDataManager.updateScheduledRecording(builder.build());
}
}
});
@@ -507,7 +532,9 @@ public class RecordingTask extends RecordingCallback
/** Clean up the task. */
public void cleanUp() {
if (mState == State.RECORDING_STARTED || mState == State.RECORDING_STOP_REQUESTED) {
- updateRecordingState(ScheduledRecording.STATE_RECORDING_FAILED);
+ updateRecordingState(
+ ScheduledRecording.STATE_RECORDING_FAILED,
+ ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED);
}
release();
if (mHandler != null) {
diff --git a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
index 37efa5ba..eadb3b9e 100644
--- a/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
+++ b/src/com/android/tv/dvr/ui/DvrSeriesSettingsFragment.java
@@ -356,7 +356,7 @@ public class DvrSeriesSettingsFragment extends GuidedStepFragment
}
private void showConfirmDialog() {
- DvrUiHelper.StartSeriesScheduledDialogActivity(
+ DvrUiHelper.startSeriesScheduledDialogActivity(
getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog, mPrograms);
finishGuidedStepFragments();
}
diff --git a/src/com/android/tv/dvr/ui/DvrUiHelper.java b/src/com/android/tv/dvr/ui/DvrUiHelper.java
index e5786895..16afbdef 100644
--- a/src/com/android/tv/dvr/ui/DvrUiHelper.java
+++ b/src/com/android/tv/dvr/ui/DvrUiHelper.java
@@ -545,7 +545,7 @@ public class DvrUiHelper {
}
/** Shows "series recording scheduled" dialog activity. */
- public static void StartSeriesScheduledDialogActivity(
+ public static void startSeriesScheduledDialogActivity(
Context context,
SeriesRecording seriesRecording,
boolean showViewScheduleOptionInDialog,
@@ -587,6 +587,17 @@ public class DvrUiHelper {
viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
} else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
viewType = DvrDetailsActivity.CURRENT_RECORDING_VIEW;
+ } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
+ && schedule.getRecordedProgramId() != null) {
+ recordingId = schedule.getRecordedProgramId();
+ viewType = DvrDetailsActivity.RECORDED_PROGRAM_VIEW;
+ } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ viewType = DvrDetailsActivity.SCHEDULED_RECORDING_VIEW;
+ hideViewSchedule = true;
+ // TODO(b/72638385): pass detailed error message
+ intent.putExtra(
+ DvrDetailsActivity.EXTRA_FAILED_MESSAGE,
+ activity.getString(R.string.dvr_recording_failed));
} else {
return;
}
@@ -681,13 +692,10 @@ public class DvrUiHelper {
builder =
TextUtils.isEmpty(episodeNumber)
? new SpannableStringBuilder(title)
- : new SpannableStringBuilder(
- Html.fromHtml(
- context.getString(
- R.string
- .program_title_with_episode_number_no_season,
- title,
- episodeNumber)));
+ : new SpannableStringBuilder(Html.fromHtml(context.getString(
+ R.string.program_title_with_episode_number_no_season,
+ title,
+ episodeNumber)));
} else {
builder =
new SpannableStringBuilder(
diff --git a/src/com/android/tv/dvr/ui/browse/DetailsContent.java b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
index 56bbdb46..cba6293b 100644
--- a/src/com/android/tv/dvr/ui/browse/DetailsContent.java
+++ b/src/com/android/tv/dvr/ui/browse/DetailsContent.java
@@ -99,6 +99,39 @@ class DetailsContent {
.build(context);
}
+ static DetailsContent createFromFailedScheduledRecording(
+ Context context, ScheduledRecording scheduledRecording, String errMsg) {
+ Channel channel =
+ TvSingletons.getSingletons(context)
+ .getChannelDataManager()
+ .getChannel(scheduledRecording.getChannelId());
+ String description;
+ if (scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED
+ && errMsg != null) {
+ description = errMsg
+ + " (Error code: " + scheduledRecording.getFailedReason() + ")";
+ } else {
+ description =
+ !TextUtils.isEmpty(scheduledRecording.getProgramDescription())
+ ? scheduledRecording.getProgramDescription()
+ : scheduledRecording.getProgramLongDescription();
+ }
+ if (TextUtils.isEmpty(description)) {
+ description = channel != null ? channel.getDescription() : null;
+ }
+ return new DetailsContent.Builder()
+ .setChannelId(scheduledRecording.getChannelId())
+ .setProgramTitle(scheduledRecording.getProgramTitle())
+ .setSeasonNumber(scheduledRecording.getSeasonNumber())
+ .setEpisodeNumber(scheduledRecording.getEpisodeNumber())
+ .setStartTimeUtcMillis(scheduledRecording.getStartTimeMs())
+ .setEndTimeUtcMillis(scheduledRecording.getEndTimeMs())
+ .setDescription(description)
+ .setPosterArtUri(scheduledRecording.getProgramPosterArtUri())
+ .setThumbnailUri(scheduledRecording.getProgramThumbnailUri())
+ .build(context);
+ }
+
private DetailsContent() {}
/** Returns title. */
diff --git a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
index 6d66eea1..40b3a1f0 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrBrowseFragment.java
@@ -31,6 +31,7 @@ import android.support.v17.leanback.widget.TitleViewAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
+
import com.android.tv.R;
import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
@@ -46,6 +47,7 @@ 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;
@@ -70,7 +72,7 @@ public class DvrBrowseFragment extends BrowseFragment
private boolean mShouldShowScheduleRow;
private boolean mEntranceTransitionEnded;
- private RecordedProgramAdapter mRecentAdapter;
+ private RecentRowAdapter mRecentAdapter;
private ScheduleAdapter mScheduleAdapter;
private SeriesAdapter mSeriesAdapter;
private RecordedProgramAdapter[] mGenreAdapters =
@@ -146,6 +148,52 @@ public class DvrBrowseFragment extends BrowseFragment
}
};
+ static final Comparator<Object> RECENT_ROW_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
+ .reversed()
+ .compare((ScheduledRecording) lhs, (ScheduledRecording) rhs);
+ } else if (rhs instanceof RecordedProgram) {
+ ScheduledRecording scheduled = (ScheduledRecording) lhs;
+ RecordedProgram recorded = (RecordedProgram) rhs;
+ int compare =
+ Long.compare(
+ recorded.getStartTimeUtcMillis(),
+ scheduled.getStartTimeMs());
+ // recorded program first when the start times are the same
+ return compare == 0 ? 1 : compare;
+ } else {
+ return -1;
+ }
+ } else if (lhs instanceof RecordedProgram) {
+ if (rhs instanceof RecordedProgram) {
+ return RecordedProgram.START_TIME_THEN_ID_COMPARATOR
+ .reversed()
+ .compare((RecordedProgram) lhs, (RecordedProgram) rhs);
+ } else if (rhs instanceof ScheduledRecording) {
+ RecordedProgram recorded = (RecordedProgram) lhs;
+ ScheduledRecording scheduled = (ScheduledRecording) rhs;
+ int compare =
+ Long.compare(
+ scheduled.getStartTimeMs(),
+ recorded.getStartTimeUtcMillis());
+ // recorded program first when the start times are the same
+ return compare == 0 ? -1 : compare;
+ } else {
+ return -1;
+ }
+ } else {
+ return !(rhs instanceof RecordedProgram)
+ && !(rhs instanceof ScheduledRecording)
+ ? 0 : 1;
+ }
+ }
+ };
+
private final DvrScheduleManager.OnConflictStateChangeListener mOnConflictStateChangeListener =
new DvrScheduleManager.OnConflictStateChangeListener() {
@Override
@@ -282,6 +330,8 @@ public class DvrBrowseFragment extends BrowseFragment
for (ScheduledRecording scheduleRecording : scheduledRecordings) {
if (needToShowScheduledRecording(scheduleRecording)) {
mScheduleAdapter.add(scheduleRecording);
+ } else if (scheduleRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ mRecentAdapter.add(scheduleRecording);
}
}
}
@@ -380,14 +430,15 @@ public class DvrBrowseFragment extends BrowseFragment
private boolean startBrowseIfDvrInitialized() {
if (mDvrDataManager.isInitialized()) {
// Setup rows
- mRecentAdapter = new RecordedProgramAdapter(MAX_RECENT_ITEM_COUNT);
+ mRecentAdapter = new RecentRowAdapter(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();
+ // only get not started or in progress recordings
+ List<ScheduledRecording> schedules = mDvrDataManager.getAvailableScheduledRecordings();
onScheduledRecordingAdded(ScheduledRecording.toArray(schedules));
mScheduleAdapter.addExtraItem(FullScheduleCardHolder.FULL_SCHEDULE_CARD_HOLDER);
// Recorded Programs.
@@ -395,6 +446,11 @@ public class DvrBrowseFragment extends BrowseFragment
handleRecordedProgramAdded(recordedProgram, false);
}
if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) {
+ // only get failed recordings
+ for (ScheduledRecording scheduledRecording
+ : mDvrDataManager.getFailedScheduledRecordings()) {
+ onScheduledRecordingAdded(scheduledRecording);
+ }
mRecentAdapter.addExtraItem(DvrHistoryCardHolder.DVR_HISTORY_CARD_HOLDER);
}
// Series Recordings. Series recordings should be added after recorded programs, because
@@ -697,4 +753,22 @@ public class DvrBrowseFragment extends BrowseFragment
}
}
}
+
+ private class RecentRowAdapter extends SortedArrayAdapter<Object> {
+ RecentRowAdapter(int maxItemCount) {
+ super(mPresenterSelector, RECENT_ROW_COMPARATOR, maxItemCount);
+ }
+
+ @Override
+ public long getId(Object item) {
+ // We takes the inverse number for the ID of scheduled recordings to make the ID stable.
+ if (item instanceof ScheduledRecording) {
+ return -((ScheduledRecording) item).getId() - 1;
+ } else if (item instanceof RecordedProgram) {
+ return ((RecordedProgram) item).getId();
+ } else {
+ return -1;
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
index 2659c3f3..0336b319 100644
--- a/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
+++ b/src/com/android/tv/dvr/ui/browse/DvrDetailsActivity.java
@@ -43,6 +43,9 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On
/** Name of shared element between activities. */
public static final String SHARED_ELEMENT_NAME = "shared_element";
+ /** Name of error message of a failed recording */
+ public static final String EXTRA_FAILED_MESSAGE = "failed_message";
+
/** CURRENT_RECORDING_VIEW refers to Current Recordings in DVR. */
public static final int CURRENT_RECORDING_VIEW = 1;
@@ -65,6 +68,7 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On
long recordId = getIntent().getLongExtra(RECORDING_ID, -1);
int detailsViewType = getIntent().getIntExtra(DETAILS_VIEW_TYPE, -1);
boolean hideViewSchedule = getIntent().getBooleanExtra(HIDE_VIEW_SCHEDULE, false);
+ String failedMsg = getIntent().getStringExtra(EXTRA_FAILED_MESSAGE);
if (recordId != -1 && detailsViewType != -1 && savedInstanceState == null) {
Bundle args = new Bundle();
args.putLong(RECORDING_ID, recordId);
@@ -73,6 +77,7 @@ public class DvrDetailsActivity extends Activity implements PinDialogFragment.On
detailsFragment = new CurrentRecordingDetailsFragment();
} else if (detailsViewType == SCHEDULED_RECORDING_VIEW) {
args.putBoolean(HIDE_VIEW_SCHEDULE, hideViewSchedule);
+ args.putString(EXTRA_FAILED_MESSAGE, failedMsg);
detailsFragment = new ScheduledRecordingDetailsFragment();
} else if (detailsViewType == RECORDED_PROGRAM_VIEW) {
detailsFragment = new RecordedProgramDetailsFragment();
diff --git a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
index e4d95630..aa2ccf75 100644
--- a/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/RecordingDetailsFragment.java
@@ -41,6 +41,10 @@ abstract class RecordingDetailsFragment extends DvrDetailsFragment {
return mRecording != null;
}
+ protected ScheduledRecording getScheduledRecording() {
+ return mRecording;
+ }
+
/** Returns {@link ScheduledRecording} for the current fragment. */
public ScheduledRecording getRecording() {
return mRecording;
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
index 0765117d..302b8318 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingDetailsFragment.java
@@ -34,11 +34,14 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
private DvrManager mDvrManager;
private Action mScheduleAction;
private boolean mHideViewSchedule;
+ private String mFailedMessage;
@Override
public void onCreate(Bundle savedInstance) {
+ Bundle args = getArguments();
mDvrManager = TvSingletons.getSingletons(getContext()).getDvrManager();
- mHideViewSchedule = getArguments().getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
+ mHideViewSchedule = args.getBoolean(DvrDetailsActivity.HIDE_VIEW_SCHEDULE);
+ mFailedMessage = args.getString(DvrDetailsActivity.EXTRA_FAILED_MESSAGE);
super.onCreate(savedInstance);
}
@@ -51,6 +54,17 @@ public class ScheduledRecordingDetailsFragment extends RecordingDetailsFragment
}
@Override
+ protected void onCreateInternal() {
+ if (mFailedMessage == null) {
+ super.onCreateInternal();
+ return;
+ }
+ setDetailsOverviewRow(
+ DetailsContent.createFromFailedScheduledRecording(
+ getContext(), getScheduledRecording(), mFailedMessage));
+ }
+
+ @Override
protected SparseArrayObjectAdapter onCreateActionsAdapter() {
SparseArrayObjectAdapter adapter =
new SparseArrayObjectAdapter(new ActionPresenterSelector());
diff --git a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
index f1ed52c8..8e028689 100644
--- a/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
+++ b/src/com/android/tv/dvr/ui/browse/ScheduledRecordingPresenter.java
@@ -119,13 +119,21 @@ class ScheduledRecordingPresenter extends DvrItemPresenter<ScheduledRecording> {
DetailsContent details = DetailsContent.createFromScheduledRecording(mContext, recording);
cardView.setTitle(details.getTitle());
cardView.setImageUri(details.getLogoImageUri(), details.isUsingChannelLogo());
- cardView.setAffiliatedIcon(
- mDvrManager.isConflicting(recording) ? R.drawable.ic_warning_white_32dp : 0);
+ if (mDvrManager.isConflicting(recording)) {
+ cardView.setAffiliatedIcon(R.drawable.ic_warning_white_32dp);
+ } else if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ cardView.setAffiliatedIcon(R.drawable.ic_error_white_48dp);
+ } else {
+ cardView.setAffiliatedIcon(0);
+ }
cardView.setContent(generateMajorContent(recording), null);
cardView.setDetailBackgroundImageUri(details.getBackgroundImageUri());
}
private String generateMajorContent(ScheduledRecording recording) {
+ if (recording.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
+ return mContext.getString(R.string.dvr_recording_failed);
+ }
int dateDifference =
Utils.computeDateDifference(System.currentTimeMillis(), recording.getStartTimeMs());
if (dateDifference <= 0) {
diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
index 08dc43c1..0ca05fac 100644
--- a/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
+++ b/src/com/android/tv/dvr/ui/list/DvrHistoryFragment.java
@@ -25,13 +25,19 @@ import android.view.ViewGroup;
import android.widget.TextView;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.dvr.DvrDataManager;
+import com.android.tv.dvr.data.RecordedProgram;
+import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRowPresenter.DateHeaderRowPresenter;
/** A fragment to show the DVR history. */
-public class DvrHistoryFragment extends DetailsFragment {
+public class DvrHistoryFragment extends DetailsFragment
+ implements DvrDataManager.ScheduledRecordingListener,
+ DvrDataManager.RecordedProgramListener {
private DvrHistoryRowAdapter mRowsAdapter;
private TextView mEmptyInfoScreenView;
+ private DvrDataManager mDvrDataManager;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -46,13 +52,22 @@ public class DvrHistoryFragment extends DetailsFragment {
getContext(), presenterSelector, singletons.getClock());
setAdapter(mRowsAdapter);
mRowsAdapter.start();
+ mDvrDataManager = singletons.getDvrDataManager();
+ mDvrDataManager.addScheduledRecordingListener(this);
+ mDvrDataManager.addRecordedProgramListener(this);
mEmptyInfoScreenView = (TextView) getActivity().findViewById(R.id.empty_info_screen);
- // TODO: handle show/hide message
+ }
+
+ @Override
+ public void onDestroy() {
+ mDvrDataManager.removeScheduledRecordingListener(this);
+ mDvrDataManager.removeRecordedProgramListener(this);
+ super.onDestroy();
}
/** Shows the empty message. */
- void showEmptyMessage(int messageId) {
- mEmptyInfoScreenView.setText(messageId);
+ void showEmptyMessage() {
+ mEmptyInfoScreenView.setText(R.string.dvr_history_empty_state);
if (mEmptyInfoScreenView.getVisibility() != View.VISIBLE) {
mEmptyInfoScreenView.setVisibility(View.VISIBLE);
}
@@ -71,4 +86,81 @@ public class DvrHistoryFragment extends DetailsFragment {
// Workaround of b/31046014
return null;
}
+
+ @Override
+ public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ mRowsAdapter.onScheduledRecordingAdded(recording);
+ }
+ if (mRowsAdapter.size() > 0) {
+ hideEmptyMessage();
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ mRowsAdapter.onScheduledRecordingRemoved(recording);
+ }
+ if (mRowsAdapter.size() == 0) {
+ showEmptyMessage();
+ }
+ }
+ }
+
+ @Override
+ public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
+ if (mRowsAdapter != null) {
+ for (ScheduledRecording recording : scheduledRecordings) {
+ mRowsAdapter.onScheduledRecordingUpdated(recording);
+ }
+ if (mRowsAdapter.size() == 0) {
+ showEmptyMessage();
+ } else {
+ hideEmptyMessage();
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
+ if (mRowsAdapter != null) {
+ for (RecordedProgram p : recordedPrograms) {
+ mRowsAdapter.onScheduledRecordingAdded(p);
+ }
+ if (mRowsAdapter.size() > 0) {
+ hideEmptyMessage();
+ }
+ }
+
+ }
+
+ @Override
+ public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) {
+ if (mRowsAdapter != null) {
+ for (RecordedProgram program : recordedPrograms) {
+ mRowsAdapter.onScheduledRecordingUpdated(program);
+ }
+ if (mRowsAdapter.size() == 0) {
+ showEmptyMessage();
+ } else {
+ hideEmptyMessage();
+ }
+ }
+ }
+
+ @Override
+ public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
+ if (mRowsAdapter != null) {
+ for (RecordedProgram p : recordedPrograms) {
+ mRowsAdapter.onScheduledRecordingRemoved(p);
+ }
+ if (mRowsAdapter.size() == 0) {
+ showEmptyMessage();
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
index ac828eb8..156d1a7e 100644
--- a/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/DvrHistoryRowAdapter.java
@@ -19,11 +19,14 @@ package com.android.tv.dvr.ui.list;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.text.format.DateUtils;
+import android.util.Log;
import com.android.tv.R;
import com.android.tv.TvSingletons;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.util.Clock;
import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.data.RecordedProgram;
@@ -32,14 +35,17 @@ import com.android.tv.dvr.recorder.ScheduledProgramReaper;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
import com.android.tv.util.Utils;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
/** An adapter for DVR history. */
@TargetApi(VERSION_CODES.N)
@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
class DvrHistoryRowAdapter extends ArrayObjectAdapter {
- // TODO: handle row added/removed/updated
+ private static final String TAG = "DvrHistoryRowAdapter";
+ private static final boolean DEBUG = false;
private static final long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1);
private static final int MAX_HISTORY_DAYS = ScheduledProgramReaper.DAYS;
@@ -48,6 +54,7 @@ class DvrHistoryRowAdapter extends ArrayObjectAdapter {
private final Clock mClock;
private final DvrDataManager mDvrDataManager;
private final List<String> mTitles = new ArrayList<>();
+ private final Map<Long, ScheduledRecording> mRecordedProgramScheduleMap = new HashMap<>();
public DvrHistoryRowAdapter(
Context context, ClassPresenterSelector classPresenterSelector, Clock clock) {
@@ -121,18 +128,226 @@ class DvrHistoryRowAdapter extends ArrayObjectAdapter {
}
private List<ScheduledRecording> recordedProgramsToScheduledRecordings(
- List<RecordedProgram> recordedPrograms, int maxDays) {
- List<ScheduledRecording> result = new ArrayList<>(recordedPrograms.size());
- long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
- for (RecordedProgram recordedProgram : recordedPrograms) {
- if (maxDays
- < Utils.computeDateDifference(
- recordedProgram.getStartTimeUtcMillis(),
- firstMillisecondToday)) {
- continue;
+ List<RecordedProgram> programs, int maxDays) {
+ List<ScheduledRecording> result = new ArrayList<>();
+ for (RecordedProgram recordedProgram : programs) {
+ ScheduledRecording scheduledRecording =
+ recordedProgramsToScheduledRecordings(recordedProgram, maxDays);
+ if (scheduledRecording != null) {
+ result.add(scheduledRecording);
}
- result.add(ScheduledRecording.builder(recordedProgram).build());
}
return result;
}
+
+ @Nullable
+ private ScheduledRecording recordedProgramsToScheduledRecordings(
+ RecordedProgram program, int maxDays) {
+ long firstMillisecondToday = Utils.getFirstMillisecondOfDay(mClock.currentTimeMillis());
+ if (maxDays
+ < Utils.computeDateDifference(
+ program.getStartTimeUtcMillis(),
+ firstMillisecondToday)) {
+ return null;
+ }
+ ScheduledRecording scheduledRecording = ScheduledRecording.builder(program).build();
+ mRecordedProgramScheduleMap.put(program.getId(), scheduledRecording);
+ return scheduledRecording;
+ }
+
+ public void onScheduledRecordingAdded(ScheduledRecording schedule) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingAdded: " + schedule);
+ }
+ if (findRowByScheduledRecording(schedule) == null
+ && (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED
+ || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED)) {
+ addScheduleRow(schedule);
+ }
+ }
+
+ public void onScheduledRecordingAdded(RecordedProgram program) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingAdded: " + program);
+ }
+ if (mRecordedProgramScheduleMap.get(program.getId()) != null) {
+ return;
+ }
+ ScheduledRecording schedule =
+ recordedProgramsToScheduledRecordings(program, MAX_HISTORY_DAYS);
+ if (schedule == null) {
+ return;
+ }
+ addScheduleRow(schedule);
+ }
+
+ public void onScheduledRecordingRemoved(ScheduledRecording schedule) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingRemoved: " + schedule);
+ }
+ ScheduleRow row = findRowByScheduledRecording(schedule);
+ if (row != null) {
+ removeScheduleRow(row);
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ }
+ }
+
+ public void onScheduledRecordingRemoved(RecordedProgram program) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingRemoved: " + program);
+ }
+ ScheduledRecording scheduledRecording = mRecordedProgramScheduleMap.get(program.getId());
+ if (scheduledRecording != null) {
+ mRecordedProgramScheduleMap.remove(program.getId());
+ ScheduleRow row = findRowByRecordedProgram(program);
+ if (row != null) {
+ removeScheduleRow(row);
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ }
+ }
+ }
+
+ public void onScheduledRecordingUpdated(ScheduledRecording schedule) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingUpdated: " + schedule);
+ }
+ ScheduleRow row = findRowByScheduledRecording(schedule);
+ if (row != null) {
+ row.setSchedule(schedule);
+ if (schedule.getState() != ScheduledRecording.STATE_RECORDING_FAILED) {
+ // Only handle failed schedules. Finished schedules are handled as recorded programs
+ removeScheduleRow(row);
+ }
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ }
+ }
+
+ public void onScheduledRecordingUpdated(RecordedProgram program) {
+ if (DEBUG) {
+ Log.d(TAG, "onScheduledRecordingUpdated: " + program);
+ }
+ ScheduleRow row = findRowByRecordedProgram(program);
+ if (row != null) {
+ removeScheduleRow(row);
+ notifyArrayItemRangeChanged(indexOf(row), 1);
+ ScheduledRecording schedule = mRecordedProgramScheduleMap.get(program.getId());
+ if (schedule != null) {
+ mRecordedProgramScheduleMap.remove(program.getId());
+ }
+ }
+ onScheduledRecordingAdded(program);
+ }
+
+ private void addScheduleRow(ScheduledRecording recording) {
+ // This method must not be called from inherited class.
+ SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class));
+ if (recording != null) {
+ int pre = -1;
+ int index = 0;
+ for (; index < size(); index++) {
+ if (get(index) instanceof ScheduleRow) {
+ ScheduleRow scheduleRow = (ScheduleRow) get(index);
+ if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.reversed()
+ .compare(scheduleRow.getSchedule(), recording) > 0) {
+ break;
+ }
+ pre = index;
+ }
+ }
+ long deadLine = Utils.getFirstMillisecondOfDay(recording.getStartTimeMs());
+ if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) {
+ SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow();
+ headerRow.setItemCount(headerRow.getItemCount() + 1);
+ ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
+ add(++pre, addedRow);
+ updateHeaderDescription(headerRow);
+ } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) {
+ SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow();
+ headerRow.setItemCount(headerRow.getItemCount() + 1);
+ ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
+ add(index, addedRow);
+ updateHeaderDescription(headerRow);
+ } else {
+ SchedulesHeaderRow headerRow =
+ new DateHeaderRow(
+ calculateHeaderDate(deadLine),
+ mContext.getResources()
+ .getQuantityString(
+ R.plurals.dvr_schedules_section_subtitle, 1, 1),
+ 1,
+ deadLine);
+ add(++pre, headerRow);
+ ScheduleRow addedRow = new ScheduleRow(recording, headerRow);
+ add(pre, addedRow);
+ }
+ }
+ }
+
+ private DateHeaderRow getHeaderRow(int index) {
+ return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow());
+ }
+
+ /** Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. */
+ private ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) {
+ if (recording == null) {
+ return null;
+ }
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) {
+ if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) {
+ return (ScheduleRow) item;
+ }
+ }
+ }
+ return null;
+ }
+
+ private ScheduleRow findRowByRecordedProgram(RecordedProgram program) {
+ if (program == null) {
+ return null;
+ }
+ for (int i = 0; i < size(); i++) {
+ Object item = get(i);
+ if (item instanceof ScheduleRow) {
+ ScheduleRow row = (ScheduleRow) item;
+ if (row.hasRecordedProgram()
+ && row.getSchedule().getRecordedProgramId() == program.getId()) {
+ return (ScheduleRow) item;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void removeScheduleRow(ScheduleRow scheduleRow) {
+ // This method must not be called from inherited class.
+ SoftPreconditions.checkState(getClass().equals(DvrHistoryRowAdapter.class));
+ if (scheduleRow != null) {
+ scheduleRow.setSchedule(null);
+ SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow();
+ remove(scheduleRow);
+ // Changes the count information of header which the removed row belongs to.
+ if (headerRow != null) {
+ int currentCount = headerRow.getItemCount();
+ headerRow.setItemCount(--currentCount);
+ if (headerRow.getItemCount() == 0) {
+ remove(headerRow);
+ } else {
+ replace(indexOf(headerRow), headerRow);
+ updateHeaderDescription(headerRow);
+ }
+ }
+ }
+ }
+
+ private void updateHeaderDescription(SchedulesHeaderRow headerRow) {
+ headerRow.setDescription(
+ mContext.getResources()
+ .getQuantityString(
+ R.plurals.dvr_schedules_section_subtitle,
+ headerRow.getItemCount(),
+ headerRow.getItemCount()));
+ }
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRow.java b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
index ce5f9516..b739c18f 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRow.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRow.java
@@ -129,6 +129,12 @@ class ScheduleRow {
|| mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED);
}
+ public boolean hasRecordedProgram() {
+ return mSchedule != null
+ && mSchedule.getRecordedProgramId() != null
+ && mSchedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED;
+ }
+
/** Creates and returns the new schedule with the existing information. */
public ScheduledRecording.Builder createNewScheduleBuilder() {
return mSchedule != null ? ScheduledRecording.buildFrom(mSchedule) : null;
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
index bbccdb15..ef4a4337 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
@@ -27,14 +27,15 @@ import android.support.v17.leanback.widget.ClassPresenterSelector;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
+
import com.android.tv.R;
-import com.android.tv.TvFeatures;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow;
import com.android.tv.util.Utils;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -115,33 +116,6 @@ class ScheduleRowAdapter extends ArrayObjectAdapter {
}
deadLine += ONE_DAY_MS;
}
- if (TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())) {
- List<ScheduledRecording> failedRecordingList =
- TvSingletons.getSingletons(mContext)
- .getDvrDataManager()
- .getFailedScheduledRecordings();
- Collections.sort(
- failedRecordingList,
- ScheduledRecording.START_TIME_COMPARATOR.reversed());
- if (!failedRecordingList.isEmpty()) {
- SchedulesHeaderRow headerRow =
- // TODO(b/72638385): use R.string
- // TODO(b/72638385): define another HeaderRow class
- new DateHeaderRow(
- "Failed recordings",
- mContext.getResources()
- .getQuantityString(
- R.plurals.dvr_schedules_section_subtitle,
- failedRecordingList.size(),
- failedRecordingList.size()),
- failedRecordingList.size(),
- Long.MAX_VALUE);
- add(headerRow);
- for (ScheduledRecording recording : failedRecordingList) {
- add(new ScheduleRow(recording, headerRow));
- }
- }
- }
sendNextUpdateMessage(System.currentTimeMillis());
}
@@ -414,9 +388,7 @@ class ScheduleRowAdapter extends ArrayObjectAdapter {
Object item = get(i);
if (item instanceof ScheduleRow) {
ScheduleRow row = (ScheduleRow) item;
- ScheduledRecording recording = row.getSchedule();
- if (row.getEndTimeMs() <= currentTimeMs && (recording == null
- || recording.getState() != ScheduledRecording.STATE_RECORDING_FAILED)) {
+ if (row.getEndTimeMs() <= currentTimeMs) {
removeScheduleRow(row);
}
}
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
index 48430956..38d3d582 100644
--- a/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
+++ b/src/com/android/tv/dvr/ui/list/ScheduleRowPresenter.java
@@ -437,6 +437,9 @@ class ScheduleRowPresenter extends RowPresenter {
// TODO(b/72638385): show real error messages
// TODO(b/72638385): use a better name for ConflictInfoXXX
conflictInfo = "Failed";
+ if (schedule.getFailedReason() != null) {
+ conflictInfo += " (Error code: " + schedule.getFailedReason() + ")";
+ }
} else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
} else {
@@ -478,13 +481,8 @@ class ScheduleRowPresenter extends RowPresenter {
/** Returns time text for time view from scheduled recording. */
protected String onGetRecordingTimeText(ScheduleRow row) {
- boolean showDate =
- TvFeatures.DVR_FAILED_LIST.isEnabled(getContext())
- && row.getSchedule() != null
- && row.getSchedule().getState()
- == ScheduledRecording.STATE_RECORDING_FAILED;
return Utils.getDurationString(
- mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, showDate, true, 0);
+ mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, false, true, 0);
}
/** Returns program info text for program title view. */
@@ -511,9 +509,10 @@ class ScheduleRowPresenter extends RowPresenter {
private boolean isInfoClickable(ScheduleRow row) {
ScheduledRecording schedule = row.getSchedule();
- // TODO: handle onClicked for finished schedules
return schedule != null
- && (schedule.isNotStarted() || schedule.isInProgress() || schedule.isFinished());
+ && (schedule.isNotStarted()
+ || schedule.isInProgress()
+ || schedule.isFinished());
}
/** Called when the button in a row is clicked. */
@@ -810,7 +809,7 @@ class ScheduleRowPresenter extends RowPresenter {
if (row.getSchedule() != null) {
if (row.isRecordingInProgress()) {
return new int[] {ACTION_STOP_RECORDING};
- } else if (row.isOnAir()) {
+ } else if (row.isOnAir() && !row.hasRecordedProgram()) {
if (row.isRecordingNotStarted()) {
if (canResolveConflict()) {
// The "START" action can change the conflict states.
@@ -865,8 +864,9 @@ class ScheduleRowPresenter extends RowPresenter {
/** Checks if the row should be grayed out. */
protected boolean shouldBeGrayedOut(ScheduleRow row) {
return row.getSchedule() == null
- || (row.isOnAir() && !row.isRecordingInProgress())
+ || (row.isOnAir() && !row.isRecordingInProgress() && !row.hasRecordedProgram())
|| mDvrManager.isConflicting(row.getSchedule())
- || row.isScheduleCanceled();
+ || row.isScheduleCanceled()
+ || row.isRecordingFailed();
}
}
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
index 33ab9ad7..5b53f904 100644
--- a/src/com/android/tv/guide/ProgramGuide.java
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -43,6 +43,7 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
import com.android.tv.R;
@@ -57,6 +58,7 @@ import com.android.tv.dvr.DvrDataManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
import com.android.tv.ui.ViewUtils;
+import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.Utils;
import java.util.ArrayList;
@@ -64,7 +66,8 @@ import java.util.List;
import java.util.concurrent.TimeUnit;
/** The program guide. */
-public class ProgramGuide implements ProgramGrid.ChildFocusListener {
+public class ProgramGuide
+ implements ProgramGrid.ChildFocusListener, AccessibilityStateChangeListener {
private static final String TAG = "ProgramGuide";
private static final boolean DEBUG = false;
@@ -141,13 +144,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
private final Handler mHandler = new ProgramGuideHandler(this);
private boolean mActive;
- private final Runnable mHideRunnable =
- new Runnable() {
- @Override
- public void run() {
- hide();
- }
- };
+ private final AutoHideScheduler mAutoHideScheduler;
private final long mShowDurationMillis;
private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow;
@@ -415,6 +412,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
mShowGuidePartial =
mAccessibilityManager.isEnabled()
|| mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
+ mAutoHideScheduler = new AutoHideScheduler(activity, this::hide);
}
@Override
@@ -569,13 +567,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
/** Schedules hiding the program guide. */
public void scheduleHide() {
- cancelHide();
- mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
+ mAutoHideScheduler.schedule(mShowDurationMillis);
}
/** Cancels hiding the program guide. */
public void cancelHide() {
- mHandler.removeCallbacks(mHideRunnable);
+ mAutoHideScheduler.cancel();
}
/** Process the {@code KEYCODE_BACK} key event. */
@@ -928,6 +925,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener {
}
}
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAutoHideScheduler.onAccessibilityStateChanged(enabled);
+ }
+
private class GlobalFocusChangeListener
implements ViewTreeObserver.OnGlobalFocusChangeListener {
private static final int UNKNOWN = 0;
diff --git a/src/com/android/tv/menu/Menu.java b/src/com/android/tv/menu/Menu.java
index 0e081ba8..19a93dbc 100644
--- a/src/com/android/tv/menu/Menu.java
+++ b/src/com/android/tv/menu/Menu.java
@@ -21,24 +21,22 @@ import android.animation.AnimatorInflater;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.content.res.Resources;
-import android.os.Looper;
-import android.os.Message;
import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v17.leanback.widget.HorizontalGridView;
import android.util.Log;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import com.android.tv.ChannelTuner;
import com.android.tv.R;
import com.android.tv.TvOptionsManager;
import com.android.tv.TvSingletons;
import com.android.tv.analytics.Tracker;
-import com.android.tv.common.WeakHandler;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.common.util.DurationTimer;
import com.android.tv.menu.MenuRowFactory.PartnerRow;
import com.android.tv.menu.MenuRowFactory.TvOptionsRow;
import com.android.tv.ui.TunableTvView;
+import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.ViewCache;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,7 +46,7 @@ import java.util.List;
import java.util.Map;
/** A class which controls the menu. */
-public class Menu {
+public class Menu implements AccessibilityStateChangeListener {
private static final String TAG = "Menu";
private static final boolean DEBUG = false;
@@ -103,15 +101,13 @@ public class Menu {
private static final String SCREEN_NAME = "Menu";
- private static final int MSG_HIDE_MENU = 1000;
-
private final Context mContext;
private final IMenuView mMenuView;
private final Tracker mTracker;
private final DurationTimer mVisibleTimer = new DurationTimer();
private final long mShowDurationMillis;
private final OnMenuVisibilityChangeListener mOnMenuVisibilityChangeListener;
- private final WeakHandler<Menu> mHandler = new MenuWeakHandler(this, Looper.getMainLooper());
+ private final AutoHideScheduler mAutoHideScheduler;
private final MenuUpdater mMenuUpdater;
private final List<MenuRow> mMenuRows = new ArrayList<>();
@@ -161,6 +157,7 @@ public class Menu {
addMenuRow(menuRowFactory.createMenuRow(this, PartnerRow.class));
addMenuRow(menuRowFactory.createMenuRow(this, TvOptionsRow.class));
mMenuView.setMenuRows(mMenuRows);
+ mAutoHideScheduler = new AutoHideScheduler(context, () -> hide(true));
}
/**
@@ -183,7 +180,7 @@ public class Menu {
for (MenuRow row : mMenuRows) {
row.release();
}
- mHandler.removeCallbacksAndMessages(null);
+ mAutoHideScheduler.cancel();
}
/** Preloads the item view used for the menu. */
@@ -238,7 +235,7 @@ public class Menu {
if (mAnimationDisabledForTest) {
withAnimation = false;
}
- mHandler.removeMessages(MSG_HIDE_MENU);
+ mAutoHideScheduler.cancel();
if (withAnimation) {
if (!mHideAnimator.isStarted()) {
mHideAnimator.start();
@@ -261,10 +258,7 @@ public class Menu {
/** Schedules to hide the menu in some seconds. */
public void scheduleHide() {
- mHandler.removeMessages(MSG_HIDE_MENU);
- if (!mKeepVisible) {
- mHandler.sendEmptyMessageDelayed(MSG_HIDE_MENU, mShowDurationMillis);
- }
+ mAutoHideScheduler.schedule(mShowDurationMillis);
}
/**
@@ -276,7 +270,7 @@ public class Menu {
public void setKeepVisible(boolean keepVisible) {
mKeepVisible = keepVisible;
if (mKeepVisible) {
- mHandler.removeMessages(MSG_HIDE_MENU);
+ mAutoHideScheduler.cancel();
} else if (isActive()) {
scheduleHide();
}
@@ -284,7 +278,7 @@ public class Menu {
@VisibleForTesting
boolean isHideScheduled() {
- return mHandler.hasMessages(MSG_HIDE_MENU);
+ return mAutoHideScheduler.isScheduled();
}
/** Returns {@code true} if the menu is open and not hiding. */
@@ -326,6 +320,11 @@ public class Menu {
mMenuUpdater.onStreamInfoChanged();
}
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAutoHideScheduler.onAccessibilityStateChanged(enabled);
+ }
+
@VisibleForTesting
void disableAnimationForTest() {
if (!CommonUtils.isRunningInTest()) {
@@ -339,17 +338,4 @@ public class Menu {
/** Called when the menu becomes visible/invisible. */
public abstract void onMenuVisibilityChange(boolean visible);
}
-
- private static class MenuWeakHandler extends WeakHandler<Menu> {
- public MenuWeakHandler(Menu menu, Looper mainLooper) {
- super(mainLooper, menu);
- }
-
- @Override
- public void handleMessage(Message msg, @NonNull Menu menu) {
- if (msg.what == MSG_HIDE_MENU) {
- menu.hide(true);
- }
- }
- }
}
diff --git a/src/com/android/tv/ui/ChannelBannerView.java b/src/com/android/tv/ui/ChannelBannerView.java
index c3b8173f..28325197 100644
--- a/src/com/android/tv/ui/ChannelBannerView.java
+++ b/src/com/android/tv/ui/ChannelBannerView.java
@@ -26,7 +26,6 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputInfo;
-import android.os.Handler;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableString;
@@ -38,6 +37,8 @@ import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
@@ -56,6 +57,8 @@ import com.android.tv.data.api.Channel;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.parental.ContentRatingsManager;
+import com.android.tv.ui.TvTransitionManager.TransitionLayout;
+import com.android.tv.ui.hideable.AutoHideScheduler;
import com.android.tv.util.Utils;
import com.android.tv.util.images.ImageCache;
import com.android.tv.util.images.ImageLoader;
@@ -63,7 +66,8 @@ import com.android.tv.util.images.ImageLoader.ImageLoaderCallback;
import com.android.tv.util.images.ImageLoader.LoadTvInputLogoTask;
/** A view to render channel banner. */
-public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout {
+public class ChannelBannerView extends FrameLayout
+ implements TransitionLayout, AccessibilityStateChangeListener {
private static final String TAG = "ChannelBannerView";
private static final boolean DEBUG = false;
@@ -113,7 +117,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private Channel mCurrentChannel;
private boolean mCurrentChannelLogoExists;
private Program mLastUpdatedProgram;
- private final Handler mHandler = new Handler();
+ private final AutoHideScheduler mAutoHideScheduler;
private final DvrManager mDvrManager;
private ContentRatingsManager mContentRatingsManager;
private TvContentRating mBlockingContentRating;
@@ -128,21 +132,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
private final Animator mProgramDescriptionFadeInAnimator;
private final Animator mProgramDescriptionFadeOutAnimator;
- private final Runnable mHideRunnable =
- new Runnable() {
- @Override
- public void run() {
- mCurrentHeight = 0;
- mMainActivity
- .getOverlayManager()
- .hideOverlays(
- TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
- | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
- }
- };
private final long mShowDurationMillis;
private final int mChannelLogoImageViewWidth;
private final int mChannelLogoImageViewHeight;
@@ -183,7 +172,6 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mResources = getResources();
-
mMainActivity = (MainActivity) context;
mShowDurationMillis = mResources.getInteger(R.integer.channel_banner_show_duration);
@@ -235,6 +223,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
if (sClosedCaptionMark == null) {
sClosedCaptionMark = context.getString(R.string.closed_caption);
}
+ mAutoHideScheduler = new AutoHideScheduler(context, this::hide);
+ context.getSystemService(AccessibilityManager.class)
+ .addAccessibilityStateChangeListener(mAutoHideScheduler);
}
@Override
@@ -278,22 +269,13 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
if (fromEmptyScene) {
ViewUtils.setTransitionAlpha(mChannelView, 1f);
}
- scheduleHide();
+ mAutoHideScheduler.schedule(mShowDurationMillis);
}
@Override
public void onExitAction() {
mCurrentHeight = 0;
- cancelHide();
- }
-
- private void scheduleHide() {
- cancelHide();
- mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
- }
-
- private void cancelHide() {
- mHandler.removeCallbacks(mHideRunnable);
+ mAutoHideScheduler.cancel();
}
private void resetAnimationEffects() {
@@ -343,7 +325,7 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mUpdateOnTune = updateOnTune;
if (mUpdateOnTune) {
if (isShown()) {
- scheduleHide();
+ mAutoHideScheduler.schedule(mShowDurationMillis);
}
mBlockingContentRating = null;
mCurrentChannel = mMainActivity.getCurrentChannel();
@@ -356,6 +338,18 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
mUpdateOnTune = false;
}
+ private void hide() {
+ mCurrentHeight = 0;
+ mMainActivity
+ .getOverlayManager()
+ .hideOverlays(
+ TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
+ | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
+ }
+
/**
* Update channel banner view with stream info.
*
@@ -831,4 +825,9 @@ public class ChannelBannerView extends FrameLayout implements TvTransitionManage
animator.addListener(mResizeAnimatorListener);
return animator;
}
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAutoHideScheduler.onAccessibilityStateChanged(enabled);
+ }
}
diff --git a/src/com/android/tv/ui/TunableTvView.java b/src/com/android/tv/ui/TunableTvView.java
index 9c75bc80..bb98d974 100644
--- a/src/com/android/tv/ui/TunableTvView.java
+++ b/src/com/android/tv/ui/TunableTvView.java
@@ -96,6 +96,8 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
public static final int VIDEO_UNAVAILABLE_REASON_SCREEN_BLOCKED = -3;
public static final int VIDEO_UNAVAILABLE_REASON_NONE = -100;
+ private OnTalkBackDpadKeyListener mOnTalkBackDpadKeyListener;
+
@Retention(RetentionPolicy.SOURCE)
@IntDef({BLOCK_SCREEN_TYPE_NO_UI, BLOCK_SCREEN_TYPE_SHRUNKEN_TV_VIEW, BLOCK_SCREEN_TYPE_NORMAL})
public @interface BlockScreenType {}
@@ -500,6 +502,30 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
}
}
});
+ View placeholder = findViewById(R.id.placeholder);
+ placeholder.requestFocus();
+ findViewById(R.id.channel_up)
+ .setOnFocusChangeListener(
+ (v, hasFocus) -> {
+ if (hasFocus) {
+ placeholder.requestFocus();
+ if (mOnTalkBackDpadKeyListener != null) {
+ mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
+ KeyEvent.KEYCODE_DPAD_UP);
+ }
+ }
+ });
+ findViewById(R.id.channel_down)
+ .setOnFocusChangeListener(
+ (v, hasFocus) -> {
+ if (hasFocus) {
+ placeholder.requestFocus();
+ if (mOnTalkBackDpadKeyListener != null) {
+ mOnTalkBackDpadKeyListener.onTalkBackDpadKey(
+ KeyEvent.KEYCODE_DPAD_DOWN);
+ }
+ }
+ });
}
public void initialize(
@@ -843,6 +869,10 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
mTvView.setOnUnhandledInputEventListener(listener);
}
+ public void setOnTalkBackDpadKeyListener(OnTalkBackDpadKeyListener listener) {
+ mOnTalkBackDpadKeyListener = listener;
+ }
+
public void setClosedCaptionEnabled(boolean enabled) {
mTvView.setCaptionEnabled(enabled);
}
@@ -1416,6 +1446,12 @@ public class TunableTvView extends FrameLayout implements StreamInfo, TunableTvV
};
}
+ /** Listens for dpad actions that are otherwise trapped by talkback */
+ public interface OnTalkBackDpadKeyListener {
+
+ void onTalkBackDpadKey(int keycode);
+ }
+
/** A listener which receives the notification when the screen is blocked/unblocked. */
public abstract static class OnScreenBlockingChangedListener {
/** Called when the screen is blocked/unblocked. */
diff --git a/src/com/android/tv/ui/TvOverlayManager.java b/src/com/android/tv/ui/TvOverlayManager.java
index 5daa525a..222fcb3a 100644
--- a/src/com/android/tv/ui/TvOverlayManager.java
+++ b/src/com/android/tv/ui/TvOverlayManager.java
@@ -32,6 +32,7 @@ import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import com.android.tv.ChannelTuner;
import com.android.tv.MainActivity;
import com.android.tv.MainActivity.KeyHandlerResultType;
@@ -78,7 +79,7 @@ import java.util.Set;
/** A class responsible for the life cycle and event handling of the pop-ups over TV view. */
@UiThread
-public class TvOverlayManager {
+public class TvOverlayManager implements AccessibilityStateChangeListener {
private static final String TAG = "TvOverlayManager";
private static final boolean DEBUG = false;
private static final String INTRO_TRACKER_LABEL = "Intro dialog";
@@ -780,6 +781,14 @@ public class TvOverlayManager {
}
}
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ // Propagate this to all elements that need it
+ mChannelBannerView.onAccessibilityStateChanged(enabled);
+ mProgramGuide.onAccessibilityStateChanged(enabled);
+ mSideFragmentManager.onAccessibilityStateChanged(enabled);
+ }
+
/**
* Returns true, if a main view needs to hide informational text. Specifically, when overlay UIs
* except banner is shown, the informational text needs to be hidden for clean UI.
diff --git a/src/com/android/tv/ui/hideable/AutoHideScheduler.java b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
new file mode 100644
index 00000000..75859792
--- /dev/null
+++ b/src/com/android/tv/ui/hideable/AutoHideScheduler.java
@@ -0,0 +1,98 @@
+package com.android.tv.ui.hideable;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.annotation.UiThread;
+import android.support.annotation.VisibleForTesting;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import com.android.tv.common.WeakHandler;
+
+/**
+ * Schedules a view element to be hidden after a delay.
+ *
+ * <p>When accessibility is turned on elements are not automatically hidden.
+ *
+ * <p>Users of this class must pass it to {@link
+ * AccessibilityManager#addAccessibilityStateChangeListener(AccessibilityStateChangeListener)} and
+ * {@link
+ * AccessibilityManager#removeAccessibilityStateChangeListener(AccessibilityStateChangeListener)}
+ * during the appropriate live cycle event, or handle calling {@link
+ * #onAccessibilityStateChanged(boolean)}.
+ */
+@UiThread
+public final class AutoHideScheduler implements AccessibilityStateChangeListener {
+ private static final int MSG_HIDE = 1;
+
+ private final HideHandler mHandler;
+ private final Runnable mRunnable;
+
+ public AutoHideScheduler(Context context, Runnable runnable) {
+ this(
+ runnable,
+ context.getSystemService(AccessibilityManager.class),
+ Looper.getMainLooper());
+ }
+
+ @VisibleForTesting
+ AutoHideScheduler(Runnable runnable, AccessibilityManager accessibilityManager, Looper looper) {
+ // Keep a reference here because HideHandler only has a weak reference to it.
+ mRunnable = runnable;
+ mHandler = new HideHandler(looper, mRunnable);
+ mHandler.setAllowAutoHide(!accessibilityManager.isEnabled());
+ }
+
+ public void cancel() {
+ mHandler.removeMessages(MSG_HIDE);
+ }
+
+ public void schedule(long delayMs) {
+ cancel();
+ if (mHandler.mAllowAutoHide) {
+ mHandler.sendEmptyMessageDelayed(MSG_HIDE, delayMs);
+ }
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mHandler.onAccessibilityStateChanged(enabled);
+ }
+
+ public boolean isScheduled() {
+ return mHandler.hasMessages(MSG_HIDE);
+ }
+
+ private static class HideHandler extends WeakHandler<Runnable>
+ implements AccessibilityStateChangeListener {
+
+ private boolean mAllowAutoHide;
+
+ public HideHandler(Looper looper, Runnable hideRunner) {
+ super(looper, hideRunner);
+ }
+
+ @Override
+ protected void handleMessage(Message msg, @NonNull Runnable runnable) {
+ switch (msg.what) {
+ case MSG_HIDE:
+ if (mAllowAutoHide) {
+ runnable.run();
+ }
+ break;
+ default:
+ // do nothing
+ }
+ }
+
+ public void setAllowAutoHide(boolean mAllowAutoHide) {
+ this.mAllowAutoHide = mAllowAutoHide;
+ }
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAllowAutoHide = !enabled;
+ }
+ }
+}
diff --git a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
index b8482a5b..5bba4097 100644
--- a/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
+++ b/src/com/android/tv/ui/sidepanel/SideFragmentManager.java
@@ -22,12 +22,14 @@ import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
-import android.os.Handler;
import android.view.View;
import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
import com.android.tv.R;
+import com.android.tv.ui.hideable.AutoHideScheduler;
-public class SideFragmentManager {
+/** Manages {@link SideFragment}s. */
+public class SideFragmentManager implements AccessibilityStateChangeListener {
private static final String FIRST_BACKSTACK_RECORD_NAME = "0";
private final Activity mActivity;
@@ -44,14 +46,7 @@ public class SideFragmentManager {
private final Animator mShowAnimator;
private final Animator mHideAnimator;
- private final Handler mHandler = new Handler();
- private final Runnable mHideAllRunnable =
- new Runnable() {
- @Override
- public void run() {
- hideAll(true);
- }
- };
+ private final AutoHideScheduler mAutoHideScheduler;
private final long mShowDurationMillis;
public SideFragmentManager(
@@ -77,6 +72,7 @@ public class SideFragmentManager {
mShowDurationMillis =
mActivity.getResources().getInteger(R.integer.side_panel_show_duration);
+ mAutoHideScheduler = new AutoHideScheduler(activity, () -> hideAll(true));
}
public int getCount() {
@@ -176,7 +172,7 @@ public class SideFragmentManager {
}
private void hideAllInternal() {
- mHandler.removeCallbacksAndMessages(null);
+ mAutoHideScheduler.cancel();
if (mFragmentCount == 0) {
return;
}
@@ -214,7 +210,7 @@ public class SideFragmentManager {
* want to empty the back stack, call {@link #hideAll}.
*/
public void hideSidePanel(boolean withAnimation) {
- mHandler.removeCallbacks(mHideAllRunnable);
+ mAutoHideScheduler.cancel();
if (withAnimation) {
Animator hideAnimator =
AnimatorInflater.loadAnimator(mActivity, R.animator.side_panel_exit);
@@ -238,8 +234,7 @@ public class SideFragmentManager {
/** Resets the timer for hiding side fragment. */
public void scheduleHideAll() {
- mHandler.removeCallbacks(mHideAllRunnable);
- mHandler.postDelayed(mHideAllRunnable, mShowDurationMillis);
+ mAutoHideScheduler.schedule(mShowDurationMillis);
}
/** Should {@code keyCode} hide the current panel. */
@@ -251,4 +246,9 @@ public class SideFragmentManager {
}
return false;
}
+
+ @Override
+ public void onAccessibilityStateChanged(boolean enabled) {
+ mAutoHideScheduler.onAccessibilityStateChanged(enabled);
+ }
}