-Added MediaSourceManager and Playlist adapters.

This commit is contained in:
John Zhen M 2017-08-28 17:38:37 -07:00 committed by John Zhen Mo
parent e70dcdc642
commit cbcd281784
13 changed files with 753 additions and 21 deletions

View File

@ -79,6 +79,11 @@ public class PlaylistFragment extends BaseFragment {
private ImageView headerAvatarView;
private TextView headerTitleView;
/*////////////////////////////////////////////////////////////////////////*/
// Reactors
//////////////////////////////////////////////////////////////////////////*/
private Disposable loadingReactor;
/*////////////////////////////////////////////////////////////////////////*/
public PlaylistFragment() {
@ -153,8 +158,8 @@ public class PlaylistFragment extends BaseFragment {
public void onStop() {
if (DEBUG) Log.d(TAG, "onStop() called");
disposable.dispose();
disposable = null;
if (loadingReactor != null) loadingReactor.dispose();
loadingReactor = null;
super.onStop();
}
@ -221,7 +226,7 @@ public class PlaylistFragment extends BaseFragment {
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
playlistStreams = (RecyclerView) rootView.findViewById(R.id.channel_streams_view);
playlistStreams = rootView.findViewById(R.id.channel_streams_view);
playlistStreams.setLayoutManager(new LinearLayoutManager(activity));
if (infoListAdapter == null) {
@ -238,9 +243,9 @@ public class PlaylistFragment extends BaseFragment {
infoListAdapter.setHeader(headerRootLayout);
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, playlistStreams, false));
headerBannerView = (ImageView) headerRootLayout.findViewById(R.id.playlist_banner_image);
headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.playlist_avatar_view);
headerTitleView = (TextView) headerRootLayout.findViewById(R.id.playlist_title_view);
headerBannerView = headerRootLayout.findViewById(R.id.playlist_banner_image);
headerAvatarView = headerRootLayout.findViewById(R.id.playlist_avatar_view);
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
}
protected void initListeners() {
@ -280,7 +285,71 @@ public class PlaylistFragment extends BaseFragment {
return NewPipe.getService(serviceId);
}
Disposable disposable;
private void loadAll() {
final Callable<PlayListInfo> task = new Callable<PlayListInfo>() {
@Override
public PlayListInfo call() throws Exception {
int pageCount = 0;
final PlayListExtractor extractor = getService(serviceId)
.getPlayListExtractorInstance(playlistUrl, 0);
final PlayListInfo info = PlayListInfo.getInfo(extractor);
boolean hasNext = info.hasNextPage;
while(hasNext) {
pageCount++;
final PlayListExtractor moreExtractor = getService(serviceId)
.getPlayListExtractorInstance(playlistUrl, pageCount);
final PlayListInfo moreInfo = PlayListInfo.getInfo(moreExtractor);
info.related_streams.addAll(moreInfo.related_streams);
hasNext = moreInfo.hasNextPage;
}
return info;
}
};
Observable.fromCallable(task)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<PlayListInfo>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (loadingReactor == null || loadingReactor.isDisposed()) {
loadingReactor = d;
isLoading.set(true);
} else {
d.dispose();
}
}
@Override
public void onNext(@NonNull PlayListInfo playListInfo) {
if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + playListInfo + "]");
if (playListInfo == null || isRemoving() || !isVisible()) return;
handlePlayListInfo(playListInfo, false, true);
isLoading.set(false);
pageNumber++;
}
@Override
public void onError(@NonNull Throwable e) {
onRxError(e, "Observer failure");
}
@Override
public void onComplete() {
if (loadingReactor != null) {
loadingReactor.dispose();
loadingReactor = null;
}
}
});
}
private void loadMore(final boolean onlyVideos) {
final Callable<PlayListInfo> task = new Callable<PlayListInfo>() {
@ -300,8 +369,8 @@ public class PlaylistFragment extends BaseFragment {
.subscribe(new Observer<PlayListInfo>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (disposable == null || disposable.isDisposed()) {
disposable = d;
if (loadingReactor == null || loadingReactor.isDisposed()) {
loadingReactor = d;
isLoading.set(true);
} else {
d.dispose();
@ -325,9 +394,9 @@ public class PlaylistFragment extends BaseFragment {
@Override
public void onComplete() {
if (disposable != null) {
disposable.dispose();
disposable = null;
if (loadingReactor != null) {
loadingReactor.dispose();
loadingReactor = null;
}
}
});

View File

@ -1,4 +0,0 @@
package org.schabi.newpipe.fragments.search;
public class PlaylistService {
}

View File

@ -344,13 +344,15 @@ public class BackgroundPlayer extends Service {
@Override
public void onFastRewind() {
super.onFastRewind();
// super.onFastRewind();
simpleExoPlayer.seekTo(0, 0);
triggerProgressUpdate();
}
@Override
public void onFastForward() {
super.onFastForward();
// super.onFastForward();
simpleExoPlayer.seekTo(2, 0);
triggerProgressUpdate();
}

View File

@ -47,7 +47,9 @@ import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.LoopingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
@ -248,7 +250,12 @@ public abstract class BasePlayer implements Player.EventListener, AudioManager.O
changeState(STATE_LOADING);
isPrepared = false;
mediaSource = buildMediaSource(url, format);
final MediaSource ms = buildMediaSource(url, format);
final DynamicConcatenatingMediaSource dcms = new DynamicConcatenatingMediaSource();
dcms.addMediaSource(ms);
mediaSource = dcms;
dcms.addMediaSource(new LoopingMediaSource(ms, 2));
if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop();
if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos);

View File

@ -0,0 +1,21 @@
package org.schabi.newpipe.player;
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.Playlist;
import java.util.List;
public class MediaSourceManager {
private DynamicConcatenatingMediaSource source;
private Playlist playlist;
private List<MediaSource> sources;
public MediaSourceManager(Playlist playlist) {
this.source = new DynamicConcatenatingMediaSource();
this.playlist = playlist;
}
}

View File

@ -0,0 +1,99 @@
package org.schabi.newpipe.playlist;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlayListExtractor;
import org.schabi.newpipe.extractor.playlist.PlayListInfo;
import org.schabi.newpipe.extractor.playlist.PlayListInfoItem;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.Maybe;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class ExternalPlaylist extends Playlist {
private AtomicInteger pageNumber;
private StreamingService service;
public ExternalPlaylist(final PlayListInfoItem playlist) {
super();
service = getService(playlist.serviceId);
pageNumber = new AtomicInteger(0);
load(playlist);
}
private void load(final PlayListInfoItem playlist) {
final int page = pageNumber.getAndIncrement();
final Callable<PlayListInfo> task = new Callable<PlayListInfo>() {
@Override
public PlayListInfo call() throws Exception {
PlayListExtractor extractor = service.getPlayListExtractorInstance(playlist.getLink(), page);
return PlayListInfo.getInfo(extractor);
}
};
final Consumer<PlayListInfo> onSuccess = new Consumer<PlayListInfo>() {
@Override
public void accept(PlayListInfo playListInfo) throws Exception {
streams.addAll(extractPlaylistItems(playListInfo));
changeBroadcast.onNext(streams);
}
};
Maybe.fromCallable(task)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.onErrorComplete()
.subscribe(onSuccess);
}
private List<PlaylistItem> extractPlaylistItems(final PlayListInfo info) {
List<PlaylistItem> result = new ArrayList<>();
for (final InfoItem stream : info.related_streams) {
if (stream instanceof StreamInfoItem) {
result.add(new PlaylistItem((StreamInfoItem) stream));
}
}
return result;
}
@Override
boolean isComplete() {
return false;
}
@Override
void load(int index) {
while (streams.size() < index) {
pageNumber.incrementAndGet();
}
}
@Override
Observable<StreamInfo> get(int index) {
return null;
}
private StreamingService getService(final int serviceId) {
try {
return NewPipe.getService(serviceId);
} catch (ExtractionException e) {
return null;
}
}
}

View File

@ -0,0 +1,38 @@
package org.schabi.newpipe.playlist;
import android.support.annotation.NonNull;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
public abstract class Playlist {
private final String TAG = "Playlist@" + Integer.toHexString(hashCode());
private final int LOAD_BOUND = 2;
List<PlaylistItem> streams;
PublishSubject<List<PlaylistItem>> changeBroadcast;
Playlist() {
streams = Collections.synchronizedList(new ArrayList<PlaylistItem>());
changeBroadcast = PublishSubject.create();
}
@NonNull
public PublishSubject<List<PlaylistItem>> getChangeBroadcast() {
return changeBroadcast;
}
abstract boolean isComplete();
abstract void load(int index);
abstract Observable<StreamInfo> get(int index);
}

View File

@ -0,0 +1,154 @@
package org.schabi.newpipe.playlist;
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.info_list.StreamInfoItemHolder;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoListAdapter.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlaylistAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = PlaylistAdapter.class.toString();
private final PlaylistItemBuilder playlistItemBuilder;
private final List<PlaylistItem> playlistItems;
private boolean showFooter = false;
private View header = null;
private View footer = null;
public class HFHolder extends RecyclerView.ViewHolder {
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public void showFooter(boolean show) {
showFooter = show;
notifyDataSetChanged();
}
public PlaylistAdapter(List<PlaylistItem> data) {
playlistItemBuilder = new PlaylistItemBuilder();
playlistItems = data;
}
public void setSelectedListener(PlaylistItemBuilder.OnSelectedListener listener) {
playlistItemBuilder.setOnSelectedListener(listener);
}
public void addInfoItemList(List<PlaylistItem> data) {
if(data != null) {
playlistItems.addAll(data);
notifyDataSetChanged();
}
}
public void addInfoItem(PlaylistItem data) {
if (data != null) {
playlistItems.add(data);
notifyDataSetChanged();
}
}
public void clearStreamItemList() {
if(playlistItems.isEmpty()) {
return;
}
playlistItems.clear();
notifyDataSetChanged();
}
public void setHeader(View header) {
this.header = header;
notifyDataSetChanged();
}
public void setFooter(View footer) {
this.footer = footer;
notifyDataSetChanged();
}
public List<PlaylistItem> getItemsList() {
return playlistItems;
}
@Override
public int getItemCount() {
int count = playlistItems.size();
if(header != null) count++;
if(footer != null && showFooter) count++;
return count;
}
// don't ask why we have to do that this way... it's android accept it -.-
@Override
public int getItemViewType(int position) {
if(header != null && position == 0) {
return 0;
} else if(header != null) {
position--;
}
if(footer != null && position == playlistItems.size() && showFooter) {
return 1;
}
return 2;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
case 0:
return new HFHolder(header);
case 1:
return new HFHolder(footer);
case 2:
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.playlist_stream_item, parent, false));
default:
Log.e(TAG, "Trollolo");
return null;
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
if(holder instanceof PlaylistItemHolder) {
if(header != null) {
i--;
}
playlistItemBuilder.buildStreamInfoItem((PlaylistItemHolder) holder, playlistItems.get(i));
} else if(holder instanceof HFHolder && i == 0 && header != null) {
((HFHolder) holder).view = header;
} else if(holder instanceof HFHolder && i == playlistItems.size() && footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}
}

View File

@ -0,0 +1,109 @@
package org.schabi.newpipe.playlist;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
import java.io.Serializable;
import java.util.concurrent.Callable;
import io.reactivex.Maybe;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class PlaylistItem implements Serializable {
private String title;
private String url;
private int serviceId;
private int duration;
private boolean isDone;
private Throwable error;
private Maybe<StreamInfo> stream;
public PlaylistItem(final StreamInfoItem streamInfoItem) {
this.title = streamInfoItem.getTitle();
this.url = streamInfoItem.getLink();
this.serviceId = streamInfoItem.service_id;
this.duration = streamInfoItem.duration;
this.isDone = false;
this.stream = getInfo();
}
@NonNull
public String getTitle() {
return title;
}
@NonNull
public String getUrl() {
return url;
}
public int getServiceId() {
return serviceId;
}
public int getDuration() {
return duration;
}
public boolean isDone() {
return isDone;
}
@Nullable
public Throwable getError() {
return error;
}
@NonNull
public Maybe<StreamInfo> getStream() {
return stream;
}
public void load() {
stream.subscribe();
}
@NonNull
private Maybe<StreamInfo> getInfo() {
final Callable<StreamInfo> task = new Callable<StreamInfo>() {
@Override
public StreamInfo call() throws Exception {
final StreamExtractor extractor = NewPipe.getService(serviceId).getExtractorInstance(url);
return StreamInfo.getVideoInfo(extractor);
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
error = throwable;
}
};
final Action onComplete = new Action() {
@Override
public void run() throws Exception {
isDone = true;
}
};
return Maybe.fromCallable(task)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnError(onError)
.onErrorComplete()
.doOnComplete(onComplete)
.cache();
}
}

View File

@ -0,0 +1,116 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.playlist.PlayListInfoItem;
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
import org.schabi.newpipe.info_list.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.InfoItemHolder;
import org.schabi.newpipe.info_list.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.StreamInfoItemHolder;
import java.util.Locale;
/**
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlaylistItemBuilder {
private static final String TAG = PlaylistItemBuilder.class.toString();
public interface OnSelectedListener {
void selected(int serviceId, String url, String title);
}
private OnSelectedListener onStreamInfoItemSelectedListener;
public PlaylistItemBuilder() {}
public void setOnSelectedListener(OnSelectedListener listener) {
this.onStreamInfoItemSelectedListener = listener;
}
public View buildView(ViewGroup parent, final PlaylistItem item) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View itemView = inflater.inflate(R.layout.stream_item, parent, false);
final PlaylistItemHolder holder = new PlaylistItemHolder(itemView);
buildStreamInfoItem(holder, item);
return itemView;
}
public void buildStreamInfoItem(PlaylistItemHolder holder, final PlaylistItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (item.getDuration() > 0) {
holder.itemDurationView.setText(getDurationString(item.getDuration()));
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(onStreamInfoItemSelectedListener != null) {
onStreamInfoItemSelectedListener.selected(item.getServiceId(), item.getUrl(), item.getTitle());
}
}
});
}
public static String getDurationString(int duration) {
if(duration < 0) {
duration = 0;
}
String output;
int days = duration / (24 * 60 * 60); /* greater than a day */
duration %= (24 * 60 * 60);
int hours = duration / (60 * 60); /* greater than an hour */
duration %= (60 * 60);
int minutes = duration / 60;
int seconds = duration % 60;
//handle days
if (days > 0) {
output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
} else if(hours > 0) {
output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
} else {
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
}
return output;
}
}

View File

@ -0,0 +1,43 @@
package org.schabi.newpipe.playlist;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemHolder;
/**
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class PlaylistItemHolder extends RecyclerView.ViewHolder {
public final TextView itemVideoTitleView, itemDurationView;
public final View itemRoot;
public PlaylistItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -328,4 +328,12 @@
tools:visibility="visible"/>
</RelativeLayout>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/video_playlist"
android:layout_width="480dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="#64000000"
android:visibility="gone"/>
</RelativeLayout>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemRoot"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:padding="6dp">
<ImageView
android:id="@+id/itemThumbnailView"
android:layout_width="62dp"
android:layout_height="40dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
android:contentDescription="@string/list_thumbnail_view_description"
android:scaleType="centerCrop"
android:src="@drawable/dummy_thumbnail"
tools:ignore="RtlHardcoded"/>
<TextView
android:id="@+id/itemDurationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@id/itemThumbnailView"
android:layout_alignRight="@id/itemThumbnailView"
android:layout_marginBottom="@dimen/video_item_search_duration_margin"
android:layout_marginRight="@dimen/video_item_search_duration_margin"
android:background="@color/duration_background_color"
android:paddingBottom="@dimen/video_item_search_duration_vertical_padding"
android:paddingLeft="@dimen/video_item_search_duration_horizontal_padding"
android:paddingRight="@dimen/video_item_search_duration_horizontal_padding"
android:paddingTop="@dimen/video_item_search_duration_vertical_padding"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="@color/duration_text_color"
android:textSize="@dimen/video_item_search_duration_text_size"
tools:ignore="RtlHardcoded"
tools:text="1:09:10"/>
<TextView
android:id="@+id/itemVideoTitleView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="@dimen/video_item_search_title_text_size"
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
<TextView
android:id="@+id/itemAdditionalDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@id/itemThumbnailView"
android:layout_toEndOf="@id/itemThumbnailView"
android:lines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textSize="@dimen/video_item_search_upload_date_text_size"
tools:text="Uploader • 2 years ago • 10M views"/>
</RelativeLayout>