- Added move mechanic to background player through handles (on both thumbnail and icon).

- Added remove and open detail as long click popup dropdown on background player.
- Vastly simplified list manipulation in MediaSourceManager by delegating most control to DynamicConcatenatingMediaSource.
This commit is contained in:
John Zhen M 2017-10-08 22:43:07 -07:00 committed by John Zhen Mo
parent f5b5982e1c
commit 2e414cfd63
10 changed files with 254 additions and 108 deletions

View File

@ -155,18 +155,6 @@ public final class BackgroundPlayer extends Service {
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
private void onClose() {
if (basePlayerImpl != null) {
basePlayerImpl.stopActivityBinding();

View File

@ -1,6 +1,7 @@
package org.schabi.newpipe.player;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Build;
@ -10,21 +11,28 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
@ -44,9 +52,14 @@ public class BackgroundPlayerActivity extends AppCompatActivity
// Views
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private View rootView;
private RecyclerView itemsList;
private ItemTouchHelper itemTouchHelper;
private TextView metadataTitle;
private TextView metadataArtist;
@ -157,14 +170,12 @@ public class BackgroundPlayerActivity extends AppCompatActivity
itemsList.setLayoutManager(new LinearLayoutManager(this));
itemsList.setAdapter(player.playQueueAdapter);
itemsList.setClickable(true);
itemsList.setLongClickable(true);
player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item) {
final int index = player.playQueue.indexOf(item);
if (index != -1) player.playQueue.setIndex(index);
}
});
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
player.playQueueAdapter.setSelectedListener(getOnSelectedListener());
}
private void buildMetadata() {
@ -192,6 +203,101 @@ public class BackgroundPlayerActivity extends AppCompatActivity
forwardButton.setOnClickListener(this);
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, "Remove");
remove.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
final int index = player.playQueue.indexOf(item);
if (index != -1) player.playQueue.remove(index);
return true;
}
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, "Detail");
detail.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
onOpenDetail(BackgroundPlayerActivity.this, item.getUrl(), item.getTitle());
return true;
}
});
menu.show();
}
////////////////////////////////////////////////////////////////////////////
// Component Helpers
////////////////////////////////////////////////////////////////////////////
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
player.playQueue.move(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
return new PlayQueueItemBuilder.OnSelectedListener() {
@Override
public void selected(PlayQueueItem item, View view) {
final int index = player.playQueue.indexOf(item);
if (index == -1) return;
if (player.playQueue.getIndex() == index) {
player.onRestart();
} else {
player.playQueue.setIndex(index);
}
}
@Override
public void held(PlayQueueItem item, View view) {
final int index = player.playQueue.indexOf(item);
if (index != -1) buildItemPopupMenu(item, view);
}
@Override
public void onStartDrag(PlayQueueItemHolder viewHolder) {
if (itemTouchHelper != null) itemTouchHelper.startDrag(viewHolder);
}
};
}
private void onOpenDetail(Context context, String videoUrl, String videoTitle) {
Intent i = new Intent(context, MainActivity.class);
i.putExtra(Constants.KEY_SERVICE_ID, 0);
i.putExtra(Constants.KEY_URL, videoUrl);
i.putExtra(Constants.KEY_TITLE, videoTitle);
i.putExtra(Constants.KEY_LINK_TYPE, StreamingService.LinkType.STREAM);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}
////////////////////////////////////////////////////////////////////////////
// Component On-Click Listener
////////////////////////////////////////////////////////////////////////////

View File

@ -559,27 +559,18 @@ public abstract class BasePlayer implements Player.EventListener,
}
/*//////////////////////////////////////////////////////////////////////////
// Timeline
// ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/
private void refreshTimeline() {
playbackManager.load();
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
final int currentSourceIndex = playbackManager.getCurrentSourceIndex();
// Sanity checks
if (currentSourceIndex < 0) return;
final int currentSourceIndex = playQueue.getIndex();
// Check if already playing correct window
final boolean isCurrentWindowCorrect = simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex;
// Check if on wrong window
if (!isCurrentWindowCorrect) {
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
}
// Check if recovering
if (isCurrentWindowCorrect && isRecovery && queuePos == playQueue.getIndex()) {
// todo: figure out exactly why this is the case
@ -591,17 +582,10 @@ public abstract class BasePlayer implements Player.EventListener,
simpleExoPlayer.seekTo(roundedPos);
isRecovery = false;
}
if (playbackManager != null) {
playbackManager.load();
}
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (DEBUG) Log.d(TAG, "onTimelineChanged(), timeline size = " + timeline.getWindowCount());
refreshTimeline();
}
@Override
@ -709,14 +693,12 @@ public abstract class BasePlayer implements Player.EventListener,
public void onPositionDiscontinuity() {
// Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
final int newQueueIndex = playbackManager.getQueueIndexOf(newWindowIndex);
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with: " +
"window index = [" + newWindowIndex + "], queue index = [" + newQueueIndex + "]");
if (DEBUG) Log.d(TAG, "onPositionDiscontinuity() called with window index = [" + newWindowIndex + "]");
// If the user selects a new track, then the discontinuity occurs after the index is changed.
// Therefore, the only source that causes a discrepancy would be autoplay,
// which can only offset the current track by +1.
if (newQueueIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
if (newWindowIndex != playQueue.getIndex()) playQueue.offsetIndex(+1);
}
@Override
@ -751,12 +733,16 @@ public abstract class BasePlayer implements Player.EventListener,
@Override
public void sync(@Nullable final StreamInfo info) {
if (simpleExoPlayer == null) return;
if (info == null || simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "Syncing...");
refreshTimeline();
if (info == null) return;
// Check if on wrong window
final int currentSourceIndex = playQueue.getIndex();
if (!(simpleExoPlayer.getCurrentWindowIndex() == currentSourceIndex)) {
final long startPos = currentInfo != null ? currentInfo.start_position : 0;
if (DEBUG) Log.d(TAG, "Rewinding to correct window: " + currentSourceIndex + " at: " + getTimeString((int)startPos));
simpleExoPlayer.seekTo(currentSourceIndex, startPos);
}
currentInfo = info;
initThumbnail(info.thumbnail_url);
@ -830,6 +816,13 @@ public abstract class BasePlayer implements Player.EventListener,
playQueue.offsetIndex(+1);
}
public void onRestart() {
if (playQueue == null) return;
if (DEBUG) Log.d(TAG, "onRestart() called");
simpleExoPlayer.seekToDefaultPosition();
}
public void seekBy(int milliSeconds) {
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))

View File

@ -12,6 +12,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.DeferredMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent;
@ -29,16 +31,12 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
// One-side rolling window size for default loading
// Effectively loads WINDOW_SIZE * 2 + 1 streams, should be at least 1 to ensure gapless playback
// todo: inject this parameter, allow user settings perhaps
private static final int WINDOW_SIZE = 2;
private static final int WINDOW_SIZE = 1;
private PlaybackListener playbackListener;
private PlayQueue playQueue;
private DynamicConcatenatingMediaSource sources;
// sourceToQueueIndex maps media source index to play queue index
// Invariant 1: this list is sorted in ascending order
// Invariant 2: this list contains no duplicates
private List<Integer> sourceToQueueIndex;
private Subscription playQueueReactor;
private SerialDisposable syncReactor;
@ -53,7 +51,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
this.syncReactor = new SerialDisposable();
this.sources = new DynamicConcatenatingMediaSource();
this.sourceToQueueIndex = Collections.synchronizedList(new ArrayList<Integer>());
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
@ -72,22 +69,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
/*//////////////////////////////////////////////////////////////////////////
// Exposed Methods
//////////////////////////////////////////////////////////////////////////*/
/**
* Returns the media source index of the currently playing stream.
* */
public int getCurrentSourceIndex() {
return sourceToQueueIndex.indexOf(playQueue.getIndex());
}
/**
* Returns the play queue index of a given media source playlist index.
* */
public int getQueueIndexOf(final int sourceIndex) {
if (sourceIndex < 0 || sourceIndex >= sourceToQueueIndex.size()) return -1;
return sourceToQueueIndex.get(sourceIndex);
}
/**
* Dispose the manager and releases all message buses and loaders.
* */
@ -95,12 +76,10 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
if (playQueueReactor != null) playQueueReactor.cancel();
if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource();
if (sourceToQueueIndex != null) sourceToQueueIndex.clear();
playQueueReactor = null;
syncReactor = null;
sources = null;
sourceToQueueIndex = null;
playbackListener = null;
playQueue = null;
}
@ -174,11 +153,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
populateSources();
break;
case SELECT:
if (isCurrentIndexLoaded()) {
sync();
} else {
reset();
}
break;
case REMOVE:
final RemoveEvent removeEvent = (RemoveEvent) event;
@ -188,8 +163,11 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
case REORDER:
reset();
break;
case ERROR:
case MOVE:
final MoveEvent moveEvent = (MoveEvent) event;
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case ERROR:
default:
break;
}
@ -214,10 +192,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
}
private boolean isCurrentIndexLoaded() {
return getCurrentSourceIndex() != -1;
}
private boolean tryBlock() {
if (!isBlocked) {
playbackListener.block();
@ -228,7 +202,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
}
private boolean tryUnblock() {
if (isPlayQueueReady() && isCurrentIndexLoaded() && isBlocked) {
if (isPlayQueueReady() && isBlocked) {
isBlocked = false;
playbackListener.unblock(sources);
return true;
@ -270,7 +244,6 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void resetSources() {
if (this.sources != null) this.sources.releaseSource();
if (this.sourceToQueueIndex != null) this.sourceToQueueIndex.clear();
this.sources = new DynamicConcatenatingMediaSource();
}
@ -294,12 +267,7 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void insert(final int queueIndex, final DeferredMediaSource source) {
if (queueIndex < 0) return;
int pos = Collections.binarySearch(sourceToQueueIndex, queueIndex);
if (pos < 0) {
final int sourceIndex = -pos-1;
sourceToQueueIndex.add(sourceIndex, queueIndex);
sources.addMediaSource(sourceIndex, source);
}
sources.addMediaSource(queueIndex, source);
}
/**
@ -310,15 +278,13 @@ public class MediaSourceManager implements DeferredMediaSource.Callback {
private void remove(final int queueIndex) {
if (queueIndex < 0) return;
final int sourceIndex = sourceToQueueIndex.indexOf(queueIndex);
if (sourceIndex == -1) return;
sourceToQueueIndex.remove(sourceIndex);
sources.removeMediaSource(sourceIndex);
// Will be slow on really large arrays, fast enough for typical use case
for (int i = sourceIndex; i < sourceToQueueIndex.size(); i++) {
sourceToQueueIndex.set(i, sourceToQueueIndex.get(i) - 1);
sources.removeMediaSource(queueIndex);
}
private void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
sources.moveMediaSource(source, target);
}
}

View File

@ -8,6 +8,7 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.InitEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
@ -272,6 +273,23 @@ public abstract class PlayQueue implements Serializable {
streams.remove(index);
}
public synchronized void move(final int source, final int target) {
if (source < 0 || target < 0) return;
if (source >= streams.size() || target >= streams.size()) return;
final int current = getIndex();
if (source == current) {
queueIndex.set(target);
} else if (source < current && target >= current) {
queueIndex.decrementAndGet();
} else if (source > current && target <= current) {
queueIndex.incrementAndGet();
}
streams.add(target, streams.remove(source));
broadcast(new MoveEvent(source, target));
}
/**
* Shuffles the current play queue.
*

View File

@ -9,6 +9,7 @@ import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.playlist.events.AppendEvent;
import org.schabi.newpipe.playlist.events.ErrorEvent;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueMessage;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.SelectEvent;
@ -131,6 +132,12 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
notifyItemRangeRemoved(removeEvent.index(), 1);
notifyItemChanged(removeEvent.index());
break;
case MOVE:
final MoveEvent moveEvent = (MoveEvent) message;
notifyItemMoved(moveEvent.getFromIndex(), moveEvent.getToIndex());
break;
case INIT:
case REORDER:
default:
notifyDataSetChanged();
break;

View File

@ -1,9 +1,8 @@
package org.schabi.newpipe.playlist;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
@ -17,7 +16,9 @@ public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
public interface OnSelectedListener {
void selected(PlayQueueItem item);
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
void onStartDrag(PlayQueueItemHolder viewHolder);
}
private OnSelectedListener onItemClickListener;
@ -28,7 +29,7 @@ public class PlayQueueItemBuilder {
this.onItemClickListener = listener;
}
public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) {
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
@ -44,10 +45,37 @@ public class PlayQueueItemBuilder {
@Override
public void onClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.selected(item);
onItemClickListener.selected(item, view);
}
}
});
holder.itemRoot.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (onItemClickListener != null) {
onItemClickListener.held(item, view);
return true;
}
return false;
}
});
holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder));
holder.itemHandle.setOnTouchListener(getOnTouchListener(holder));
}
private View.OnTouchListener getOnTouchListener(final PlayQueueItemHolder holder) {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
view.performClick();
if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
onItemClickListener.onStartDrag(holder);
}
return false;
}
};
}
private static final DisplayImageOptions IMAGE_OPTIONS =

View File

@ -32,7 +32,7 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
public final ImageView itemThumbnailView;
public final ImageView itemThumbnailView, itemHandle;
public final View itemRoot;
@ -43,5 +43,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
itemHandle = v.findViewById(R.id.itemHandle);
}
}

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.playlist.events;
public class MoveEvent implements PlayQueueMessage {
final private int fromIndex;
final private int toIndex;
@Override
public PlayQueueEvent type() {
return PlayQueueEvent.MOVE;
}
public MoveEvent(final int oldIndex, final int newIndex) {
this.fromIndex = oldIndex;
this.toIndex = newIndex;
}
public int getFromIndex() {
return fromIndex;
}
public int getToIndex() {
return toIndex;
}
}

View File

@ -23,6 +23,16 @@
android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/>
<ImageView
android:id="@+id/itemHandle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:layout_alignParentRight="true"
android:scaleType="center"
android:src="?attr/filter"
tools:ignore="ContentDescription,RtlHardcoded"/>
<TextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
@ -50,13 +60,15 @@
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
android:textColor="?attr/selector_color"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. "/>
<TextView
android:id="@+id/itemAdditionalDetails"
@ -65,9 +77,12 @@
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:layout_toLeftOf="@id/itemHandle"
android:layout_toStartOf="@id/itemHandle"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
android:textColor="?attr/selector_color"
tools:text="Uploader"/>
</RelativeLayout>