Surf mode experiment (#1003)
* surf mode implementation * occupy entire vertical area * fix onResume logic * shuffle mode with selected channel ids
This commit is contained in:
parent
722c829502
commit
9b9ef9ab74
13 changed files with 904 additions and 32 deletions
|
@ -188,9 +188,10 @@ import io.lbry.browser.ui.other.AboutFragment;
|
||||||
import io.lbry.browser.ui.publish.PublishFormFragment;
|
import io.lbry.browser.ui.publish.PublishFormFragment;
|
||||||
import io.lbry.browser.ui.publish.PublishFragment;
|
import io.lbry.browser.ui.publish.PublishFragment;
|
||||||
import io.lbry.browser.ui.publish.PublishesFragment;
|
import io.lbry.browser.ui.publish.PublishesFragment;
|
||||||
import io.lbry.browser.ui.findcontent.SearchFragment;
|
|
||||||
import io.lbry.browser.ui.other.SettingsFragment;
|
|
||||||
import io.lbry.browser.ui.findcontent.AllContentFragment;
|
import io.lbry.browser.ui.findcontent.AllContentFragment;
|
||||||
|
import io.lbry.browser.ui.findcontent.SearchFragment;
|
||||||
|
import io.lbry.browser.ui.findcontent.ShuffleFragment;
|
||||||
|
import io.lbry.browser.ui.other.SettingsFragment;
|
||||||
import io.lbry.browser.ui.wallet.InvitesFragment;
|
import io.lbry.browser.ui.wallet.InvitesFragment;
|
||||||
import io.lbry.browser.ui.wallet.RewardsFragment;
|
import io.lbry.browser.ui.wallet.RewardsFragment;
|
||||||
import io.lbry.browser.ui.wallet.WalletFragment;
|
import io.lbry.browser.ui.wallet.WalletFragment;
|
||||||
|
@ -214,6 +215,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
private static final String SPECIAL_URL_PREFIX = "lbry://?";
|
private static final String SPECIAL_URL_PREFIX = "lbry://?";
|
||||||
private static final int REMOTE_NOTIFICATION_REFRESH_TTL = 300000; // 5 minutes
|
private static final int REMOTE_NOTIFICATION_REFRESH_TTL = 300000; // 5 minutes
|
||||||
public static final String SKU_SKIP = "lbryskip";
|
public static final String SKU_SKIP = "lbryskip";
|
||||||
|
|
||||||
|
public static final int SOURCE_NOW_PLAYING_FILE = 1;
|
||||||
|
public static final int SOURCE_NOW_PLAYING_SHUFFLE = 2;
|
||||||
public static MainActivity instance;
|
public static MainActivity instance;
|
||||||
|
|
||||||
private boolean shuttingDown;
|
private boolean shuttingDown;
|
||||||
|
@ -232,6 +236,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
public static boolean playerReassigned;
|
public static boolean playerReassigned;
|
||||||
public CastContext castContext;
|
public CastContext castContext;
|
||||||
public static CastPlayer castPlayer;
|
public static CastPlayer castPlayer;
|
||||||
|
public static int nowPlayingSource;
|
||||||
public static Claim nowPlayingClaim;
|
public static Claim nowPlayingClaim;
|
||||||
public static String nowPlayingClaimUrl;
|
public static String nowPlayingClaimUrl;
|
||||||
public static boolean startingFilePickerActivity = false;
|
public static boolean startingFilePickerActivity = false;
|
||||||
|
@ -264,6 +269,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
fragmentClassNavIdMap.put(FollowingFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
|
fragmentClassNavIdMap.put(FollowingFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
|
||||||
fragmentClassNavIdMap.put(EditorsChoiceFragment.class, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
|
fragmentClassNavIdMap.put(EditorsChoiceFragment.class, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
|
||||||
fragmentClassNavIdMap.put(AllContentFragment.class, NavMenuItem.ID_ITEM_ALL_CONTENT);
|
fragmentClassNavIdMap.put(AllContentFragment.class, NavMenuItem.ID_ITEM_ALL_CONTENT);
|
||||||
|
fragmentClassNavIdMap.put(ShuffleFragment.class, NavMenuItem.ID_ITEM_SHUFFLE);
|
||||||
|
|
||||||
fragmentClassNavIdMap.put(PublishFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH);
|
fragmentClassNavIdMap.put(PublishFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH);
|
||||||
fragmentClassNavIdMap.put(ChannelManagerFragment.class, NavMenuItem.ID_ITEM_CHANNELS);
|
fragmentClassNavIdMap.put(ChannelManagerFragment.class, NavMenuItem.ID_ITEM_CHANNELS);
|
||||||
|
@ -580,7 +586,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) {
|
if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) {
|
||||||
hideNotifications();
|
hideNotifications();
|
||||||
openFileUrl(nowPlayingClaimUrl);
|
if (nowPlayingSource == SOURCE_NOW_PLAYING_SHUFFLE) {
|
||||||
|
openFragment(ShuffleFragment.class, true, NavMenuItem.ID_ITEM_SHUFFLE);
|
||||||
|
} else {
|
||||||
|
openFileUrl(nowPlayingClaimUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -658,6 +668,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
specialRouteFragmentClassMap.put("settings", SettingsFragment.class);
|
specialRouteFragmentClassMap.put("settings", SettingsFragment.class);
|
||||||
specialRouteFragmentClassMap.put("subscription", FollowingFragment.class);
|
specialRouteFragmentClassMap.put("subscription", FollowingFragment.class);
|
||||||
specialRouteFragmentClassMap.put("subscriptions", FollowingFragment.class);
|
specialRouteFragmentClassMap.put("subscriptions", FollowingFragment.class);
|
||||||
|
specialRouteFragmentClassMap.put("surf", ShuffleFragment.class);
|
||||||
specialRouteFragmentClassMap.put("wallet", WalletFragment.class);
|
specialRouteFragmentClassMap.put("wallet", WalletFragment.class);
|
||||||
specialRouteFragmentClassMap.put("discover", FollowingFragment.class);
|
specialRouteFragmentClassMap.put("discover", FollowingFragment.class);
|
||||||
}
|
}
|
||||||
|
@ -813,6 +824,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
case NavMenuItem.ID_ITEM_ALL_CONTENT:
|
case NavMenuItem.ID_ITEM_ALL_CONTENT:
|
||||||
openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT);
|
openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT);
|
||||||
break;
|
break;
|
||||||
|
case NavMenuItem.ID_ITEM_SHUFFLE:
|
||||||
|
openFragment(ShuffleFragment.class, true, NavMenuItem.ID_ITEM_SHUFFLE);
|
||||||
|
break;
|
||||||
|
|
||||||
case NavMenuItem.ID_ITEM_NEW_PUBLISH:
|
case NavMenuItem.ID_ITEM_NEW_PUBLISH:
|
||||||
openFragment(PublishFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH);
|
openFragment(PublishFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH);
|
||||||
|
@ -1027,7 +1041,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
fragment instanceof LibraryFragment ||
|
fragment instanceof LibraryFragment ||
|
||||||
fragment instanceof SearchFragment;
|
fragment instanceof SearchFragment;
|
||||||
findViewById(R.id.floating_balance_main_container).setVisibility(!canShowFloatingBalance || inFullscreenMode ? View.INVISIBLE : View.VISIBLE);
|
findViewById(R.id.floating_balance_main_container).setVisibility(!canShowFloatingBalance || inFullscreenMode ? View.INVISIBLE : View.VISIBLE);
|
||||||
if (!(fragment instanceof FileViewFragment) && !inFullscreenMode) {
|
if (!(fragment instanceof FileViewFragment) && !(fragment instanceof ShuffleFragment) && !inFullscreenMode) {
|
||||||
findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
|
findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
/*if (!Lbry.SDK_READY && !inFullscreenMode) {
|
/*if (!Lbry.SDK_READY && !inFullscreenMode) {
|
||||||
|
@ -3025,7 +3039,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
|
||||||
findContentGroup.setItems(Arrays.asList(
|
findContentGroup.setItems(Arrays.asList(
|
||||||
new NavMenuItem(NavMenuItem.ID_ITEM_FOLLOWING, R.string.fa_heart, R.string.following, "Following", context),
|
new NavMenuItem(NavMenuItem.ID_ITEM_FOLLOWING, R.string.fa_heart, R.string.following, "Following", context),
|
||||||
new NavMenuItem(NavMenuItem.ID_ITEM_EDITORS_CHOICE, R.string.fa_star, R.string.editors_choice, "EditorsChoice", context),
|
new NavMenuItem(NavMenuItem.ID_ITEM_EDITORS_CHOICE, R.string.fa_star, R.string.editors_choice, "EditorsChoice", context),
|
||||||
new NavMenuItem(NavMenuItem.ID_ITEM_ALL_CONTENT, R.string.fa_globe_americas, R.string.all_content, "AllContent", context)
|
new NavMenuItem(NavMenuItem.ID_ITEM_ALL_CONTENT, R.string.fa_globe_americas, R.string.all_content, "AllContent", context),
|
||||||
|
new NavMenuItem(NavMenuItem.ID_ITEM_SHUFFLE, R.string.fa_random, R.string.shuffle, "Shuffle",context)
|
||||||
));
|
));
|
||||||
|
|
||||||
yourContentGroup.setItems(Arrays.asList(
|
yourContentGroup.setItems(Arrays.asList(
|
||||||
|
|
|
@ -20,6 +20,7 @@ public class NavMenuItem {
|
||||||
public static final int ID_ITEM_FOLLOWING = 101;
|
public static final int ID_ITEM_FOLLOWING = 101;
|
||||||
public static final int ID_ITEM_EDITORS_CHOICE = 102;
|
public static final int ID_ITEM_EDITORS_CHOICE = 102;
|
||||||
public static final int ID_ITEM_ALL_CONTENT = 103;
|
public static final int ID_ITEM_ALL_CONTENT = 103;
|
||||||
|
public static final int ID_ITEM_SHUFFLE = 104;
|
||||||
|
|
||||||
// Your Content
|
// Your Content
|
||||||
public static final int ID_ITEM_CHANNELS = 201;
|
public static final int ID_ITEM_CHANNELS = 201;
|
||||||
|
|
|
@ -51,8 +51,8 @@ public class CommentListTask extends AsyncTask<Void, Void, List<Comment>> {
|
||||||
options.put("skip_validation", true);
|
options.put("skip_validation", true);
|
||||||
options.put("visible", true);
|
options.put("visible", true);
|
||||||
|
|
||||||
|
|
||||||
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options);
|
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_COMMENT_LIST, options);
|
||||||
|
android.util.Log.d("LbryMain", result.toString(2));
|
||||||
JSONArray items = result.getJSONArray("items");
|
JSONArray items = result.getJSONArray("items");
|
||||||
|
|
||||||
List<Comment> children = new ArrayList<>();
|
List<Comment> children = new ArrayList<>();
|
||||||
|
@ -78,6 +78,7 @@ public class CommentListTask extends AsyncTask<Void, Void, List<Comment>> {
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
error = ex;
|
error = ex;
|
||||||
|
android.util.Log.d("LbryMain", ex.toString(), ex);
|
||||||
}
|
}
|
||||||
return comments;
|
return comments;
|
||||||
}
|
}
|
||||||
|
|
|
@ -227,6 +227,8 @@ public class ChannelContentFragment extends Fragment implements DownloadActionLi
|
||||||
null,
|
null,
|
||||||
getContentSortOrder(),
|
getContentSortOrder(),
|
||||||
contentReleaseTime,
|
contentReleaseTime,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
||||||
Helper.CONTENT_PAGE_SIZE);
|
Helper.CONTENT_PAGE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -414,6 +414,8 @@ public class AllContentFragment extends BaseFragment implements DownloadActionLi
|
||||||
null,
|
null,
|
||||||
getContentSortOrder(),
|
getContentSortOrder(),
|
||||||
contentReleaseTime,
|
contentReleaseTime,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
||||||
Helper.CONTENT_PAGE_SIZE);
|
Helper.CONTENT_PAGE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ActivityInfo;
|
import android.content.pm.ActivityInfo;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Outline;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
@ -37,7 +36,6 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.AppCompatSpinner;
|
import androidx.appcompat.widget.AppCompatSpinner;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.core.widget.NestedScrollView;
|
import androidx.core.widget.NestedScrollView;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
@ -167,7 +165,7 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
WalletBalanceListener {
|
WalletBalanceListener {
|
||||||
private static final int RELATED_CONTENT_SIZE = 16;
|
private static final int RELATED_CONTENT_SIZE = 16;
|
||||||
private static final String DEFAULT_PLAYBACK_SPEED = "1x";
|
private static final String DEFAULT_PLAYBACK_SPEED = "1x";
|
||||||
private static final String CDN_PREFIX = "https://cdn.lbryplayer.xyz";
|
public static final String CDN_PREFIX = "https://cdn.lbryplayer.xyz";
|
||||||
|
|
||||||
private PlayerControlView castControlView;
|
private PlayerControlView castControlView;
|
||||||
private Player currentPlayer;
|
private Player currentPlayer;
|
||||||
|
@ -741,6 +739,13 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onPause() {
|
||||||
|
if (MainActivity.appPlayer != null) {
|
||||||
|
MainActivity.nowPlayingSource = MainActivity.SOURCE_NOW_PLAYING_FILE;
|
||||||
|
}
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
public void onStop() {
|
public void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
@ -2781,7 +2786,7 @@ public class FileViewFragment extends BaseFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class StreamLoadErrorPolicy extends DefaultLoadErrorHandlingPolicy {
|
public static class StreamLoadErrorPolicy extends DefaultLoadErrorHandlingPolicy {
|
||||||
@Override
|
@Override
|
||||||
public long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {
|
public long getRetryDelayMsFor(int dataType, long loadDurationMs, IOException exception, int errorCount) {
|
||||||
return exception instanceof ParserException
|
return exception instanceof ParserException
|
||||||
|
|
|
@ -434,6 +434,8 @@ public class FollowingFragment extends BaseFragment implements
|
||||||
null,
|
null,
|
||||||
getContentSortOrder(),
|
getContentSortOrder(),
|
||||||
contentReleaseTime,
|
contentReleaseTime,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
||||||
Helper.CONTENT_PAGE_SIZE);
|
Helper.CONTENT_PAGE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,748 @@
|
||||||
|
package io.lbry.browser.ui.findcontent;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.GestureDetector;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||||
|
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||||
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import io.lbry.browser.MainActivity;
|
||||||
|
import io.lbry.browser.R;
|
||||||
|
import io.lbry.browser.exceptions.LbryUriException;
|
||||||
|
import io.lbry.browser.model.Claim;
|
||||||
|
import io.lbry.browser.model.lbryinc.Reward;
|
||||||
|
import io.lbry.browser.tasks.BufferEventTask;
|
||||||
|
import io.lbry.browser.tasks.GenericTaskHandler;
|
||||||
|
import io.lbry.browser.tasks.claim.ClaimSearchResultHandler;
|
||||||
|
import io.lbry.browser.tasks.claim.ClaimSearchTask;
|
||||||
|
import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
|
||||||
|
import io.lbry.browser.tasks.lbryinc.LogFileViewTask;
|
||||||
|
import io.lbry.browser.ui.BaseFragment;
|
||||||
|
import io.lbry.browser.utils.Helper;
|
||||||
|
import io.lbry.browser.utils.Lbry;
|
||||||
|
import io.lbry.browser.utils.LbryAnalytics;
|
||||||
|
import io.lbry.browser.utils.LbryUri;
|
||||||
|
import io.lbry.browser.utils.Lbryio;
|
||||||
|
import io.lbry.browser.utils.Predefined;
|
||||||
|
|
||||||
|
public class ShuffleFragment extends BaseFragment {
|
||||||
|
|
||||||
|
private static final int PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
private int currentClaimSearchPage;
|
||||||
|
private int playlistIndex;
|
||||||
|
private Claim current;
|
||||||
|
private List<Claim> playlist;
|
||||||
|
|
||||||
|
private long sessionStart;
|
||||||
|
private ProgressBar surfModeLoading;
|
||||||
|
private TextView textTitle;
|
||||||
|
private TextView textPublisher;
|
||||||
|
private Player player;
|
||||||
|
private long elapsedDuration = 0;
|
||||||
|
private long totalDuration = 0;
|
||||||
|
private boolean elapsedPlaybackScheduled;
|
||||||
|
private ScheduledExecutorService elapsedPlaybackScheduler;
|
||||||
|
private boolean playbackStarted;
|
||||||
|
private long startTimeMillis;
|
||||||
|
private boolean isPlaying;
|
||||||
|
private boolean newPlayerCreated;
|
||||||
|
private String currentUrl;
|
||||||
|
private Player.EventListener playerListener;
|
||||||
|
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||||
|
ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
View root = inflater.inflate(R.layout.fragment_shuffle, container, false);
|
||||||
|
|
||||||
|
surfModeLoading = root.findViewById(R.id.shuffle_loading);
|
||||||
|
textTitle = root.findViewById(R.id.shuffle_content_title);
|
||||||
|
textPublisher = root.findViewById(R.id.shuffle_content_publisher);
|
||||||
|
playerListener = new Player.EventListener() {
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||||
|
if (playbackState == Player.STATE_READY) {
|
||||||
|
elapsedDuration = MainActivity.appPlayer.getCurrentPosition();
|
||||||
|
totalDuration = MainActivity.appPlayer.getDuration() < 0 ? 0 : MainActivity.appPlayer.getDuration();
|
||||||
|
if (!playbackStarted) {
|
||||||
|
logPlay(currentUrl, startTimeMillis);
|
||||||
|
playbackStarted = true;
|
||||||
|
isPlaying = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTotalDuration();
|
||||||
|
scheduleElapsedPlayback();
|
||||||
|
hideBuffering();
|
||||||
|
} else if (playbackState == Player.STATE_BUFFERING) {
|
||||||
|
Context ctx = getContext();
|
||||||
|
boolean sendBufferingEvents = true;
|
||||||
|
|
||||||
|
if (ctx != null) {
|
||||||
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(ctx);
|
||||||
|
sendBufferingEvents = sp.getBoolean(MainActivity.PREFERENCE_KEY_SEND_BUFFERING_EVENTS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MainActivity.appPlayer != null && MainActivity.appPlayer.getCurrentPosition() > 0 && sendBufferingEvents) {
|
||||||
|
// we only want to log a buffer event after the media has already started playing
|
||||||
|
String mediaSourceUrl = getStreamingUrl();
|
||||||
|
long duration = MainActivity.appPlayer.getDuration();
|
||||||
|
long position = MainActivity.appPlayer.getCurrentPosition();
|
||||||
|
// TODO: Determine a hash for the userId
|
||||||
|
String userIdHash = Helper.SHA256(Lbryio.currentUser != null ? String.valueOf(Lbryio.currentUser.getId()) : "0");
|
||||||
|
if (mediaSourceUrl.startsWith(FileViewFragment.CDN_PREFIX)) {
|
||||||
|
BufferEventTask bufferEvent = new BufferEventTask(current.getPermanentUrl(), duration, position, 1, userIdHash);
|
||||||
|
bufferEvent.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showBuffering();
|
||||||
|
} else if (playbackState == Player.STATE_ENDED) {
|
||||||
|
playNextClaim();
|
||||||
|
} else {
|
||||||
|
hideBuffering();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Context context = getContext();
|
||||||
|
PlayerView playerView = root.findViewById(R.id.shuffle_exoplayer_view);
|
||||||
|
playerView.setOnTouchListener(new SwipeListener(playerView, context) {
|
||||||
|
@Override
|
||||||
|
public void onSwipeLeft() { playNextClaim(); }
|
||||||
|
@Override
|
||||||
|
public void onSwipeRight() { playPreviousClaim(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
root.findViewById(R.id.shuffle_share_button).setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (current != null) {
|
||||||
|
try {
|
||||||
|
String shareUrl = LbryUri.parse(
|
||||||
|
!Helper.isNullOrEmpty(current.getCanonicalUrl()) ? current.getCanonicalUrl() :
|
||||||
|
(!Helper.isNullOrEmpty(current.getShortUrl()) ? current.getShortUrl() : current.getPermanentUrl())).toTvString();
|
||||||
|
Intent shareIntent = new Intent();
|
||||||
|
shareIntent.setAction(Intent.ACTION_SEND);
|
||||||
|
shareIntent.setType("text/plain");
|
||||||
|
shareIntent.putExtra(Intent.EXTRA_TEXT, shareUrl);
|
||||||
|
|
||||||
|
MainActivity.startingShareActivity = true;
|
||||||
|
Intent shareUrlIntent = Intent.createChooser(shareIntent, getString(R.string.share_lbry_content));
|
||||||
|
shareUrlIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
startActivity(shareUrlIntent);
|
||||||
|
} catch (LbryUriException ex) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
root.setOnTouchListener(new SwipeListener(root, context) {
|
||||||
|
@Override
|
||||||
|
public void onSwipeLeft() { playNextClaim(); }
|
||||||
|
@Override
|
||||||
|
public void onSwipeRight() { playPreviousClaim(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStreamingUrl() {
|
||||||
|
return current != null ? String.format("https://cdn.lbryplayer.xyz/content/claims/%s/%s/stream", current.getName(), current.getClaimId()) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildContentOptions() {
|
||||||
|
Context context = getContext();
|
||||||
|
boolean canShowMatureContent = false;
|
||||||
|
if (context != null) {
|
||||||
|
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||||
|
canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Lbry.buildClaimSearchOptions(
|
||||||
|
Claim.TYPE_STREAM,
|
||||||
|
canShowMatureContent ? null : new ArrayList<>(Predefined.MATURE_TAGS),
|
||||||
|
null/*contentChannelIds*/,
|
||||||
|
Arrays.asList(Claim.ORDER_BY_TRENDING_GROUP),
|
||||||
|
121, // 2 minutes or less
|
||||||
|
1,
|
||||||
|
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
|
||||||
|
PAGE_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
MainActivity activity = (MainActivity) getContext();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.hideFloatingWalletBalance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
sessionStart = System.currentTimeMillis();
|
||||||
|
MainActivity activity = (MainActivity) getContext();
|
||||||
|
if (activity != null) {
|
||||||
|
LbryAnalytics.setCurrentScreen(activity, "Shuffle", "Shuffle");
|
||||||
|
}
|
||||||
|
if (MainActivity.appPlayer != null && MainActivity.nowPlayingSource != MainActivity.SOURCE_NOW_PLAYING_SHUFFLE) {
|
||||||
|
MainActivity.appPlayer.setPlayWhenReady(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playlist == null) {
|
||||||
|
loadContent();
|
||||||
|
} else {
|
||||||
|
if (current != null) {
|
||||||
|
playbackCurrentClaim();
|
||||||
|
} else {
|
||||||
|
startPlaylist();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPause() {
|
||||||
|
if (MainActivity.appPlayer != null && MainActivity.appPlayer.isPlaying()) {
|
||||||
|
MainActivity.nowPlayingSource = MainActivity.SOURCE_NOW_PLAYING_SHUFFLE;
|
||||||
|
}
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onStop() {
|
||||||
|
long sessionDuration = System.currentTimeMillis() - sessionStart;
|
||||||
|
if (sessionStart > 0 && sessionDuration > 0) {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putLong("duration_ms", sessionDuration);
|
||||||
|
bundle.putInt("duration", Double.valueOf(Math.ceil(sessionDuration / 1000.0)).intValue());
|
||||||
|
LbryAnalytics.logEvent(LbryAnalytics.EVENT_SHUFFLE_SESSION, bundle);
|
||||||
|
}
|
||||||
|
sessionStart = 0;
|
||||||
|
|
||||||
|
MainActivity activity = (MainActivity) getContext();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.hideFloatingWalletBalance();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadContent() {
|
||||||
|
if (playlist == null || playlist.size() == 0) {
|
||||||
|
Helper.setViewVisibility(surfModeLoading, View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> claimSearchOptions = buildContentOptions();
|
||||||
|
ClaimSearchTask task = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimSearchResultHandler() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
|
||||||
|
if (playlist == null) {
|
||||||
|
playlist = new ArrayList<>(claims);
|
||||||
|
startPlaylist();
|
||||||
|
} else {
|
||||||
|
for (Claim claim : claims) {
|
||||||
|
if (!playlist.contains(claim)) {
|
||||||
|
playlist.add(claim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Helper.setViewVisibility(surfModeLoading, View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception error) {
|
||||||
|
Helper.setViewVisibility(surfModeLoading, View.GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPlaylist() {
|
||||||
|
if (playlist == null || playlist.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playlistIndex = 0;
|
||||||
|
current = playlist.get(playlistIndex);
|
||||||
|
checkCurrentClaimIsVideo(false);
|
||||||
|
playbackCurrentClaim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCurrentClaimIsVideo(boolean previous) {
|
||||||
|
while (current == null || current.getMediaType() == null || !current.getMediaType().startsWith("video")) {
|
||||||
|
// only play videos
|
||||||
|
if (previous) {
|
||||||
|
playlistIndex--;
|
||||||
|
} else {
|
||||||
|
playlistIndex++;
|
||||||
|
}
|
||||||
|
current = playlist.get(playlistIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playPreviousClaim() {
|
||||||
|
if (playlist == null || playlist.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (playlistIndex > 0) {
|
||||||
|
playlistIndex--;
|
||||||
|
}
|
||||||
|
current = playlist.get(playlistIndex);
|
||||||
|
checkCurrentClaimIsVideo(true);
|
||||||
|
playbackCurrentClaim();
|
||||||
|
}
|
||||||
|
private void playNextClaim() {
|
||||||
|
if (playlist == null || playlist.size() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (playlistIndex < playlist.size() - 1) {
|
||||||
|
playlistIndex++;
|
||||||
|
}
|
||||||
|
if (playlist.size() - playlistIndex < 10) {
|
||||||
|
currentClaimSearchPage++;
|
||||||
|
loadContent();
|
||||||
|
}
|
||||||
|
current = playlist.get(playlistIndex);
|
||||||
|
checkCurrentClaimIsVideo(false);
|
||||||
|
playbackCurrentClaim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playbackCurrentClaim() {
|
||||||
|
resetPlayer();
|
||||||
|
String publisherText = !Helper.isNullOrEmpty(current.getPublisherTitle()) ?
|
||||||
|
String.format("%s (%s)", current.getPublisherTitle(), current.getPublisherName()) :
|
||||||
|
!Helper.isNullOrEmpty(current.getPublisherName()) ? current.getPublisherName() : getString(R.string.anonymous);
|
||||||
|
|
||||||
|
textTitle.setText(current.getTitle());
|
||||||
|
textPublisher.setText(publisherText);
|
||||||
|
|
||||||
|
Context context = getContext();
|
||||||
|
if (MainActivity.appPlayer == null && context != null) {
|
||||||
|
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||||
|
.setUsage(C.USAGE_MEDIA)
|
||||||
|
.setContentType(C.CONTENT_TYPE_MOVIE)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
MainActivity.appPlayer = new SimpleExoPlayer.Builder(context).build();
|
||||||
|
MainActivity.appPlayer.setAudioAttributes(audioAttributes, true);
|
||||||
|
MainActivity.playerCache =
|
||||||
|
new SimpleCache(context.getCacheDir(),
|
||||||
|
new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 256), new ExoDatabaseProvider(context));
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
MainActivity activity = (MainActivity) context;
|
||||||
|
activity.initMediaSession();
|
||||||
|
activity.initPlaybackNotification();
|
||||||
|
}
|
||||||
|
if (playerListener != null) {
|
||||||
|
MainActivity.appPlayer.addListener(playerListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
newPlayerCreated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
MainActivity activity = (MainActivity) context;
|
||||||
|
activity.initPlaybackNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
View root = getView();
|
||||||
|
if (root != null) {
|
||||||
|
PlayerView view = root.findViewById(R.id.shuffle_exoplayer_view);
|
||||||
|
view.setShutterBackgroundColor(Color.TRANSPARENT);
|
||||||
|
view.setPlayer(MainActivity.appPlayer);
|
||||||
|
view.setUseController(true);
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
((MainActivity) context).getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MainActivity.nowPlayingClaim != null &&
|
||||||
|
MainActivity.nowPlayingClaim.getClaimId().equalsIgnoreCase(current.getClaimId()) &&
|
||||||
|
!newPlayerCreated) {
|
||||||
|
// if the claim is already playing, we don't need to reload the media source
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MainActivity.appPlayer != null) {
|
||||||
|
showBuffering();
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
((MainActivity) context).setNowPlayingClaim(current, current.getPermanentUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
MainActivity.appPlayer.setPlayWhenReady(true);
|
||||||
|
String userAgent = Util.getUserAgent(context, getString(R.string.app_name));
|
||||||
|
String mediaSourceUrl = getStreamingUrl();
|
||||||
|
MediaSource mediaSource = new ProgressiveMediaSource.Factory(
|
||||||
|
new CacheDataSourceFactory(MainActivity.playerCache, new DefaultDataSourceFactory(context, userAgent)),
|
||||||
|
new DefaultExtractorsFactory()
|
||||||
|
).setLoadErrorHandlingPolicy(new FileViewFragment.StreamLoadErrorPolicy()).createMediaSource(Uri.parse(mediaSourceUrl));
|
||||||
|
|
||||||
|
MainActivity.appPlayer.prepare(mediaSource, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetPlayer() {
|
||||||
|
elapsedDuration = 0;
|
||||||
|
totalDuration = 0;
|
||||||
|
renderElapsedDuration();
|
||||||
|
renderTotalDuration();
|
||||||
|
|
||||||
|
elapsedPlaybackScheduled = false;
|
||||||
|
if (elapsedPlaybackScheduler != null) {
|
||||||
|
elapsedPlaybackScheduler.shutdownNow();
|
||||||
|
elapsedPlaybackScheduler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
playbackStarted = false;
|
||||||
|
startTimeMillis = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (MainActivity.appPlayer != null) {
|
||||||
|
MainActivity.appPlayer.stop(true);
|
||||||
|
MainActivity.appPlayer.removeListener(fileViewPlayerListener);
|
||||||
|
PlaybackParameters params = new PlaybackParameters(1.0f);
|
||||||
|
MainActivity.appPlayer.setPlaybackParameters(params);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logPlay(String url, long startTimeMillis) {
|
||||||
|
long timeToStartMillis = startTimeMillis > 0 ? System.currentTimeMillis() - startTimeMillis : 0;
|
||||||
|
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
bundle.putString("uri", url);
|
||||||
|
bundle.putLong("time_to_start_ms", timeToStartMillis);
|
||||||
|
bundle.putLong("time_to_start_seconds", Double.valueOf(timeToStartMillis / 1000.0).longValue());
|
||||||
|
LbryAnalytics.logEvent(LbryAnalytics.EVENT_PLAY, bundle);
|
||||||
|
|
||||||
|
logFileView(current.getPermanentUrl(), timeToStartMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logFileView(String url, long timeToStart) {
|
||||||
|
if (current != null) {
|
||||||
|
LogFileViewTask task = new LogFileViewTask(url, current, timeToStart, new GenericTaskHandler() {
|
||||||
|
@Override
|
||||||
|
public void beforeStart() { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess() {
|
||||||
|
claimEligibleRewards();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception error) { }
|
||||||
|
});
|
||||||
|
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBuffering() {
|
||||||
|
View root = getView();
|
||||||
|
if (root != null) {
|
||||||
|
root.findViewById(R.id.player_buffering_progress).setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
PlayerView playerView = root.findViewById(R.id.shuffle_exoplayer_view);
|
||||||
|
playerView.findViewById(R.id.player_skip_back_10).setVisibility(View.INVISIBLE);
|
||||||
|
playerView.findViewById(R.id.player_skip_forward_10).setVisibility(View.INVISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideBuffering() {
|
||||||
|
View root = getView();
|
||||||
|
if (root != null) {
|
||||||
|
root.findViewById(R.id.player_buffering_progress).setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
PlayerView playerView = root.findViewById(R.id.shuffle_exoplayer_view);
|
||||||
|
playerView.findViewById(R.id.player_skip_back_10).setVisibility(View.VISIBLE);
|
||||||
|
playerView.findViewById(R.id.player_skip_forward_10).setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderElapsedDuration() {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
Helper.setViewText(view.findViewById(R.id.player_duration_elapsed), Helper.formatDuration(Double.valueOf(elapsedDuration / 1000.0).longValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderTotalDuration() {
|
||||||
|
View view = getView();
|
||||||
|
if (view != null) {
|
||||||
|
Helper.setViewText(view.findViewById(R.id.player_duration_total), Helper.formatDuration(Double.valueOf(totalDuration / 1000.0).longValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAndScheduleDurations() {
|
||||||
|
if (MainActivity.appPlayer != null && playbackStarted) {
|
||||||
|
elapsedDuration = MainActivity.appPlayer.getCurrentPosition() < 0 ? 0 : MainActivity.appPlayer.getCurrentPosition();
|
||||||
|
totalDuration = MainActivity.appPlayer.getDuration() < 0 ? 0 : MainActivity.appPlayer.getDuration();
|
||||||
|
|
||||||
|
renderElapsedDuration();
|
||||||
|
renderTotalDuration();
|
||||||
|
scheduleElapsedPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleElapsedPlayback() {
|
||||||
|
if (!elapsedPlaybackScheduled) {
|
||||||
|
elapsedPlaybackScheduler = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
elapsedPlaybackScheduler.scheduleAtFixedRate(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Context context = getContext();
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
((MainActivity) context).runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (MainActivity.appPlayer != null) {
|
||||||
|
elapsedDuration = MainActivity.appPlayer.getCurrentPosition();
|
||||||
|
int elapsedSeconds = Double.valueOf(elapsedDuration / 1000.0).intValue();
|
||||||
|
renderElapsedDuration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0, 500, TimeUnit.MILLISECONDS);
|
||||||
|
elapsedPlaybackScheduled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move this call to MainActivity?
|
||||||
|
private void claimEligibleRewards() {
|
||||||
|
// attempt to claim eligible rewards after viewing or playing a file (fail silently)
|
||||||
|
Context context = getContext();
|
||||||
|
ClaimRewardTask firstStreamTask = new ClaimRewardTask(Reward.TYPE_FIRST_STREAM, null, null, context, eligibleRewardHandler);
|
||||||
|
ClaimRewardTask dailyViewTask = new ClaimRewardTask(Reward.TYPE_DAILY_VIEW, null, null, context, eligibleRewardHandler);
|
||||||
|
firstStreamTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
dailyViewTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClaimRewardTask.ClaimRewardHandler eligibleRewardHandler = new ClaimRewardTask.ClaimRewardHandler() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(double amountClaimed, String message) {
|
||||||
|
if (Helper.isNullOrEmpty(message)) {
|
||||||
|
message = getResources().getQuantityString(
|
||||||
|
R.plurals.claim_reward_message,
|
||||||
|
amountClaimed == 1 ? 1 : 2,
|
||||||
|
new DecimalFormat(Helper.LBC_CURRENCY_FORMAT_PATTERN).format(amountClaimed));
|
||||||
|
}
|
||||||
|
Context context = getContext();
|
||||||
|
if (context instanceof MainActivity) {
|
||||||
|
((MainActivity) context).showMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Exception error) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static class SwipeListener implements View.OnTouchListener {
|
||||||
|
private final GestureDetector gestureDetector;
|
||||||
|
private final View control;
|
||||||
|
public SwipeListener(View control, Context context) {
|
||||||
|
this.control = control;
|
||||||
|
gestureDetector = new GestureDetector(context, new SwipeGestureListener());
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(View view, MotionEvent motionEvent) {
|
||||||
|
return gestureDetector.onTouchEvent(motionEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSwipeLeft() { }
|
||||||
|
public void onSwipeRight() { }
|
||||||
|
public void onSwipeTop() { }
|
||||||
|
public void onSwipeBottom() { }
|
||||||
|
|
||||||
|
private final class SwipeGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||||
|
private static final int SWIPE_THRESHOLD = 100;
|
||||||
|
private static final int SWIPE_VELOCITY_THRESHOLD = 100;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onDown(MotionEvent e) {
|
||||||
|
if (control instanceof PlayerView) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
||||||
|
boolean result = false;
|
||||||
|
try {
|
||||||
|
float diffY = e2.getY() - e1.getY();
|
||||||
|
float diffX = e2.getX() - e1.getX();
|
||||||
|
if (Math.abs(diffX) > Math.abs(diffY)) {
|
||||||
|
if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffX > 0) {
|
||||||
|
onSwipeRight();
|
||||||
|
} else {
|
||||||
|
onSwipeLeft();
|
||||||
|
}
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
|
||||||
|
if (diffY > 0) {
|
||||||
|
onSwipeBottom();
|
||||||
|
} else {
|
||||||
|
onSwipeTop();
|
||||||
|
}
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldHideGlobalPlayer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<String> contentChannelIds = Arrays.asList(
|
||||||
|
"3fec094c5937e9eb4e8f5e71e4ca430e8a993d03",
|
||||||
|
"6184648aab0431c4c95c649072d1f9ff08b9bb7c",
|
||||||
|
"b5d31cde873073718c033076656a27471e392afc",
|
||||||
|
"7317cdf6f62be93b22295062e191f6ba59a5db26",
|
||||||
|
"1cdb5d0bdcb484907d0a2fea4efdfe0153838642",
|
||||||
|
"b516294f541a18ce00b71a60b2c82ad2f87ff78d",
|
||||||
|
"91e42cc450075f2c4c245bac7617bf903f16b4ce",
|
||||||
|
"b6e207c5f8c58e7c8362cd05a1501bf2f5b694f2",
|
||||||
|
"25f384bd95e218f6ac37fcaca99ed40f36760d8c",
|
||||||
|
"f33657a2fcbab2dc3ce555d5d6728f8758af7bc7",
|
||||||
|
"294f5c164da5ac9735658b2d58d8fee6745dfc45",
|
||||||
|
"119a2e8c0b50f78d3861636d37c3b44ba8e689b5",
|
||||||
|
"7b23cca3f49059f005e812be03931c81272eaac4",
|
||||||
|
"fb0efeaa3788d1292bb49a94d77622503fe08129",
|
||||||
|
"797a528c49b6535560f7fd8222b121b0223287c8",
|
||||||
|
"bc490776f367b8afccf0ea7349d657431ba1ded6",
|
||||||
|
"48c7ea8bc2c4adba09bf21a29689e3b8c2967522",
|
||||||
|
"bf7490f905904e79de5c90e472bb9e6f26e634a0",
|
||||||
|
"df961194a798cc76306b9290701130c592530fb6",
|
||||||
|
"cf0be9078d76951e2e228df68b5b0bbf71313aaa",
|
||||||
|
"d746ac8d782f94d12d176c7a591f5bf8365bef3d",
|
||||||
|
"1f30267438257020f08abf452746a48e53a71ad5",
|
||||||
|
"4ad942982e43326c7700b1b6443049b3cfd82161",
|
||||||
|
"1cdb5d0bdcb484907d0a2fea4efdfe0153838642",
|
||||||
|
"6616707e1109aaa1c11b9f399f914d0cfb4f5303",
|
||||||
|
"4ee7cfaf1fc50a6df858ed0b99c278d633bccca9",
|
||||||
|
"b7d02b4a0036114732c072269adb891dc5e34ca4",
|
||||||
|
"9c51c1a119137cd17ed5ae09daa80c1cab6ac01d",
|
||||||
|
"5f2a5c14b971a6f5eed0a67dc7af3a3fe5c0b6a4",
|
||||||
|
"0e2b5b4cf59e859860000ff123dc12a317ad416b",
|
||||||
|
"3fe68ad3da93065e35c37b14fbeef88b4b7785ed",
|
||||||
|
"fd7ffcbafb74412a8812df4720feaf11fe70fe12",
|
||||||
|
"b4c30fe36b79870a79c55e1e909adb5ad23f323f",
|
||||||
|
"92c0f2f3239f1f61496997bd2cdc197ec51bd423",
|
||||||
|
"29193e9240a71a735639c66ee954e68414f11236",
|
||||||
|
"25f384bd95e218f6ac37fcaca99ed40f36760d8c",
|
||||||
|
"87b13b074936b1f42a7c6758c7c2995f58c602e7",
|
||||||
|
"8d935c6c30510e1dfc10f803a9646fa8aa128b07",
|
||||||
|
"8f4fecfc836ea33798ee3e5cef56926fa54e2cf9",
|
||||||
|
"9a5dfcb1a4b29c3a1598392d039744b9938b5a26",
|
||||||
|
"c5724e280283cd985186af9a62494aae377daabd",
|
||||||
|
"b39833be3032bbe1005f4f719f379a4621faeb13",
|
||||||
|
"589276465a23c589801d874f484cc39f307d7ec7",
|
||||||
|
"fb364ef587872515f545a5b4b3182b58073f230f",
|
||||||
|
"6c0bf1fed2705d675da950a94e4af004ec975a06",
|
||||||
|
"b924ac36b7499591f7929d9c4903de79b07b1cb9",
|
||||||
|
"113515e893b8186595595e594ecc410bae50c026",
|
||||||
|
"72f9815b087b6d346745e3de71a6ce5fe73a8677",
|
||||||
|
"b0198a465290f065378f3535666bee0653d6a9bb",
|
||||||
|
"020ebeb40642bfb4bc3d9f6d28c098afc0a47481",
|
||||||
|
"5c15e604c4207f52c8cf58fe21e63164c230e257",
|
||||||
|
"273a2fa759f1a9f56b078633ea2f08fc2406002a",
|
||||||
|
"930fc43ca7bae20d4706543e97175d1872b0671f",
|
||||||
|
"0cb2ec46f06ba85520a1c1a56706acf35d5176dd",
|
||||||
|
"057053dfb657aaa98553e2c544b06e1a2371557e",
|
||||||
|
"64e091964a611a48424d254a3de2b952d0d6565a",
|
||||||
|
"50ebba2b06908f93d7963b1c6826cc0fd6104477",
|
||||||
|
"374ff82251a384601da73f30485c3ac8d7f4176b",
|
||||||
|
"1487afc813124abbeb0629d2172be0f01ccec3bf",
|
||||||
|
"6a4fa1a68b92336e64006a4310cb160b07854329",
|
||||||
|
"15f986a262fc6eff5774050c94d174c0533d505d",
|
||||||
|
"6184648aab0431c4c95c649072d1f9ff08b9bb7c",
|
||||||
|
"1efa9b640ad980b2ec53834d60e9cff9554979cd",
|
||||||
|
"064d4999ea15e433e06f16f391922390acab01cb",
|
||||||
|
"4884e30b93b3c4c123a83154516196095f9e831e",
|
||||||
|
"2827bfc459c12d7c6d280cbacee750811291d4ba",
|
||||||
|
"9626816275585ac3443e7cddd1272c8652c23f1d",
|
||||||
|
"a2e1bb1fed32c6a6290b679785dd31ca5c59cb5f",
|
||||||
|
"d9535951222dd7a1ff7f763872cb0df44f7962bf",
|
||||||
|
"243b6f18093ff97c861d0568c7d3379606201a4b",
|
||||||
|
"1ce5ac7bab7f2e82af02305ced3c095e320b52e5",
|
||||||
|
"3e63119a8503a6f666b0a736c8fdeb9e79d11eb4",
|
||||||
|
"e33372c0d8b2cdd3e12252962ee1671d66143075",
|
||||||
|
"7364ba2ac9090e468855ce9074bb50c306196f4c",
|
||||||
|
"d6350f9158825662b99e4b5e0442bcc94d39bc11",
|
||||||
|
"2a294ea41312c6da8de5ebd0f18dbfb2963bb1a4",
|
||||||
|
"44c49bbab8a3e9999f3ba9dee0186288b1d960a7",
|
||||||
|
"2305db36455aa0d18571015b9e9bd0950262aa0f",
|
||||||
|
"82f1d8c257d3e76b711a5cecd1e49bd3fa6a9de9",
|
||||||
|
"faed2b028a9b5a712d5180eaa6fd2aa619f941bc",
|
||||||
|
"39ac239b5687f7d1c2ba74cd020b3547545dfdaf",
|
||||||
|
"5737eb22f6119f27c9afccfe73ba710afd885371",
|
||||||
|
"5f22b6daf7204d73cf79d3ff0b46fc4fe237c7f7",
|
||||||
|
"3583f5a570af6870504eea5a5f7afad6e1508508",
|
||||||
|
"b0e489f986c345aef23c4a48d91cbcf5a6fdb9ac",
|
||||||
|
"ba79c80788a9e1751e49ad401f5692d86f73a2db",
|
||||||
|
"c54323436f50c633c870298bb374ac8e7560e6cd",
|
||||||
|
"83725c7ee23bd4a8ca28a4fab0e313409def1dc7",
|
||||||
|
"61c4e8636704f2f38bbe88b1f30ef0d74d6c0f49",
|
||||||
|
"87ef9ba36019f7f3bf217cf47511645893b13f2e",
|
||||||
|
"1bd0adfcf1c75bafc1ba3fc9b65a1a0470df6a91",
|
||||||
|
"1be15348c51955179b7bf9aa90230a9425927ef6",
|
||||||
|
"1f45ab3495df2c3be7d9c882bca9966305115cbb",
|
||||||
|
"7317777e3751efa66218f6da5ef0d01dda69af48",
|
||||||
|
"3346a4ff80b70ee7eea8a79fc79f19c43bb4464a",
|
||||||
|
"452916a904030d2ce4ea2440fad2d0774e7296d9",
|
||||||
|
"2675ef3adf52ebf8a44ff5da4306c293dfa6f901",
|
||||||
|
"e2a76643735f8611a561794509c6bb2aac70eb04",
|
||||||
|
"dc577db3caf5ff83a3b573ba92f2d447f067eee1",
|
||||||
|
"89c4cf244099918b1d3ed413df27d4216e97b499",
|
||||||
|
"4b2b5822c8af3074c6ef9b789a8142d0ef623402",
|
||||||
|
"3c5794f775975669745c412b0c30f48991d9e455",
|
||||||
|
"1df464dbb302ced815c61431a5548a273e6de8e1"
|
||||||
|
);
|
||||||
|
}
|
|
@ -375,6 +375,23 @@ public final class Lbry {
|
||||||
"any_tags", "channel_ids", "order_by", "not_tags", "not_channel_ids", "urls"
|
"any_tags", "channel_ids", "order_by", "not_tags", "not_channel_ids", "urls"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// build claim search for surf mode
|
||||||
|
public static Map<String, Object> buildClaimSearchOptions(
|
||||||
|
String claimType, List<String> notTags, List<String> channelIds, List<String> orderBy, long maxDuration, int limitClaimsPerChannel, int page, int pageSize) {
|
||||||
|
return buildClaimSearchOptions(
|
||||||
|
Collections.singletonList(claimType),
|
||||||
|
null,
|
||||||
|
notTags,
|
||||||
|
channelIds,
|
||||||
|
null,
|
||||||
|
orderBy,
|
||||||
|
null,
|
||||||
|
maxDuration,
|
||||||
|
limitClaimsPerChannel,
|
||||||
|
page,
|
||||||
|
pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, Object> buildClaimSearchOptions(
|
public static Map<String, Object> buildClaimSearchOptions(
|
||||||
String claimType,
|
String claimType,
|
||||||
List<String> anyTags,
|
List<String> anyTags,
|
||||||
|
@ -393,6 +410,8 @@ public final class Lbry {
|
||||||
notChannelIds,
|
notChannelIds,
|
||||||
orderBy,
|
orderBy,
|
||||||
releaseTime,
|
releaseTime,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
page,
|
page,
|
||||||
pageSize);
|
pageSize);
|
||||||
}
|
}
|
||||||
|
@ -405,6 +424,8 @@ public final class Lbry {
|
||||||
List<String> notChannelIds,
|
List<String> notChannelIds,
|
||||||
List<String> orderBy,
|
List<String> orderBy,
|
||||||
String releaseTime,
|
String releaseTime,
|
||||||
|
long maxDuration,
|
||||||
|
int limitClaimsPerChannel,
|
||||||
int page,
|
int page,
|
||||||
int pageSize) {
|
int pageSize) {
|
||||||
Map<String, Object> options = new HashMap<>();
|
Map<String, Object> options = new HashMap<>();
|
||||||
|
@ -417,6 +438,12 @@ public final class Lbry {
|
||||||
if (!Helper.isNullOrEmpty(releaseTime)) {
|
if (!Helper.isNullOrEmpty(releaseTime)) {
|
||||||
options.put("release_time", releaseTime);
|
options.put("release_time", releaseTime);
|
||||||
}
|
}
|
||||||
|
if (maxDuration > 0) {
|
||||||
|
options.put("duration", String.format("<%d", maxDuration));
|
||||||
|
}
|
||||||
|
if (limitClaimsPerChannel > 0) {
|
||||||
|
options.put("limit_claims_per_channel", limitClaimsPerChannel);
|
||||||
|
}
|
||||||
|
|
||||||
addClaimSearchListOption("any_tags", anyTags, options);
|
addClaimSearchListOption("any_tags", anyTags, options);
|
||||||
addClaimSearchListOption("not_tags", notTags, options);
|
addClaimSearchListOption("not_tags", notTags, options);
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class LbryAnalytics {
|
||||||
public static final String EVENT_PUBLISH_UPDATE = "publish_update";
|
public static final String EVENT_PUBLISH_UPDATE = "publish_update";
|
||||||
public static final String EVENT_CHANNEL_CREATE = "channel_create";
|
public static final String EVENT_CHANNEL_CREATE = "channel_create";
|
||||||
public static final String EVENT_SEARCH = "search";
|
public static final String EVENT_SEARCH = "search";
|
||||||
|
public static final String EVENT_SHUFFLE_SESSION = "shuffle_session";
|
||||||
|
|
||||||
private static FirebaseAnalytics analytics;
|
private static FirebaseAnalytics analytics;
|
||||||
|
|
||||||
|
|
87
app/src/main/res/layout/fragment_shuffle.xml
Normal file
87
app/src/main/res/layout/fragment_shuffle.xml
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<RelativeLayout
|
||||||
|
android:background="@android:color/black"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/shuffle_exoplayer_container"
|
||||||
|
android:background="@android:color/black"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_above="@id/shuffle_content_details">
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/shuffle_exoplayer_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:controller_layout_id="@layout/exo_playback_control_view"/>
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/player_buffering_progress"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/shuffle_content_details"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_alignParentBottom="true">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shuffle_content_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textFontWeight="300"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:textSize="20sp"
|
||||||
|
/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/shuffle_content_publisher"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textFontWeight="300"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/shuffle_loading"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/shuffle_share_button"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:clickable="true"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:tint="@color/white"
|
||||||
|
android:src="@drawable/ic_share" />
|
||||||
|
</RelativeLayout>
|
||||||
|
</RelativeLayout>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".ui.slideshow.SlideshowFragment">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_slideshow"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="20sp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
|
@ -20,6 +20,7 @@
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
<string name="sign_in">Sign In</string>
|
<string name="sign_in">Sign In</string>
|
||||||
|
<string name="shuffle">Shuffle</string>
|
||||||
<string name="startup_failed">App startup failed. Please check your data connection and try again. If this problem persists, please email hello@lbry.com</string>
|
<string name="startup_failed">App startup failed. Please check your data connection and try again. If this problem persists, please email hello@lbry.com</string>
|
||||||
<string name="no_claim_search_content">No content to display at this time. Please refine your selection or check back later.</string>
|
<string name="no_claim_search_content">No content to display at this time. Please refine your selection or check back later.</string>
|
||||||
|
|
||||||
|
@ -650,4 +651,6 @@
|
||||||
|
|
||||||
<string name="fa_asterisk" translatable="false"></string>
|
<string name="fa_asterisk" translatable="false"></string>
|
||||||
<string name="fa_comment_alt" translatable="false"></string>
|
<string name="fa_comment_alt" translatable="false"></string>
|
||||||
|
<string name="fa_broadcast_tower" translatable="false"></string>
|
||||||
|
<string name="fa_random" translatable="false"></string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue