In-app notifications #969

Merged
akinwale merged 13 commits from in-app-notifications into master 2020-08-18 15:19:35 +02:00
15 changed files with 296 additions and 112 deletions
Showing only changes of commit 9af6b74934 - Show all commits

View file

@ -16,8 +16,8 @@ android {
applicationId "io.lbry.browser" applicationId "io.lbry.browser"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 1514 versionCode 1515
versionName "0.15.14" versionName "0.15.15"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@ -101,8 +101,8 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
__32bitImplementation 'io.lbry:lbrysdk32:0.76.0' __32bitImplementation 'io.lbry:lbrysdk32:0.79.1'
__64bitImplementation 'io.lbry:lbrysdk64:0.76.0' __64bitImplementation 'io.lbry:lbrysdk64:0.79.1'
} }
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'

View file

@ -56,6 +56,13 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lbry" /> <data android:scheme="lbry" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" android:host="open.lbry.com"/>
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity> </activity>
<activity <activity

Binary file not shown.

View file

@ -306,7 +306,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX"; public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX";
public static final String SECURE_VALUE_FIRST_RUN_PASSWORD = "firstRunPassword"; public static final String SECURE_VALUE_FIRST_RUN_PASSWORD = "firstRunPassword";
private static final String TAG = "io.lbry.browser.Main"; private static final String TAG = "LbryMain";
private NavigationMenuAdapter navMenuAdapter; private NavigationMenuAdapter navMenuAdapter;
private UrlSuggestionListAdapter urlSuggestionListAdapter; private UrlSuggestionListAdapter urlSuggestionListAdapter;
@ -1585,7 +1585,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE); findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
syncWalletAndLoadPreferences();
scheduleWalletBalanceUpdate(); scheduleWalletBalanceUpdate();
scheduleWalletSyncTask(); scheduleWalletSyncTask();
fetchOwnChannels(); fetchOwnChannels();
@ -1807,7 +1806,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
//openNavFragments.get //openNavFragments.get
MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask( MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
subscriptions, subscriptions,
!initialSubscriptionMergeDone(), initialSubscriptionMergeDone(),
MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() { MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
@Override @Override
public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) { public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) {
@ -1818,11 +1817,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, true).apply(); sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_INITIAL_SUBSCRIPTION_MERGE_DONE, true).apply();
Lbryio.cacheResolvedSubscriptions.clear();
for (Fragment fragment : openNavFragments.values()) { for (Fragment fragment : openNavFragments.values()) {
if (fragment instanceof FollowingFragment) { if (fragment instanceof FollowingFragment) {
// reload local subscriptions // reload local subscriptions
((FollowingFragment) fragment).fetchLoadedSubscriptions(true); FollowingFragment followingFragment = (FollowingFragment) fragment;
followingFragment.fetchLoadedSubscriptions(true);
} }
} }
} }
@ -1960,7 +1961,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public void onSyncGetSuccess(WalletSync walletSync) { public void onSyncGetSuccess(WalletSync walletSync) {
Lbryio.lastWalletSync = walletSync; Lbryio.lastWalletSync = walletSync;
Lbryio.lastRemoteHash = walletSync.getHash(); Lbryio.lastRemoteHash = walletSync.getHash();
loadSharedUserState();
} }
@Override @Override
@ -2678,7 +2678,6 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle); LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle);
} }
private void registerServiceActionsReceiver() { private void registerServiceActionsReceiver() {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT); intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
@ -2840,9 +2839,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
JSONObject startupStatus = status.getJSONObject("startup_status"); JSONObject startupStatus = status.getJSONObject("startup_status");
sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet"); sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet");
} }
} catch (ConnectException ex) { } catch (ConnectException | JSONException ex) {
// pass
} catch (JSONException ex) {
// pass // pass
} }

View file

@ -0,0 +1,65 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONObject;
import java.util.concurrent.TimeUnit;
import io.lbry.browser.utils.Helper;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class BufferEventTask extends AsyncTask<Void, Void, Void> {
private static final String TAG = "LbryBufferEvent";
private static final String ENDPOINT = "https://collector-service.api.lbry.tv/api/v1/events/video";
private String streamUrl;
private String userIdHash;
private long streamDuration;
private long streamPosition;
private long bufferDuration;
public BufferEventTask(String streamUrl, long streamDuration, long streamPosition, long bufferDuration, String userIdHash) {
this.streamUrl = streamUrl;
this.bufferDuration = bufferDuration;
this.streamDuration = streamDuration;
this.streamPosition = streamPosition;
this.userIdHash = userIdHash;
}
protected Void doInBackground(Void... params) {
JSONObject requestBody = new JSONObject();
JSONObject data = new JSONObject();
try {
data.put("url", streamUrl);
data.put("position", streamPosition);
data.put("stream_duration", streamDuration);
//data.put("duration", bufferDuration);
requestBody.put("device", "android");
requestBody.put("type", "buffering");
requestBody.put("client", userIdHash);
requestBody.put("data", data);
RequestBody body = RequestBody.create(requestBody.toString(), Helper.JSON_MEDIA_TYPE);
Request request = new Request.Builder().url(ENDPOINT).post(body).build();
OkHttpClient client = new OkHttpClient.Builder().
writeTimeout(60, TimeUnit.SECONDS).
readTimeout(60, TimeUnit.SECONDS).
build();
Response response = client.newCall(request).execute();
String responseString = response.body().string();
Log.d(TAG, String.format("buffer event sent: %s", responseString));
} catch (Exception ex) {
// we don't want to fail if a buffer event fails to register
Log.d(TAG, String.format("buffer event log failed: %s", ex.getMessage()), ex);
}
return null;
}
}

View file

@ -73,6 +73,7 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
// fetch remote subscriptions // fetch remote subscriptions
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context)); JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
if (array != null) { if (array != null) {
// check for any remote subs that may have been removed, and unsubscribe from them
for (int i = 0; i < array.length(); i++) { for (int i = 0; i < array.length(); i++) {
JSONObject item = array.getJSONObject(i); JSONObject item = array.getJSONObject(i);
String claimId = item.getString("claim_id"); String claimId = item.getString("claim_id");
@ -86,23 +87,21 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
} }
} }
for (int i = 0; i < combined.size(); i++) { List<Subscription> remoteUnsubs = new ArrayList<>();
Subscription local = combined.get(i); List<Subscription> finalRemoteSubs = new ArrayList<>();
if (!remoteSubs.contains(local)) { if (remoteSubs.size() > 0) {
// add to remote subscriptions for (int i = 0; i < remoteSubs.size(); i++) {
try { Subscription sub = remoteSubs.get(i);
LbryUri uri = LbryUri.parse(local.getUrl()); if (!combined.contains(sub)) {
Map<String, String> options = new HashMap<>(); Map<String, String> options = new HashMap<>();
String channelClaimId = uri.getChannelClaimId(); LbryUri uri = LbryUri.tryParse(sub.getUrl());
String channelName = Helper.normalizeChannelName(local.getChannelName()); if (uri != null) {
if (!Helper.isNullOrEmpty(channelClaimId) && !Helper.isNullOrEmpty(channelName)) { options.put("claim_id", uri.getChannelClaimId());
options.put("claim_id", channelClaimId); Lbryio.parseResponse(Lbryio.call("subscription", "delete", options, context));
options.put("channel_name", channelName); remoteUnsubs.add(sub);
Lbryio.parseResponse(Lbryio.call("subscription", "new", options, context)); } else {
finalRemoteSubs.add(sub);
} }
} catch (LbryUriException | LbryioRequestException | LbryioResponseException ex) {
// pass
Log.e(TAG, String.format("subscription/new failed: %s", ex.getMessage()), ex);
} }
} }
} }
@ -114,13 +113,13 @@ public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
diff.add(local); diff.add(local);
} }
} }
} for (int i = 0; i < finalRemoteSubs.size(); i++) {
for (int i = 0; i < remoteSubs.size(); i++) { Subscription remote = finalRemoteSubs.get(i);
Subscription remote = remoteSubs.get(i); if (!combined.contains(remote)) {
if (!combined.contains(remote)) { combined.add(remote);
combined.add(remote); if (!diff.contains(remote)) {
if (!diff.contains(remote)) { diff.add(remote);
diff.add(remote); }
} }
} }
} }

View file

@ -107,7 +107,7 @@ public class SyncGetTask extends AsyncTask<Void, Void, WalletSync> {
if (applySyncChanges) { if (applySyncChanges) {
if (applySyncSuccessful) { if (applySyncSuccessful) {
handler.onSyncApplySuccess(syncHash, syncData); handler.onSyncApplySuccess(syncHash, syncData);
} else { } else if (syncApplyError != null) {
handler.onSyncApplyError(syncApplyError); handler.onSyncApplyError(syncApplyError);
} }
} }

View file

@ -49,6 +49,7 @@ import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ResolveTask; import io.lbry.browser.tasks.claim.ResolveTask;
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask; import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.controls.OutlineIconView;
import io.lbry.browser.ui.controls.SolidIconView; import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.ui.findcontent.FollowingFragment; import io.lbry.browser.ui.findcontent.FollowingFragment;
import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Helper;
@ -80,7 +81,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
private View buttonTip; private View buttonTip;
private View buttonFollowUnfollow; private View buttonFollowUnfollow;
private int subCount; private int subCount;
private SolidIconView iconFollowUnfollow; private OutlineIconView iconFollow;
private SolidIconView iconUnfollow;
private View layoutNothingAtLocation; private View layoutNothingAtLocation;
private View layoutLoadingState; private View layoutLoadingState;
@ -105,7 +107,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
buttonShare = root.findViewById(R.id.channel_view_share); buttonShare = root.findViewById(R.id.channel_view_share);
buttonTip = root.findViewById(R.id.channel_view_tip); buttonTip = root.findViewById(R.id.channel_view_tip);
buttonFollowUnfollow = root.findViewById(R.id.channel_view_follow_unfollow); buttonFollowUnfollow = root.findViewById(R.id.channel_view_follow_unfollow);
iconFollowUnfollow = root.findViewById(R.id.channel_view_icon_follow_unfollow); iconFollow = root.findViewById(R.id.channel_view_icon_follow);
iconUnfollow = root.findViewById(R.id.channel_view_icon_unfollow);
tabPager = root.findViewById(R.id.channel_view_pager); tabPager = root.findViewById(R.id.channel_view_pager);
tabLayout = root.findViewById(R.id.channel_view_tabs); tabLayout = root.findViewById(R.id.channel_view_tabs);
@ -274,13 +277,8 @@ public class ChannelFragment extends BaseFragment implements FetchChannelsListen
private void checkIsFollowing() { private void checkIsFollowing() {
if (claim != null) { if (claim != null) {
boolean isFollowing = Lbryio.isFollowing(claim); boolean isFollowing = Lbryio.isFollowing(claim);
if (iconFollowUnfollow != null) { Helper.setViewVisibility(iconFollow, !isFollowing ? View.VISIBLE : View.GONE);
iconFollowUnfollow.setText(isFollowing ? R.string.fa_heart_broken : R.string.fa_heart); Helper.setViewVisibility(iconUnfollow, isFollowing ? View.VISIBLE : View.GONE);
Context context = getContext();
if (context != null) {
iconFollowUnfollow.setTextColor(ContextCompat.getColor(context, isFollowing ? R.color.foreground : R.color.red));
}
}
} }
} }

View file

@ -0,0 +1,29 @@
package io.lbry.browser.ui.controls;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.Gravity;
import androidx.appcompat.widget.AppCompatTextView;
public class OutlineIconView extends AppCompatTextView {
private Context context;
public OutlineIconView(Context context) {
super(context);
this.context = context;
init();
}
public OutlineIconView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
private void init() {
setGravity(Gravity.CENTER);
setTypeface(Typeface.createFromAsset(context.getAssets(), "font_awesome_5_free_regular.otf"));
}
}

View file

@ -9,6 +9,7 @@ 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;
@ -119,6 +120,7 @@ import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.lbryinc.Reward; import io.lbry.browser.model.lbryinc.Reward;
import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.BufferEventTask;
import io.lbry.browser.tasks.CommentCreateWithTipTask; import io.lbry.browser.tasks.CommentCreateWithTipTask;
import io.lbry.browser.tasks.CommentListHandler; import io.lbry.browser.tasks.CommentListHandler;
import io.lbry.browser.tasks.CommentListTask; import io.lbry.browser.tasks.CommentListTask;
@ -141,6 +143,7 @@ import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
import io.lbry.browser.tasks.lbryinc.FetchStatCountTask; import io.lbry.browser.tasks.lbryinc.FetchStatCountTask;
import io.lbry.browser.tasks.lbryinc.LogFileViewTask; import io.lbry.browser.tasks.lbryinc.LogFileViewTask;
import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.controls.OutlineIconView;
import io.lbry.browser.ui.controls.SolidIconView; import io.lbry.browser.ui.controls.SolidIconView;
import io.lbry.browser.ui.publish.PublishFragment; import io.lbry.browser.ui.publish.PublishFragment;
import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Helper;
@ -163,6 +166,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";
private PlayerControlView castControlView; private PlayerControlView castControlView;
private Player currentPlayer; private Player currentPlayer;
@ -290,6 +294,27 @@ public class FileViewFragment extends BaseFragment implements
loadingNewClaim = false; loadingNewClaim = false;
} }
} else if (playbackState == Player.STATE_BUFFERING) { } else if (playbackState == Player.STATE_BUFFERING) {
if (MainActivity.appPlayer != null && MainActivity.appPlayer.getCurrentPosition() > 0) {
// 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(CDN_PREFIX)) {
BufferEventTask bufferEvent = new BufferEventTask(claim.getPermanentUrl(), duration, position, 1, userIdHash);
bufferEvent.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
} else {
// sdk stream buffer events should be handled differently
Bundle bundle = new Bundle();
bundle.putString("url", claim.getPermanentUrl());
bundle.putLong("stream_duration", duration);
bundle.putLong("stream_position", position);
bundle.putString("user_id_hash", userIdHash);
LbryAnalytics.logEvent(LbryAnalytics.EVENT_BUFFER, bundle);
}
}
showBuffering(); showBuffering();
} else { } else {
hideBuffering(); hideBuffering();
@ -724,6 +749,44 @@ public class FileViewFragment extends BaseFragment implements
} }
} }
private View.OnClickListener followUnfollowListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
if (claim != null && claim.getSigningChannel() != null) {
Claim publisher = claim.getSigningChannel();
boolean isFollowing = Lbryio.isFollowing(publisher);
Subscription subscription = Subscription.fromClaim(publisher);
view.setEnabled(false);
Context context = getContext();
new ChannelSubscribeTask(context, publisher.getClaimId(), subscription, isFollowing, new ChannelSubscribeTask.ChannelSubscribeHandler() {
@Override
public void onSuccess() {
if (isFollowing) {
Lbryio.removeSubscription(subscription);
Lbryio.removeCachedResolvedSubscription(publisher);
} else {
Lbryio.addSubscription(subscription);
Lbryio.addCachedResolvedSubscription(publisher);
}
view.setEnabled(true);
checkIsFollowing();
FollowingFragment.resetClaimSearchContent = true;
// Save shared user state
if (context != null) {
context.sendBroadcast(new Intent(MainActivity.ACTION_SAVE_SHARED_USER_STATE));
}
}
@Override
public void onError(Exception exception) {
view.setEnabled(true);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
};
private void resolveUrl(String url) { private void resolveUrl(String url) {
resolving = true; resolving = true;
Helper.setViewVisibility(layoutDisplayArea, View.INVISIBLE); Helper.setViewVisibility(layoutDisplayArea, View.INVISIBLE);
@ -1060,44 +1123,10 @@ public class FileViewFragment extends BaseFragment implements
} }
}); });
View buttonFollowUnfollow = root.findViewById(R.id.file_view_icon_follow_unfollow); View buttonFollow = root.findViewById(R.id.file_view_icon_follow);
buttonFollowUnfollow.setOnClickListener(new View.OnClickListener() { View buttonUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
@Override buttonFollow.setOnClickListener(followUnfollowListener);
public void onClick(View view) { buttonUnfollow.setOnClickListener(followUnfollowListener);
if (claim != null && claim.getSigningChannel() != null) {
Claim publisher = claim.getSigningChannel();
boolean isFollowing = Lbryio.isFollowing(publisher);
Subscription subscription = Subscription.fromClaim(publisher);
buttonFollowUnfollow.setEnabled(false);
Context context = getContext();
new ChannelSubscribeTask(context, publisher.getClaimId(), subscription, isFollowing, new ChannelSubscribeTask.ChannelSubscribeHandler() {
@Override
public void onSuccess() {
if (isFollowing) {
Lbryio.removeSubscription(subscription);
Lbryio.removeCachedResolvedSubscription(publisher);
} else {
Lbryio.addSubscription(subscription);
Lbryio.addCachedResolvedSubscription(publisher);
}
buttonFollowUnfollow.setEnabled(true);
checkIsFollowing();
FollowingFragment.resetClaimSearchContent = true;
// Save shared user state
if (context != null) {
context.sendBroadcast(new Intent(MainActivity.ACTION_SAVE_SHARED_USER_STATE));
}
}
@Override
public void onError(Exception exception) {
buttonFollowUnfollow.setEnabled(true);
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
});
commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>()); commentChannelSpinnerAdapter = new InlineChannelSpinnerAdapter(getContext(), R.layout.spinner_item_channel, new ArrayList<>());
commentChannelSpinnerAdapter.addPlaceholder(false); commentChannelSpinnerAdapter.addPlaceholder(false);
@ -1418,7 +1447,17 @@ public class FileViewFragment extends BaseFragment implements
} }
} }
root.findViewById(R.id.file_view_icon_follow_unfollow).setVisibility(claim.getSigningChannel() != null ? View.VISIBLE : View.GONE); boolean isAnonymous = claim.getSigningChannel() == null;
View iconFollow = root.findViewById(R.id.file_view_icon_follow);
View iconUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
if (isAnonymous) {
if (iconFollow.getVisibility() == View.VISIBLE) {
iconFollow.setVisibility(View.INVISIBLE);
}
if (iconUnfollow.getVisibility() == View.VISIBLE) {
iconUnfollow.setVisibility(View.INVISIBLE);
}
}
MaterialButton mainActionButton = root.findViewById(R.id.file_view_main_action_button); MaterialButton mainActionButton = root.findViewById(R.id.file_view_main_action_button);
if (claim.isPlayable()) { if (claim.isPlayable()) {
@ -2408,11 +2447,10 @@ public class FileViewFragment extends BaseFragment implements
Context context = getContext(); Context context = getContext();
View root = getView(); View root = getView();
if (context != null && root != null) { if (context != null && root != null) {
SolidIconView iconFollowUnfollow = root.findViewById(R.id.file_view_icon_follow_unfollow); OutlineIconView iconFollow = root.findViewById(R.id.file_view_icon_follow);
if (iconFollowUnfollow != null) { SolidIconView iconUnfollow = root.findViewById(R.id.file_view_icon_unfollow);
iconFollowUnfollow.setText(isFollowing ? R.string.fa_heart_broken : R.string.fa_heart); Helper.setViewVisibility(iconFollow, !isFollowing ? View.VISIBLE: View.INVISIBLE);
iconFollowUnfollow.setTextColor(ContextCompat.getColor(context, isFollowing ? R.color.foreground : R.color.red)); Helper.setViewVisibility(iconUnfollow, isFollowing ? View.VISIBLE : View.INVISIBLE);
}
} }
} }
} }

View file

@ -30,6 +30,8 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.gms.common.util.Hex;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -37,6 +39,9 @@ import org.json.JSONObject;
import java.io.Closeable; import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -766,4 +771,14 @@ public final class Helper {
} }
return id.toString(); return id.toString();
} }
public static String SHA256(String value) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(value.getBytes("UTF-8"));
return Hex.bytesToStringLowercase(hash);
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
return null;
}
}
} }

View file

@ -11,6 +11,7 @@ public class LbryAnalytics {
public static final String EVENT_APP_ERROR = "app_error"; public static final String EVENT_APP_ERROR = "app_error";
public static final String EVENT_APP_LAUNCH = "app_launch"; public static final String EVENT_APP_LAUNCH = "app_launch";
public static final String EVENT_COMMENT_CREATE = "comment_create"; public static final String EVENT_COMMENT_CREATE = "comment_create";
public static final String EVENT_BUFFER = "buffer";
public static final String EVENT_EMAIL_ADDED = "email_added"; public static final String EVENT_EMAIL_ADDED = "email_added";
public static final String EVENT_EMAIL_VERIFIED = "email_verified"; public static final String EVENT_EMAIL_VERIFIED = "email_verified";
public static final String EVENT_FIRST_RUN_COMPLETED = "first_run_completed"; public static final String EVENT_FIRST_RUN_COMPLETED = "first_run_completed";

View file

@ -17,7 +17,8 @@ public class LbryUri {
public static final int CHANNEL_NAME_MIN_LENGTH = 1; public static final int CHANNEL_NAME_MIN_LENGTH = 1;
public static final int CLAIM_ID_MAX_LENGTH = 40; public static final int CLAIM_ID_MAX_LENGTH = 40;
private static final String REGEX_PART_PROTOCOL = "^((?:lbry://)?)"; private static final String REGEX_PART_PROTOCOL = "^((?:lbry://|https://)?)";
private static final String REGEX_PART_HOST = "((?:open.lbry.com/)?)";
private static final String REGEX_PART_STREAM_OR_CHANNEL_NAME = "([^:$#/]*)"; private static final String REGEX_PART_STREAM_OR_CHANNEL_NAME = "([^:$#/]*)";
private static final String REGEX_PART_MODIFIER_SEPARATOR = "([:$#]?)([^/]*)"; private static final String REGEX_PART_MODIFIER_SEPARATOR = "([:$#]?)([^/]*)";
private static final String QUERY_STRING_BREAKER = "^([\\S]+)([?][\\S]*)"; private static final String QUERY_STRING_BREAKER = "^([\\S]+)([?][\\S]*)";
@ -58,8 +59,9 @@ public class LbryUri {
return parse(url, false); return parse(url, false);
} }
public static LbryUri parse(String url, boolean requireProto) throws LbryUriException { public static LbryUri parse(String url, boolean requireProto) throws LbryUriException {
Pattern componentsPattern = Pattern.compile(String.format("%s%s%s(/?)%s%s", Pattern componentsPattern = Pattern.compile(String.format("%s%s%s%s(/?)%s%s",
REGEX_PART_PROTOCOL, REGEX_PART_PROTOCOL,
REGEX_PART_HOST,
REGEX_PART_STREAM_OR_CHANNEL_NAME, REGEX_PART_STREAM_OR_CHANNEL_NAME,
REGEX_PART_MODIFIER_SEPARATOR, REGEX_PART_MODIFIER_SEPARATOR,
REGEX_PART_STREAM_OR_CHANNEL_NAME, REGEX_PART_STREAM_OR_CHANNEL_NAME,
@ -93,37 +95,48 @@ public class LbryUri {
} }
// components[0] = proto // components[0] = proto
// components[1] = streamNameOrChannelName // components[1] = host
// components[2] = primaryModSeparator // components[2] = streamNameOrChannelName
// components[3] = primaryModValue // components[3] = primaryModSeparator
// components[4] = pathSep // components[4] = primaryModValue
// components[5] = possibleStreamName // components[5] = pathSep
// components[6] = secondaryModSeparator // components[6] = possibleStreamName
// components[7] = secondaryModValue // components[7] = secondaryModSeparator
// components[8] = secondaryModValue
if (requireProto && Helper.isNullOrEmpty(components.get(0))) { if (requireProto && Helper.isNullOrEmpty(components.get(0))) {
throw new LbryUriException("LBRY URLs must include a protocol prefix (lbry://)."); throw new LbryUriException("LBRY URLs must include a protocol prefix (lbry://).");
} }
if (Helper.isNullOrEmpty(components.get(1))) { if (Helper.isNullOrEmpty(components.get(2))) {
throw new LbryUriException("URL does not include name."); throw new LbryUriException("URL does not include name.");
} }
for (String component : components.subList(1, components.size())) { for (String component : components.subList(2, components.size())) {
if (component.indexOf(' ') > -1) { if (component.indexOf(' ') > -1) {
throw new LbryUriException("URL cannot include a space."); throw new LbryUriException("URL cannot include a space.");
} }
} }
String streamOrChannelName = components.get(1); String streamOrChannelName = components.get(2);
String primaryModSeparator = components.get(2); String primaryModSeparator = components.get(3);
String primaryModValue = components.get(3); String primaryModValue = components.get(4);
String possibleStreamName = components.get(5); String possibleStreamName = components.get(6);
String secondaryModSeparator = components.get(6); String secondaryModSeparator = components.get(7);
String secondaryModValue = components.get(7); String secondaryModValue = components.get(8);
boolean includesChannel = streamOrChannelName.startsWith("@"); boolean includesChannel = streamOrChannelName.startsWith("@");
boolean isChannel = includesChannel && Helper.isNullOrEmpty(possibleStreamName); boolean isChannel = includesChannel && Helper.isNullOrEmpty(possibleStreamName);
String channelName = includesChannel && streamOrChannelName.length() > 1 ? streamOrChannelName.substring(1) : null; String channelName = includesChannel && streamOrChannelName.length() > 1 ? streamOrChannelName.substring(1) : null;
// It would have thrown already on the RegEx parser if protocol value was incorrect
// open.lbry.com uses ':' as ModSeparators while lbry:// expects '#'
if (components.get(1).equals("open.lbry.com/")) {
if (primaryModSeparator.equals(":"))
primaryModSeparator = "#";
if (secondaryModSeparator.equals(":"))
secondaryModSeparator = "#";
}
if (includesChannel) { if (includesChannel) {
if (Helper.isNullOrEmpty(channelName)) { if (Helper.isNullOrEmpty(channelName)) {
throw new LbryUriException("No channel name after @."); throw new LbryUriException("No channel name after @.");
@ -147,7 +160,7 @@ public class LbryUri {
LbryUri uri = new LbryUri(); LbryUri uri = new LbryUri();
uri.setChannel(isChannel); uri.setChannel(isChannel);
uri.setPath(Helper.join(components.subList(1, components.size()), "")); uri.setPath(Helper.join(components.subList(2, components.size()), ""));
uri.setStreamName(streamName); uri.setStreamName(streamName);
uri.setStreamClaimId(streamClaimId); uri.setStreamClaimId(streamClaimId);
uri.setChannelName(channelName); uri.setChannelName(channelName);

View file

@ -210,14 +210,22 @@
android:clickable="true" android:clickable="true"
android:layout_width="36dp" android:layout_width="36dp"
android:layout_height="36dp"> android:layout_height="36dp">
<io.lbry.browser.ui.controls.SolidIconView <io.lbry.browser.ui.controls.OutlineIconView
android:id="@+id/channel_view_icon_follow_unfollow" android:id="@+id/channel_view_icon_follow"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:text="@string/fa_heart" android:text="@string/fa_heart"
android:textColor="@color/red" android:textColor="@color/red"
android:textSize="20dp" /> android:textSize="20dp" />
<io.lbry.browser.ui.controls.SolidIconView
android:id="@+id/channel_view_icon_unfollow"
android:layout_centerInParent="true"
android:layout_width="24dp"
android:layout_height="24dp"
android:text="@string/fa_heart_broken"
android:textSize="20dp"
android:visibility="invisible"/>
</RelativeLayout> </RelativeLayout>
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -484,7 +484,7 @@
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingLeft="16dp" android:paddingLeft="16dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:layout_toLeftOf="@id/file_view_icon_follow_unfollow"> android:layout_toLeftOf="@id/file_view_icon_follow">
<RelativeLayout <RelativeLayout
android:id="@+id/file_view_publisher_avatar" android:id="@+id/file_view_publisher_avatar"
android:layout_width="50dp" android:layout_width="50dp"
@ -541,8 +541,8 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
<io.lbry.browser.ui.controls.SolidIconView <io.lbry.browser.ui.controls.OutlineIconView
android:id="@+id/file_view_icon_follow_unfollow" android:id="@+id/file_view_icon_follow"
android:clickable="true" android:clickable="true"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
@ -554,6 +554,20 @@
android:text="@string/fa_heart" android:text="@string/fa_heart"
android:textColor="@color/red" android:textColor="@color/red"
android:textSize="20dp" /> android:textSize="20dp" />
<io.lbry.browser.ui.controls.SolidIconView
android:id="@+id/file_view_icon_unfollow"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:text="@string/fa_heart_broken"
android:textSize="20dp"
android:visibility="invisible" />
</RelativeLayout> </RelativeLayout>
<View <View