diff options
48 files changed, 1321 insertions, 406 deletions
diff --git a/res/layout/tunable_tv_view.xml b/res/layout/tunable_tv_view.xml index 00c9908c..549d0535 100644 --- a/res/layout/tunable_tv_view.xml +++ b/res/layout/tunable_tv_view.xml @@ -17,6 +17,27 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" > + <View android:id="@+id/channel_up" + android:layout_width="wrap_content" + android:focusable="false" + android:focusableInTouchMode="true" + android:layout_height="1dp" + android:layout_gravity="top" /> + <View android:id="@+id/placeholder" + android:layout_width="1dp" + android:layout_height="1dp" + android:focusable="false" + android:focusableInTouchMode="true" + android:focusedByDefault="true" + android:layout_gravity="center" /> + + <View android:id="@+id/channel_down" + android:layout_width="wrap_content" + android:focusable="false" + android:focusableInTouchMode="true" + android:layout_height="1dp" + android:layout_gravity="bottom" /> + <com.android.tv.ui.AppLayerTvView android:id="@+id/tv_view" android:layout_width="match_parent" android:layout_height="match_parent" diff --git a/res/values/strings.xml b/res/values/strings.xml index 255d9663..cea4ee6c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -576,6 +576,8 @@ <string name="dvr_history_card_view_title">Recording History</string> <!-- Description of a card view to show full list of scheduled recordings. [CHAR LIMIT=25] --> <string name="dvr_full_schedule_card_view_title">Full schedule</string> + <!-- Description of failed recordings. [CHAR LIMIT=25] --> + <string name="dvr_recording_failed">Recording Failed</string> <!-- Description of how many following days the schedule list will show. [CHAR LIMIT=25] --> <plurals name="dvr_full_schedule_card_view_content"> <item quantity="one">Next %1$d day</item> @@ -892,7 +894,10 @@ <string name="dvr_schedules_tuner_conflict_will_not_be_recorded_info">Won\'t be recorded due to tuner conflicts.</string> <!-- Description of no schedule recording for now, and ask user to schedule recordings from the program guide. --> - <string name="dvr_schedules_empty_state">There are no recordings on schedule yet.\nYou can schedule recording from the program guide.</string> + <string name="dvr_schedules_empty_state">There are no recordings scheduled yet.\nYou can schedule recordings from the program guide.</string> + <!-- Description of no recording history for now, and ask user to schedule recordings from + the program guide. --> + <string name="dvr_history_empty_state">There is no recording history.\nYou can schedule recordings from the program guide.</string> <!-- Description of schedule list header about how many recordings conflict. --> <plurals name="dvr_series_schedules_header_description"> <item quantity="one">%1$d recording conflict</item> 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); + } } diff --git a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java index 70e8bfb6..b8a055c7 100644 --- a/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java +++ b/tests/common/src/com/android/tv/testing/dvr/DvrDataManagerInMemoryImpl.java @@ -145,13 +145,17 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { /** Add a new scheduled recording. */ @Override public void addScheduledRecording(ScheduledRecording... scheduledRecordings) { + addScheduledRecording(false, scheduledRecordings); + } + + public void addScheduledRecording(boolean keepIds, ScheduledRecording... scheduledRecordings) { for (ScheduledRecording r : scheduledRecordings) { - addScheduledRecordingInternal(r); + addScheduledRecordingInternal(r, keepIds); } } public void addRecordedProgram(RecordedProgram recordedProgram) { - addRecordedProgramInternal(recordedProgram); + addRecordedProgramInternal(recordedProgram, false); } public void updateRecordedProgram(RecordedProgram r) { @@ -170,29 +174,42 @@ public final class DvrDataManagerInMemoryImpl extends BaseDvrDataManager { } public ScheduledRecording addScheduledRecordingInternal(ScheduledRecording scheduledRecording) { - SoftPreconditions.checkState( - scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, - TAG, - "expected id of " - + ScheduledRecording.ID_NOT_SET - + " but was " - + scheduledRecording); - scheduledRecording = - ScheduledRecording.buildFrom(scheduledRecording) - .setId(mNextId.incrementAndGet()) - .build(); + return addScheduledRecordingInternal(scheduledRecording, false); + } + + public ScheduledRecording addScheduledRecordingInternal( + ScheduledRecording scheduledRecording, boolean keepId) { + if (!keepId) { + SoftPreconditions.checkState( + scheduledRecording.getId() == ScheduledRecording.ID_NOT_SET, + TAG, + "expected id of " + + ScheduledRecording.ID_NOT_SET + + " but was " + + scheduledRecording); + scheduledRecording = + ScheduledRecording.buildFrom(scheduledRecording) + .setId(mNextId.incrementAndGet()) + .build(); + } mScheduledRecordings.put(scheduledRecording.getId(), scheduledRecording); notifyScheduledRecordingAdded(scheduledRecording); return scheduledRecording; } - public RecordedProgram addRecordedProgramInternal(RecordedProgram recordedProgram) { - SoftPreconditions.checkState( - recordedProgram.getId() == RecordedProgram.ID_NOT_SET, - TAG, - "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram); - recordedProgram = - RecordedProgram.buildFrom(recordedProgram).setId(mNextId.incrementAndGet()).build(); + public RecordedProgram addRecordedProgramInternal( + RecordedProgram recordedProgram, boolean keepId) { + if (!keepId) { + SoftPreconditions.checkState( + recordedProgram.getId() == RecordedProgram.ID_NOT_SET, + TAG, + "expected id of " + RecordedProgram.ID_NOT_SET + " but was " + recordedProgram); + recordedProgram = + RecordedProgram + .buildFrom(recordedProgram) + .setId(mNextId.incrementAndGet()) + .build(); + } mRecordedPrograms.put(recordedProgram.getId(), recordedProgram); notifyRecordedProgramsAdded(recordedProgram); return recordedProgram; diff --git a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java index 60a36389..600b52b6 100644 --- a/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java +++ b/tests/func/src/com/android/tv/tests/ui/ChannelBannerViewTest.java @@ -21,6 +21,7 @@ import android.support.test.uiautomator.Until; import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +44,7 @@ public class ChannelBannerViewTest { + Constants.MAX_SHOW_DELAY_MILLIS; } + @Ignore("b/73727914") @Test public void testChannelBannerAppearDisappear() { controller.pressDPadCenter(); diff --git a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java index 2fc0c97e..4b6befe4 100644 --- a/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java +++ b/tests/func/src/com/android/tv/tests/ui/TimeoutTest.java @@ -19,6 +19,7 @@ import android.support.test.filters.LargeTest; import android.support.test.uiautomator.Until; import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -37,6 +38,12 @@ public class TimeoutTest { @Rule public final LiveChannelsTestController controller = new LiveChannelsTestController(); @Test + public void placeholder() { + // There must be at least one test + } + + @Ignore("b/73727914") + @Test public void testMenu() { controller.liveChannelsHelper.assertAppStarted(); controller.pressMenu(); @@ -47,6 +54,7 @@ public class TimeoutTest { controller.getTargetResources().getInteger(R.integer.menu_show_duration)); } + @Ignore("b/73727914") @Test public void testProgramGuide() { controller.liveChannelsHelper.assertAppStarted(); diff --git a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java index 374900c4..09b855e2 100644 --- a/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java +++ b/tests/func/src/com/android/tv/tests/ui/sidepanel/CustomizeChannelListFragmentTest.java @@ -30,6 +30,7 @@ import com.android.tv.R; import com.android.tv.testing.uihelper.Constants; import com.android.tv.tests.ui.LiveChannelsTestController; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -105,6 +106,7 @@ public class CustomizeChannelListFragmentTest { assertShrunkenTvView(false); } + @Ignore("b/73727914") @Test public void testCustomizeChannelList_timeout() { // Show customize channel list fragment diff --git a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java index 3183d92e..d510da32 100644 --- a/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java +++ b/tests/unit/src/com/android/tv/dvr/recorder/DvrRecordingServiceTest.java @@ -16,6 +16,8 @@ package com.android.tv.dvr.recorder; +import static com.google.common.truth.Truth.assertThat; + import android.content.Intent; import android.os.Build; import android.support.test.filters.SdkSuppress; @@ -53,13 +55,13 @@ public class DvrRecordingServiceTest public void testStartService_null() throws Exception { // Not recording startService(null); - assertFalse(getService().mInForeground); + assertThat(getService().mInForeground).isFalse(); // Recording getService().startRecording(); startService(null); - assertTrue(getService().mInForeground); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().reset(); } @@ -69,38 +71,38 @@ public class DvrRecordingServiceTest // Not recording startService(intent); - assertTrue(getService().mInForeground); - assertFalse(getService().mForegroundForUpcomingRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isFalse(); getService().stopForegroundIfNotRecordingInternal(); - assertFalse(getService().mInForeground); + assertThat(getService().mInForeground).isFalse(); // Recording, ended quickly getService().startRecording(); startService(intent); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopRecording(); - assertFalse(getService().mInForeground); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isFalse(); + assertThat(getService().mIsRecording).isFalse(); getService().stopForegroundIfNotRecordingInternal(); - assertFalse(getService().mInForeground); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isFalse(); + assertThat(getService().mIsRecording).isFalse(); getService().reset(); // Recording, ended later getService().startRecording(); startService(intent); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopForegroundIfNotRecordingInternal(); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopRecording(); - assertFalse(getService().mInForeground); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isFalse(); + assertThat(getService().mIsRecording).isFalse(); getService().reset(); } @@ -110,38 +112,39 @@ public class DvrRecordingServiceTest // Not recording startService(intent); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isFalse(); getService().startRecording(); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopRecording(); - assertFalse(getService().mInForeground); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isFalse(); + assertThat(getService().mIsRecording).isFalse(); getService().reset(); // Recording getService().startRecording(); startService(intent); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().startRecording(); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopRecording(); - assertTrue(getService().mInForeground); - assertTrue(getService().mForegroundForUpcomingRecording); - assertTrue(getService().mIsRecording); + assertThat(getService().mInForeground).isTrue(); + assertThat(getService().mForegroundForUpcomingRecording).isTrue(); + assertThat(getService().mIsRecording).isTrue(); getService().stopRecording(); - assertFalse(getService().mInForeground); - assertFalse(getService().mIsRecording); + assertThat(getService().mInForeground).isFalse(); + assertThat(getService().mIsRecording).isFalse(); getService().reset(); } + /** Mock {@link DvrRecordingService}. */ public static class MockDvrRecordingService extends DvrRecordingService { private int mRecordingCount = 0; private boolean mInForeground; diff --git a/tests/unit/src/com/android/tv/menu/MenuTest.java b/tests/unit/src/com/android/tv/menu/MenuTest.java index 9bdb8681..028a185d 100644 --- a/tests/unit/src/com/android/tv/menu/MenuTest.java +++ b/tests/unit/src/com/android/tv/menu/MenuTest.java @@ -16,13 +16,13 @@ package com.android.tv.menu; import static android.support.test.InstrumentationRegistry.getTargetContext; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertWithMessage; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import com.android.tv.menu.Menu.OnMenuVisibilityChangeListener; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Matchers; @@ -49,26 +49,27 @@ public class MenuTest { mMenu.disableAnimationForTest(); } + @Ignore("b/73727914") @Test public void testScheduleHide() { mMenu.show(Menu.REASON_NONE); setMenuVisible(true); - assertTrue("Hide is not scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is not scheduled").that(mMenu.isHideScheduled()).isTrue(); mMenu.hide(false); setMenuVisible(false); - assertFalse("Hide is scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is scheduled").that(mMenu.isHideScheduled()).isFalse(); mMenu.setKeepVisible(true); mMenu.show(Menu.REASON_NONE); setMenuVisible(true); - assertFalse("Hide is scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is scheduled").that(mMenu.isHideScheduled()).isFalse(); mMenu.setKeepVisible(false); - assertTrue("Hide is not scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is not scheduled").that(mMenu.isHideScheduled()).isTrue(); mMenu.setKeepVisible(true); - assertFalse("Hide is scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is scheduled").that(mMenu.isHideScheduled()).isFalse(); mMenu.hide(false); setMenuVisible(false); - assertFalse("Hide is scheduled", mMenu.isHideScheduled()); + assertWithMessage("Hide is scheduled").that(mMenu.isHideScheduled()).isFalse(); } @Test diff --git a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java index 7086e344..0f815a7a 100644 --- a/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java +++ b/tests/unit/src/com/android/tv/menu/TvOptionsRowAdapterTest.java @@ -16,7 +16,7 @@ package com.android.tv.menu; import static android.support.test.InstrumentationRegistry.getInstrumentation; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import android.media.tv.TvTrackInfo; @@ -73,9 +73,10 @@ public class TvOptionsRowAdapterTest extends BaseMainActivityTestCase { waitUntilAudioTrackSelected(Constants.EN_STEREO_AUDIO_TRACK.getId()); boolean result = mTvOptionsRowAdapter.updateMultiAudioAction(); - assertEquals("update Action had change", true, result); - assertEquals( - "Multi Audio enabled", true, MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + assertWithMessage("update Action had change").that(result).isTrue(); + assertWithMessage("Multi Audio enabled") + .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()) + .isTrue(); } @Test @@ -90,9 +91,10 @@ public class TvOptionsRowAdapterTest extends BaseMainActivityTestCase { waitUntilAudioTrackSelected(Constants.GENERIC_AUDIO_TRACK.getId()); boolean result = mTvOptionsRowAdapter.updateMultiAudioAction(); - assertEquals("update Action had change", true, result); - assertEquals( - "Multi Audio enabled", false, MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + assertWithMessage("update Action had change").that(result).isTrue(); + assertWithMessage("Multi Audio enabled") + .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()) + .isFalse(); } @Test @@ -108,9 +110,10 @@ public class TvOptionsRowAdapterTest extends BaseMainActivityTestCase { waitUntilVideoTrackSelected(data.mSelectedVideoTrackId); boolean result = mTvOptionsRowAdapter.updateMultiAudioAction(); - assertEquals("update Action had change", true, result); - assertEquals( - "Multi Audio enabled", false, MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()); + assertWithMessage("update Action had change").that(result).isTrue(); + assertWithMessage("Multi Audio enabled") + .that(MenuAction.SELECT_AUDIO_LANGUAGE_ACTION.isEnabled()) + .isFalse(); } private void waitUntilAudioTracksHaveSize(int expected) { diff --git a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java index ac0e0cac..e63bdc3a 100644 --- a/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java +++ b/tests/unit/src/com/android/tv/recommendation/ChannelRecordTest.java @@ -17,7 +17,7 @@ package com.android.tv.recommendation; import static android.support.test.InstrumentationRegistry.getContext; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertThat; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -47,14 +47,14 @@ public class ChannelRecordTest { @Test public void testGetLastWatchEndTime_noHistory() { - assertEquals(0, mChannelRecord.getLastWatchEndTimeMs()); + assertThat(mChannelRecord.getLastWatchEndTimeMs()).isEqualTo(0); } @Test public void testGetLastWatchEndTime_oneHistory() { addWatchLog(); - assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + assertThat(mChannelRecord.getLastWatchEndTimeMs()).isEqualTo(mLatestWatchEndTimeMs); } @Test @@ -63,7 +63,7 @@ public class ChannelRecordTest { addWatchLog(); } - assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + assertThat(mChannelRecord.getLastWatchEndTimeMs()).isEqualTo(mLatestWatchEndTimeMs); } @Test @@ -72,19 +72,19 @@ public class ChannelRecordTest { addWatchLog(); } - assertEquals(mLatestWatchEndTimeMs, mChannelRecord.getLastWatchEndTimeMs()); + assertThat(mChannelRecord.getLastWatchEndTimeMs()).isEqualTo(mLatestWatchEndTimeMs); } @Test public void testGetTotalWatchDuration_noHistory() { - assertEquals(0, mChannelRecord.getTotalWatchDurationMs()); + assertThat(mChannelRecord.getTotalWatchDurationMs()).isEqualTo(0); } @Test public void testGetTotalWatchDuration_oneHistory() { long durationMs = addWatchLog(); - assertEquals(durationMs, mChannelRecord.getTotalWatchDurationMs()); + assertThat(mChannelRecord.getTotalWatchDurationMs()).isEqualTo(durationMs); } @Test @@ -95,7 +95,7 @@ public class ChannelRecordTest { totalWatchTimeMs += durationMs; } - assertEquals(totalWatchTimeMs, mChannelRecord.getTotalWatchDurationMs()); + assertThat(mChannelRecord.getTotalWatchDurationMs()).isEqualTo(totalWatchTimeMs); } @Test @@ -110,8 +110,9 @@ public class ChannelRecordTest { } } - // Only latest CHANNEL_RECORD_MAX_HISTORY_SIZE logs are remained. - assertEquals(totalWatchTimeMs - firstDurationMs, mChannelRecord.getTotalWatchDurationMs()); + // Only latest CHANNEL_RECORD_MAX_HISTORY_SIZE logs are remained. + assertThat(mChannelRecord.getTotalWatchDurationMs()) + .isEqualTo(totalWatchTimeMs - firstDurationMs); } /** diff --git a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java index e3560467..e14320f0 100644 --- a/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/FavoriteChannelEvaluatorTest.java @@ -16,7 +16,7 @@ package com.android.tv.recommendation; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -106,7 +106,7 @@ public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase<FavoriteChan double previousScore = Recommender.Evaluator.NOT_RECOMMENDED; for (long channelId : channelIdList) { double score = mEvaluator.evaluateChannel(channelId); - assertTrue(previousScore <= score); + assertThat(previousScore).isAtMost(score); previousScore = score; } } @@ -125,8 +125,8 @@ public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase<FavoriteChan TimeUnit.MINUTES.toMillis(30)); notifyChannelAndWatchLogLoaded(); - assertTrue( - mEvaluator.evaluateChannel(channelOne) == mEvaluator.evaluateChannel(channelTwo)); + assertThat(mEvaluator.evaluateChannel(channelOne) == mEvaluator.evaluateChannel(channelTwo)) + .isTrue(); } @Test @@ -143,16 +143,18 @@ public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase<FavoriteChan TimeUnit.HOURS.toMillis(1)); notifyChannelAndWatchLogLoaded(); - // Channel two was watched longer than channel one, so it's score is bigger. - assertTrue(mEvaluator.evaluateChannel(channelOne) < mEvaluator.evaluateChannel(channelTwo)); + // Channel two was watched longer than channel one, so it's score is bigger. + assertThat(mEvaluator.evaluateChannel(channelOne)) + .isLessThan(mEvaluator.evaluateChannel(channelTwo)); addWatchLog( channelOne, System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1), TimeUnit.HOURS.toMillis(1)); - // Now, channel one was watched longer than channel two, so it's score is bigger. - assertTrue(mEvaluator.evaluateChannel(channelOne) > mEvaluator.evaluateChannel(channelTwo)); + // Now, channel one was watched longer than channel two, so it's score is bigger. + assertThat(mEvaluator.evaluateChannel(channelOne)) + .isGreaterThan(mEvaluator.evaluateChannel(channelTwo)); } @Test @@ -169,7 +171,7 @@ public class FavoriteChannelEvaluatorTest extends EvaluatorTestCase<FavoriteChan addWatchLog(channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10)); - // Score must be increased because total watch duration of the channel increases. - assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId)); + // Score must be increased because total watch duration of the channel increases. + assertThat(previousScore).isAtMost(mEvaluator.evaluateChannel(channelId)); } } diff --git a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java index 7fa09b7a..f8d6b220 100644 --- a/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RecentChannelEvaluatorTest.java @@ -16,7 +16,7 @@ package com.android.tv.recommendation; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -106,7 +106,7 @@ public class RecentChannelEvaluatorTest extends EvaluatorTestCase<RecentChannelE double previousScore = Recommender.Evaluator.NOT_RECOMMENDED; for (long channelId : channelIdList) { double score = mEvaluator.evaluateChannel(channelId); - assertTrue(previousScore <= score); + assertThat(previousScore).isAtMost(score); previousScore = score; } } @@ -129,8 +129,8 @@ public class RecentChannelEvaluatorTest extends EvaluatorTestCase<RecentChannelE addWatchLog(channelId, latestWatchEndTimeMs, durationMs); latestWatchEndTimeMs += durationMs; - // Score must be increased because recentness of the log increases. - assertTrue(previousScore <= mEvaluator.evaluateChannel(channelId)); + // Score must be increased because recentness of the log increases. + assertThat(previousScore).isAtMost(mEvaluator.evaluateChannel(channelId)); } } @@ -155,8 +155,8 @@ public class RecentChannelEvaluatorTest extends EvaluatorTestCase<RecentChannelE addWatchLog(newChannelId, latestWatchedEndTimeMs, TimeUnit.MINUTES.toMillis(10)); for (long channelId : channelIdList) { - // Score must be decreased because LastWatchLogUpdateTime increases by new log. - assertTrue(mEvaluator.evaluateChannel(channelId) <= scores.get(channelId)); + // Score must be decreased because LastWatchLogUpdateTime increases by new log. + assertThat(mEvaluator.evaluateChannel(channelId)).isAtMost(scores.get(channelId)); } } } diff --git a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java index f70c8e26..812a3eb1 100644 --- a/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RecommenderTest.java @@ -17,9 +17,7 @@ package com.android.tv.recommendation; import static android.support.test.InstrumentationRegistry.getContext; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -94,26 +92,26 @@ public class RecommenderTest { public void testRecommendChannels_includeRecommendedOnly_allChannelsHaveNoScore() { createRecommender(true, mStartDatamanagerRunnableAddFourChannels); - // Recommender doesn't recommend any channels because all channels are not recommended. - assertEquals(0, mRecommender.recommendChannels().size()); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); - assertEquals(0, mRecommender.recommendChannels(3).size()); - assertEquals(0, mRecommender.recommendChannels(4).size()); - assertEquals(0, mRecommender.recommendChannels(5).size()); + // Recommender doesn't recommend any channels because all channels are not recommended. + assertThat(mRecommender.recommendChannels()).isEmpty(); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); + assertThat(mRecommender.recommendChannels(3)).isEmpty(); + assertThat(mRecommender.recommendChannels(4)).isEmpty(); + assertThat(mRecommender.recommendChannels(5)).isEmpty(); } @Test public void testRecommendChannels_notIncludeRecommendedOnly_allChannelsHaveNoScore() { createRecommender(false, mStartDatamanagerRunnableAddFourChannels); - // Recommender recommends every channel because it recommends not-recommended channels too. - assertEquals(4, mRecommender.recommendChannels().size()); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); - assertEquals(3, mRecommender.recommendChannels(3).size()); - assertEquals(4, mRecommender.recommendChannels(4).size()); - assertEquals(4, mRecommender.recommendChannels(5).size()); + // Recommender recommends every channel because it recommends not-recommended channels too. + assertThat(mRecommender.recommendChannels()).hasSize(4); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); + assertThat(mRecommender.recommendChannels(3)).hasSize(3); + assertThat(mRecommender.recommendChannels(4)).hasSize(4); + assertThat(mRecommender.recommendChannels(5)).hasSize(4); } @Test @@ -126,8 +124,8 @@ public class RecommenderTest { // (i.e. sorted by channel ID in decreasing order in this case) MoreAsserts.assertContentsInOrder( mRecommender.recommendChannels(), mChannel_4, mChannel_3, mChannel_2, mChannel_1); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); MoreAsserts.assertContentsInOrder( mRecommender.recommendChannels(3), mChannel_4, mChannel_3, mChannel_2); MoreAsserts.assertContentsInOrder( @@ -146,8 +144,8 @@ public class RecommenderTest { // (i.e. sorted by channel ID in decreasing order in this case) MoreAsserts.assertContentsInOrder( mRecommender.recommendChannels(), mChannel_4, mChannel_3, mChannel_2, mChannel_1); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); MoreAsserts.assertContentsInOrder( mRecommender.recommendChannels(3), mChannel_4, mChannel_3, mChannel_2); MoreAsserts.assertContentsInOrder( @@ -166,8 +164,8 @@ public class RecommenderTest { // Only two channels are recommended because recommender doesn't recommend other channels. MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels(), mChannel_1, mChannel_2); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels(3), mChannel_1, mChannel_2); MoreAsserts.assertContentsInAnyOrder( @@ -183,22 +181,22 @@ public class RecommenderTest { mEvaluator.setChannelScore(mChannel_1.getId(), 1.0); mEvaluator.setChannelScore(mChannel_2.getId(), 1.0); - assertEquals(4, mRecommender.recommendChannels().size()); + assertThat(mRecommender.recommendChannels()).hasSize(4); MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels().subList(0, 2), mChannel_1, mChannel_2); - assertEquals(0, mRecommender.recommendChannels(-5).size()); - assertEquals(0, mRecommender.recommendChannels(0).size()); + assertThat(mRecommender.recommendChannels(-5)).isEmpty(); + assertThat(mRecommender.recommendChannels(0)).isEmpty(); - assertEquals(3, mRecommender.recommendChannels(3).size()); + assertThat(mRecommender.recommendChannels(3)).hasSize(3); MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels(3).subList(0, 2), mChannel_1, mChannel_2); - assertEquals(4, mRecommender.recommendChannels(4).size()); + assertThat(mRecommender.recommendChannels(4)).hasSize(4); MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels(4).subList(0, 2), mChannel_1, mChannel_2); - assertEquals(4, mRecommender.recommendChannels(5).size()); + assertThat(mRecommender.recommendChannels(5)).hasSize(4); MoreAsserts.assertContentsInAnyOrder( mRecommender.recommendChannels(5).subList(0, 2), mChannel_1, mChannel_2); } @@ -226,10 +224,9 @@ public class RecommenderTest { setChannelScores_scoreIncreasesAsChannelIdIncreases(); List<Channel> expectedChannelList = mRecommender.recommendChannels(3); - // A channel which is not recommended by the recommender has to get an invalid sort key. - assertEquals( - Recommender.INVALID_CHANNEL_SORT_KEY, - mRecommender.getChannelSortKey(mChannel_1.getId())); + // A channel which is not recommended by the recommender has to get an invalid sort key. + assertThat(mRecommender.getChannelSortKey(mChannel_1.getId())) + .isEqualTo(Recommender.INVALID_CHANNEL_SORT_KEY); List<Channel> channelList = Arrays.asList(mChannel_2, mChannel_3, mChannel_4); Collections.sort(channelList, mChannelSortKeyComparator); @@ -241,9 +238,9 @@ public class RecommenderTest { @Test public void testListener_onRecommendationChanged() { createRecommender(true, mStartDatamanagerRunnableAddFourChannels); - // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel - // doesn't have a watch log, nothing is recommended and recommendation isn't changed. - assertFalse(mOnRecommendationChanged); + // FakeEvaluator doesn't recommend a channel with empty watch log. As every channel + // doesn't have a watch log, nothing is recommended and recommendation isn't changed. + assertThat(mOnRecommendationChanged).isFalse(); // Set lastRecommendationUpdatedTimeUtcMs to check recommendation changed because, // recommender has a minimum recommendation update period. @@ -252,16 +249,17 @@ public class RecommenderTest { long latestWatchEndTimeMs = DEFAULT_WATCH_START_TIME_MS; for (long channelId : mChannelRecordSortedMap.keySet()) { mEvaluator.setChannelScore(channelId, 1.0); - // Add a log to recalculate the recommendation score. - assertTrue( - mChannelRecordSortedMap.addWatchLog( - channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10))); + // Add a log to recalculate the recommendation score. + assertThat( + mChannelRecordSortedMap.addWatchLog( + channelId, latestWatchEndTimeMs, TimeUnit.MINUTES.toMillis(10))) + .isTrue(); latestWatchEndTimeMs += TimeUnit.MINUTES.toMillis(10); } - // onRecommendationChanged must be called, because recommend channels are not empty, - // by setting score to each channel. - assertTrue(mOnRecommendationChanged); + // onRecommendationChanged must be called, because recommend channels are not empty, + // by setting score to each channel. + assertThat(mOnRecommendationChanged).isTrue(); } @Test @@ -279,8 +277,8 @@ public class RecommenderTest { } }); - // After loading channels and watch logs are finished, recommender must be available to use. - assertTrue(mOnRecommenderReady); + // After loading channels and watch logs are finished, recommender must be available to use. + assertThat(mOnRecommenderReady).isTrue(); } private void assertSortKeyNotInvalid(List<Channel> channelList) { diff --git a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java index 91d61c06..39e6e9c5 100644 --- a/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java +++ b/tests/unit/src/com/android/tv/recommendation/RoutineWatchEvaluatorTest.java @@ -16,21 +16,20 @@ package com.android.tv.recommendation; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import android.test.MoreAsserts; import com.android.tv.data.Program; import com.android.tv.recommendation.RoutineWatchEvaluator.ProgramTime; -import java.util.Arrays; import java.util.Calendar; import java.util.List; -import java.util.TreeSet; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; +/** Tests for {@link RoutineWatchEvaluator}. */ @SmallTest @RunWith(AndroidJUnit4.class) public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEvaluator> { @@ -67,13 +66,23 @@ public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEva @Test public void testSplitTextToWords() { - assertSplitTextToWords(""); - assertSplitTextToWords("Google", "Google"); - assertSplitTextToWords("The Big Bang Theory", "The", "Big", "Bang", "Theory"); - assertSplitTextToWords("Hello, world!", "Hello", "world"); - assertSplitTextToWords("Adam's Rib", "Adam's", "Rib"); - assertSplitTextToWords("G.I. Joe", "G.I", "Joe"); - assertSplitTextToWords("A.I.", "A.I"); + assertThat(RoutineWatchEvaluator.splitTextToWords("")).containsExactly().inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("Google")) + .containsExactly("Google") + .inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("The Big Bang Theory")) + .containsExactly("The", "Big", "Bang", "Theory") + .inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("Hello, world!")) + .containsExactly("Hello", "world") + .inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("Adam's Rib")) + .containsExactly("Adam's", "Rib") + .inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("G.I. Joe")) + .containsExactly("G.I", "Joe") + .inOrder(); + assertThat(RoutineWatchEvaluator.splitTextToWords("A.I.")).containsExactly("A.I").inOrder(); } @Test @@ -112,11 +121,15 @@ public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEva @Test public void testCalculateTitleMatchScore_longerMatchIsBetter() { String base = "foo bar baz"; - assertInOrder( - score(base, ""), - score(base, "bar"), - score(base, "foo bar"), - score(base, "foo bar baz")); + assertThat( + new ScoredItem[] { + score(base, ""), + score(base, "bar"), + score(base, "foo bar"), + score(base, "foo bar baz") + }) + .asList() + .isOrdered(); } @Test @@ -229,11 +242,6 @@ public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEva RoutineWatchEvaluator.getTimeOfDayInSec(todayAtHourMinSec(23, 59, 59))); } - private void assertSplitTextToWords(String text, String... words) { - List<String> wordList = RoutineWatchEvaluator.splitTextToWords(text); - MoreAsserts.assertContentsInOrder(wordList, words); - } - private void assertMaximumMatchedWordSequenceLength( int expectedLength, String text1, String text2) { List<String> wordList1 = RoutineWatchEvaluator.splitTextToWords(text1); @@ -317,9 +325,4 @@ public class RoutineWatchEvaluatorTest extends EvaluatorTestCase<RoutineWatchEva .setEndTimeUtcMillis(startTimeMs + programDurationMs) .build(); } - - private static <T> void assertInOrder(T... items) { - TreeSet<T> copy = new TreeSet<>(Arrays.asList(items)); - MoreAsserts.assertContentsInOrder(copy, items); - } } diff --git a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java index c4623bc7..d84a90d4 100644 --- a/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java +++ b/tests/unit/src/com/android/tv/util/TvTrackInfoUtilsTest.java @@ -16,7 +16,7 @@ package com.android.tv.util; import static com.android.tv.util.TvTrackInfoUtils.getBestTrackInfo; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertWithMessage; import android.media.tv.TvTrackInfo; import android.support.test.filters.SmallTest; @@ -60,49 +60,49 @@ public class TvTrackInfoUtilsTest { @Test public void testGetBestTrackInfo_empty() { TvTrackInfo result = getBestTrackInfo(Collections.emptyList(), UN_MATCHED_ID, "en", 1); - assertEquals("best track ", null, result); + assertWithMessage("best track ").that(result).isEqualTo(null); } @Test public void testGetBestTrackInfo_exactMatch() { TvTrackInfo result = getBestTrackInfo(ALL, "1", "en", 1); - assertEquals("best track ", INFO_1_EN_1, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); } @Test public void testGetBestTrackInfo_langAndChannelCountMatch() { TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "en", 5); - assertEquals("best track ", INFO_2_EN_5, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_2_EN_5); } @Test public void testGetBestTrackInfo_languageOnlyMatch() { TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "fr", 1); - assertEquals("best track ", INFO_3_FR_8, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8); } @Test public void testGetBestTrackInfo_channelCountOnlyMatchWithNullLanguage() { TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 8); - assertEquals("best track ", INFO_3_FR_8, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_3_FR_8); } @Test public void testGetBestTrackInfo_noMatches() { TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, "kr", 1); - assertEquals("best track ", INFO_1_EN_1, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); } @Test public void testGetBestTrackInfo_noMatchesWithNullLanguage() { TvTrackInfo result = getBestTrackInfo(ALL, UN_MATCHED_ID, null, 0); - assertEquals("best track ", INFO_1_EN_1, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_1_EN_1); } @Test public void testGetBestTrackInfo_channelCountAndIdMatch() { TvTrackInfo result = getBestTrackInfo(NULL_LANGUAGE_TRACKS, "5", null, 6); - assertEquals("best track ", INFO_5_NULL_6, result); + assertWithMessage("best track ").that(result).isEqualTo(INFO_5_NULL_6); } @Test diff --git a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java index b094adc3..b7715c4a 100644 --- a/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java +++ b/tests/unit/src/com/android/tv/util/images/ImageCacheTest.java @@ -17,7 +17,7 @@ package com.android.tv.util.images; import static com.android.tv.util.images.BitmapUtils.createScaledBitmapInfo; -import static org.junit.Assert.assertSame; +import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Bitmap; import android.support.test.filters.MediumTest; @@ -52,28 +52,28 @@ public class ImageCacheTest { public void testPutIfLarger_smaller() throws Exception { mImageCache.putIfNeeded(INFO_50); - assertSame("before", INFO_50, mImageCache.get(KEY)); + assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_50); mImageCache.putIfNeeded(INFO_25); - assertSame("after", INFO_50, mImageCache.get(KEY)); + assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_50); } @Test public void testPutIfLarger_larger() throws Exception { mImageCache.putIfNeeded(INFO_50); - assertSame("before", INFO_50, mImageCache.get(KEY)); + assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_50); mImageCache.putIfNeeded(INFO_100); - assertSame("after", INFO_100, mImageCache.get(KEY)); + assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_100); } @Test public void testPutIfLarger_alreadyMax() throws Exception { mImageCache.putIfNeeded(INFO_100); - assertSame("before", INFO_100, mImageCache.get(KEY)); + assertWithMessage("before").that(mImageCache.get(KEY)).isSameAs(INFO_100); mImageCache.putIfNeeded(INFO_200); - assertSame("after", INFO_100, mImageCache.get(KEY)); + assertWithMessage("after").that(mImageCache.get(KEY)).isSameAs(INFO_100); } } diff --git a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java index 0bde0981..005775b6 100644 --- a/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java +++ b/tests/unit/src/com/android/tv/util/images/ScaledBitmapInfoTest.java @@ -15,7 +15,7 @@ */ package com.android.tv.util.images; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertWithMessage; import android.graphics.Bitmap; import android.support.test.filters.SmallTest; @@ -58,10 +58,9 @@ public class ScaledBitmapInfoTest { private static void assertNeedsToReload( boolean expected, ScaledBitmapInfo scaledBitmap, int reqWidth, int reqHeight) { - assertEquals( - scaledBitmap.id + " needToReload(" + reqWidth + "," + reqHeight + ")", - expected, - scaledBitmap.needToReload(reqWidth, reqHeight)); + assertWithMessage(scaledBitmap.id + " needToReload(" + reqWidth + "," + reqHeight + ")") + .that(scaledBitmap.needToReload(reqWidth, reqHeight)) + .isEqualTo(expected); } private static void assertScaledBitmapSize( @@ -69,8 +68,12 @@ public class ScaledBitmapInfoTest { int expectedWidth, int expectedHeight, ScaledBitmapInfo actual) { - assertEquals(actual.id + " inSampleSize", expectedInSampleSize, actual.inSampleSize); - assertEquals(actual.id + " width", expectedWidth, actual.bitmap.getWidth()); - assertEquals(actual.id + " height", expectedHeight, actual.bitmap.getHeight()); + assertWithMessage(actual.id + " inSampleSize") + .that(actual.inSampleSize) + .isEqualTo(expectedInSampleSize); + assertWithMessage(actual.id + " width").that(actual.bitmap.getWidth()).isEqualTo(expectedWidth); + assertWithMessage(actual.id + " height") + .that(actual.bitmap.getHeight()) + .isEqualTo(expectedHeight); } } @@ -58,7 +58,7 @@ code_version_build := 001 ##################################################### ##################################################### # Collect automatic version code parameters -ifeq ($(strip $(HAS_BUILD_NUMBER)),false) +ifneq "" "$(filter eng.%,$(BUILD_NUMBER))" # This is an eng build base_version_buildtype := 0 else @@ -94,12 +94,12 @@ version_code_package := $(code_version_major)$(base_version_minor)$(code_version # and hh is the git hash # On eng builds, the BUILD_NUMBER has the user and timestamp inline ifdef TARGET_BUILD_APPS -ifeq ($(strip $(HAS_BUILD_NUMBER)),false) +ifneq "" "$(filter eng.%,$(BUILD_NUMBER))" git_hash := $(shell git --git-dir $(LOCAL_PATH)/.git log -n 1 --pretty=format:%h) date_string := $(shell date +%Y-%m-%d) version_name_package := $(base_version_major).$(base_version_minor).$(code_version_build) (eng.$(USER).$(git_hash).$(date_string)-$(base_version_arch)$(base_version_density)) else - version_name_package := $(base_version_major).$(base_version_minor).$(code_version_build) ($(BUILD_NUMBER_FROM_FILE)-$(base_version_arch)$(base_version_density)) + version_name_package := $(base_version_major).$(base_version_minor).$(code_version_build) ($(BUILD_NUMBER)-$(base_version_arch)$(base_version_density)) endif else # !TARGET_BUILD_APPS version_name_package := $(base_version_major).$(base_version_minor).$(code_version_build) |