Native rewrite #878

Merged
akinwale merged 65 commits from native-rewrite into master 2020-05-23 08:49:00 +02:00
24 changed files with 854 additions and 40 deletions
Showing only changes of commit 5ceb081972 - Show all commits

View file

@ -21,6 +21,7 @@ import android.os.Handler;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Base64; import android.util.Base64;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.Menu; import android.view.Menu;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@ -74,6 +75,7 @@ import java.util.concurrent.TimeUnit;
import io.lbry.browser.adapter.NavigationMenuAdapter; import io.lbry.browser.adapter.NavigationMenuAdapter;
import io.lbry.browser.adapter.UrlSuggestionListAdapter; import io.lbry.browser.adapter.UrlSuggestionListAdapter;
import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.dialog.ContentScopeDialogFragment;
import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.listener.SdkStatusListener; import io.lbry.browser.listener.SdkStatusListener;
import io.lbry.browser.listener.WalletBalanceListener; import io.lbry.browser.listener.WalletBalanceListener;
@ -86,9 +88,14 @@ import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.WalletSync; import io.lbry.browser.model.WalletSync;
import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.LighthouseAutoCompleteTask; import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
import io.lbry.browser.tasks.MergeSubscriptionsTask;
import io.lbry.browser.tasks.ResolveTask; import io.lbry.browser.tasks.ResolveTask;
import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler; import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler;
import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask;
import io.lbry.browser.tasks.wallet.SaveSharedUserStateTask;
import io.lbry.browser.tasks.wallet.SyncApplyTask;
import io.lbry.browser.tasks.wallet.SyncGetTask; import io.lbry.browser.tasks.wallet.SyncGetTask;
import io.lbry.browser.tasks.wallet.SyncSetTask;
import io.lbry.browser.tasks.wallet.WalletBalanceTask; import io.lbry.browser.tasks.wallet.WalletBalanceTask;
import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.ui.channel.ChannelFragment; import io.lbry.browser.ui.channel.ChannelFragment;
@ -104,7 +111,6 @@ import io.lbry.browser.utils.Lbryio;
import io.lbry.lbrysdk.LbrynetService; import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.ServiceHelper; import io.lbry.lbrysdk.ServiceHelper;
import io.lbry.lbrysdk.Utils; import io.lbry.lbrysdk.Utils;
import lombok.Data;
import lombok.Getter; import lombok.Getter;
public class MainActivity extends AppCompatActivity implements SdkStatusListener { public class MainActivity extends AppCompatActivity implements SdkStatusListener {
@ -190,6 +196,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
@Getter @Getter
private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private boolean walletBalanceUpdateScheduled; private boolean walletBalanceUpdateScheduled;
private boolean walletSyncScheduled;
private String pendingAllContentTag; private String pendingAllContentTag;
private String pendingChannelUrl; private String pendingChannelUrl;
private boolean pendingFollowingReload; private boolean pendingFollowingReload;
@ -836,6 +843,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
}, CHECK_SDK_READY_INTERVAL); }, CHECK_SDK_READY_INTERVAL);
} else { } else {
scheduleWalletBalanceUpdate(); scheduleWalletBalanceUpdate();
scheduleWalletSyncTask();
} }
} }
@ -843,7 +851,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
if (Lbryio.isSignedIn()) { if (Lbryio.isSignedIn()) {
checkSyncedWallet(); checkSyncedWallet();
} }
//overrideRemoteWallet();
scheduleWalletBalanceUpdate(); scheduleWalletBalanceUpdate();
scheduleWalletSyncTask();
} }
private void scheduleWalletBalanceUpdate() { private void scheduleWalletBalanceUpdate() {
@ -858,6 +869,157 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
} }
} }
private void scheduleWalletSyncTask() {
if (scheduler != null && !walletSyncScheduled) {
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
syncWalletAndLoadPreferences();
}
}, 0, 5, TimeUnit.MINUTES);
walletSyncScheduled = true;
}
}
public void saveSharedUserState() {
if (!userSyncEnabled()) {
return;
}
SaveSharedUserStateTask saveTask = new SaveSharedUserStateTask(new SaveSharedUserStateTask.SaveSharedUserStateHandler() {
@Override
public void onSuccess() {
// push wallet sync changes
pushCurrentWalletSync();
}
@Override
public void onError(Exception error) {
// pass
}
});
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void loadSharedUserState() {
// load wallet preferences
LoadSharedUserStateTask loadTask = new LoadSharedUserStateTask(MainActivity.this, new LoadSharedUserStateTask.LoadSharedUserStateHandler() {
@Override
public void onSuccess(List<Subscription> subscriptions, List<Tag> followedTags) {
if (subscriptions != null && subscriptions.size() > 0) {
// reload subscriptions if wallet fragment is FollowingFragment
//openNavFragments.get
MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
subscriptions, MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
@Override
public void onSuccess(List<Subscription> subscriptions, List<Subscription> diff) {
Lbryio.subscriptions = new ArrayList<>(subscriptions);
if (diff != null && diff.size() > 0) {
saveSharedUserState();
}
for (Fragment fragment : openNavFragments.values()) {
if (fragment instanceof FollowingFragment) {
// reload local subscriptions
((FollowingFragment) fragment).fetchLoadedSubscriptions();
}
}
}
@Override
public void onError(Exception error) {
Log.e(TAG, String.format("merge subscriptions failed: %s", error.getMessage()), error);
}
});
mergeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
if (followedTags != null && followedTags.size() > 0) {
List<Tag> previousTags = new ArrayList<>(Lbry.followedTags);
Lbry.followedTags = new ArrayList<>(followedTags);
for (Fragment fragment : openNavFragments.values()) {
if (fragment instanceof AllContentFragment) {
AllContentFragment acFragment = (AllContentFragment) fragment;
if (!acFragment.isSingleTagView() &&
acFragment.getCurrentContentScope() == ContentScopeDialogFragment.ITEM_TAGS &&
!previousTags.equals(followedTags)) {
acFragment.fetchClaimSearchContent(true);
}
}
}
}
}
@Override
public void onError(Exception error) {
Log.e(TAG, String.format("load shared user state failed: %s", error != null ? error.getMessage() : "no error message"), error);
}
});
loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void pushCurrentWalletSync() {
String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
SyncApplyTask fetchTask = new SyncApplyTask(true, password, new DefaultSyncTaskHandler() {
@Override
public void onSyncApplySuccess(String hash, String data) {
SyncSetTask setTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, null);
setTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public void onSyncApplyError(Exception error) { }
});
fetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private boolean userSyncEnabled() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
boolean walletSyncEnabled = sp.getBoolean(PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, false);
return walletSyncEnabled && Lbryio.isSignedIn();
}
private void syncWalletAndLoadPreferences() {
if (!userSyncEnabled()) {
return;
}
String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
SyncGetTask task = new SyncGetTask(password, true, null, new DefaultSyncTaskHandler() {
@Override
public void onSyncGetSuccess(WalletSync walletSync) {
Lbryio.lastWalletSync = walletSync;
Lbryio.lastRemoteHash = walletSync.getHash();
loadSharedUserState();
}
@Override
public void onSyncGetWalletNotFound() {
// pass. This actually shouldn't happen at this point.
}
@Override
public void onSyncGetError(Exception error) {
// pass
Log.e(TAG, String.format("sync get failed: %s", error != null ? error.getMessage() : "no error message"), error);
}
@Override
public void onSyncApplySuccess(String hash, String data) {
if (!hash.equalsIgnoreCase(Lbryio.lastRemoteHash)) {
SyncSetTask setTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, null);
setTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
loadSharedUserState();
}
@Override
public void onSyncApplyError(Exception error) {
// pass
Log.e(TAG, String.format("sync apply failed: %s", error != null ? error.getMessage() : "no error message"), error);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void registerRequestsReceiver() { private void registerRequestsReceiver() {
IntentFilter intentFilter = new IntentFilter(); IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_AUTH_TOKEN_GENERATED); intentFilter.addAction(ACTION_AUTH_TOKEN_GENERATED);
@ -979,11 +1141,15 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
showSignedInUser(); showSignedInUser();
if (requestCode == REQUEST_WALLET_SYNC_SIGN_IN) { if (requestCode == REQUEST_WALLET_SYNC_SIGN_IN) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, true).apply();
for (Fragment fragment : openNavFragments.values()) { for (Fragment fragment : openNavFragments.values()) {
if (fragment instanceof WalletFragment) { if (fragment instanceof WalletFragment) {
((WalletFragment) fragment).onWalletSyncEnabled(); ((WalletFragment) fragment).onWalletSyncEnabled();
} }
} }
scheduleWalletSyncTask();
} }
} }
} }
@ -1063,7 +1229,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
Lbryio.newInstall(context); Lbryio.newInstall(context);
// (light) fetch subscriptions // (light) fetch subscriptions
if (Lbryio.cacheSubscriptions.size() == 0) { if (Lbryio.subscriptions.size() == 0) {
List<Subscription> subscriptions = new ArrayList<>(); List<Subscription> subscriptions = new ArrayList<>();
List<String> subUrls = new ArrayList<>(); List<String> subUrls = new ArrayList<>();
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context)); JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
@ -1079,10 +1245,10 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
subscriptions.add(new Subscription(channelName, url.toString())); subscriptions.add(new Subscription(channelName, url.toString()));
subUrls.add(url.toString()); subUrls.add(url.toString());
} }
Lbryio.cacheSubscriptions = subscriptions; Lbryio.subscriptions = subscriptions;
// resolve subscriptions // resolve subscriptions
if (subUrls.size() > 0 && Lbryio.cacheResolvedSubscriptions.size() != Lbryio.cacheSubscriptions.size()) { if (subUrls.size() > 0 && Lbryio.cacheResolvedSubscriptions.size() != Lbryio.subscriptions.size()) {
List<Claim> resolvedSubs = Lbry.resolve(subUrls, Lbry.LBRY_TV_CONNECTION_STRING); List<Claim> resolvedSubs = Lbry.resolve(subUrls, Lbry.LBRY_TV_CONNECTION_STRING);
Lbryio.cacheResolvedSubscriptions = resolvedSubs; Lbryio.cacheResolvedSubscriptions = resolvedSubs;
} }
@ -1481,8 +1647,4 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
}); });
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private void loadTags() {
}
} }

View file

@ -5,6 +5,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
import java.sql.SQLInput;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -48,8 +49,8 @@ public class DatabaseHelper extends SQLiteOpenHelper {
private static final String SQL_GET_RECENT_HISTORY = "SELECT value, url, type FROM history ORDER BY timestamp DESC LIMIT 10"; private static final String SQL_GET_RECENT_HISTORY = "SELECT value, url, type FROM history ORDER BY timestamp DESC LIMIT 10";
private static final String SQL_INSERT_TAG = "REPLACE INTO tags (name, is_followed) VALUES (?, ?)"; private static final String SQL_INSERT_TAG = "REPLACE INTO tags (name, is_followed) VALUES (?, ?)";
private static final String SQL_SET_TAG_FOLLOWED = "UPDATE tags SET is_followed = ? WHERE name = ?"; private static final String SQL_GET_KNOWN_TAGS = "SELECT name, is_followed FROM tags";
private static final String SQL_GET_KNOWN_TAGS = "SELECT name FROM tags"; private static final String SQL_UNFOLLOW_TAGS = "UPDATE tags SET is_followed = 0";
private static final String SQL_GET_FOLLOWED_TAGS = "SELECT name FROM tags WHERE is_followed = 1"; private static final String SQL_GET_FOLLOWED_TAGS = "SELECT name FROM tags WHERE is_followed = 1";
public DatabaseHelper(Context context) { public DatabaseHelper(Context context) {
@ -108,8 +109,8 @@ public class DatabaseHelper extends SQLiteOpenHelper {
public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) { public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) {
db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 }); db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 });
} }
public static void setTagFollowed(boolean followed, String name, SQLiteDatabase db) { public static void setAllTagsUnfollowed(SQLiteDatabase db) {
db.execSQL(SQL_SET_TAG_FOLLOWED, new Object[] { followed ? 1 : 0, name }); db.execSQL(SQL_UNFOLLOW_TAGS);
} }
public static List<Tag> getTags(SQLiteDatabase db) { public static List<Tag> getTags(SQLiteDatabase db) {
List<Tag> tags = new ArrayList<>(); List<Tag> tags = new ArrayList<>();

View file

@ -0,0 +1,23 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class EditorsChoiceItem {
private boolean header;
private String title;
private String parent;
private String description;
private String thumbnailUrl;
private String permanentUrl;
public static EditorsChoiceItem fromClaim(Claim claim) {
EditorsChoiceItem item = new EditorsChoiceItem();
item.setTitle(claim.getTitle());
item.setDescription(claim.getDescription());
item.setThumbnailUrl(claim.getThumbnailUrl());
item.setPermanentUrl(claim.getPermanentUrl());
return item;
}
}

View file

@ -29,6 +29,9 @@ public class Tag implements Comparator<Tag> {
return Predefined.MATURE_TAGS.contains(name.toLowerCase()); return Predefined.MATURE_TAGS.contains(name.toLowerCase());
} }
public String toString() {
return getLowercaseName();
}
public boolean equals(Object o) { public boolean equals(Object o) {
return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name); return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name);
} }

View file

@ -1,10 +1,14 @@
package io.lbry.browser.model.lbryinc; package io.lbry.browser.model.lbryinc;
import lombok.Data; import lombok.Getter;
import lombok.Setter;
@Data
public class Subscription { public class Subscription {
@Getter
@Setter
private String channelName; private String channelName;
@Getter
@Setter
private String url; private String url;
public Subscription() { public Subscription() {
@ -14,4 +18,11 @@ public class Subscription {
this.channelName = channelName; this.channelName = channelName;
this.url = url; this.url = url;
} }
public boolean equals(Object o) {
return (o instanceof Subscription) && url.equalsIgnoreCase(((Subscription) o).getUrl());
}
public int hashCode() {
return url.toLowerCase().hashCode();
}
} }

View file

@ -55,6 +55,12 @@ public class ChannelSubscribeTask extends AsyncTask<Void, Void, Boolean> {
String action = isUnsubscribing ? "delete" : "new"; String action = isUnsubscribing ? "delete" : "new";
Lbryio.call("subscription", action, options, context); Lbryio.call("subscription", action, options, context);
if (!isUnsubscribing) {
Lbryio.addSubscription(subscription);
} else {
Lbryio.removeSubscription(subscription);
}
} catch (LbryioRequestException | LbryioResponseException | SQLiteException ex) { } catch (LbryioRequestException | LbryioResponseException | SQLiteException ex) {
error = ex; error = ex;
return false; return false;

View file

@ -61,7 +61,7 @@ public class FetchSubscriptionsTask extends AsyncTask<Void, Void, List<Subscript
} }
} }
} }
} catch (LbryioRequestException | LbryioResponseException | JSONException | ClassCastException ex) { } catch (ClassCastException | LbryioRequestException | LbryioResponseException | JSONException | IllegalStateException ex) {
error = ex; error = ex;
return null; return null;
} finally { } finally {

View file

@ -0,0 +1,65 @@
package io.lbry.browser.tasks;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.model.Tag;
import io.lbry.browser.utils.Lbry;
public class FollowUnfollowTagTask extends AsyncTask<Void, Void, Boolean> {
private Tag tag;
private boolean unfollowing;
private Context context;
private FollowUnfollowTagHandler handler;
private Exception error;
public FollowUnfollowTagTask(Tag tag, boolean unfollowing, Context context, FollowUnfollowTagHandler handler) {
this.tag = tag;
this.context = context;
this.unfollowing = unfollowing;
this.handler = handler;
}
public Boolean doInBackground(Void... params) {
try {
SQLiteDatabase db = null;
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getWritableDatabase();
if (db != null) {
if (!Lbry.knownTags.contains(tag)) {
DatabaseHelper.createOrUpdateTag(tag, db);
Lbry.addKnownTag(tag);
}
tag.setFollowed(!unfollowing);
DatabaseHelper.createOrUpdateTag(tag, db);
if (unfollowing) {
Lbry.removeFollowedTag(tag);
} else {
Lbry.addFollowedTag(tag);
}
return true;
}
}
} catch (Exception ex) {
error = ex;
}
return false;
}
protected void onPostExecute(Boolean result) {
if (handler != null) {
if (result) {
handler.onSuccess(tag, unfollowing);
} else {
handler.onError(error);
}
}
}
public interface FollowUnfollowTagHandler {
void onSuccess(Tag tag, boolean unfollowing);
void onError(Exception error);
}
}

View file

@ -0,0 +1,134 @@
package io.lbry.browser.tasks;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
// background task to create a diff of local and remote subscriptions and try to merge
public class MergeSubscriptionsTask extends AsyncTask<Void, Void, List<Subscription>> {
private static final String TAG = "MergeSubscriptionsTask";
private Context context;
private List<Subscription> base;
private List<Subscription> diff;
private MergeSubscriptionsHandler handler;
private Exception error;
public MergeSubscriptionsTask(List<Subscription> base, Context context, MergeSubscriptionsHandler handler) {
this.base = base;
this.context = context;
this.handler = handler;
}
protected List<Subscription> doInBackground(Void... params) {
List<Subscription> combined = new ArrayList<>(base);
List<Subscription> localSubs = new ArrayList<>();
List<Subscription> remoteSubs = new ArrayList<>();
diff = new ArrayList<>();
SQLiteDatabase db = null;
try {
// fetch local subscriptions
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getWritableDatabase();
}
if (db != null) {
localSubs = DatabaseHelper.getSubscriptions(db);
for (Subscription sub : localSubs) {
if (!combined.contains(sub)) {
combined.add(sub);
}
}
}
// fetch remote subscriptions
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
if (array != null) {
for (int i = 0; i < array.length(); i++) {
JSONObject item = array.getJSONObject(i);
String claimId = item.getString("claim_id");
String channelName = item.getString("channel_name");
LbryUri url = new LbryUri();
url.setChannelName(channelName);
url.setClaimId(claimId);
Subscription subscription = new Subscription(channelName, url.toString());
remoteSubs.add(subscription);
}
}
for (int i = 0; i < combined.size(); i++) {
Subscription local = combined.get(i);
if (!remoteSubs.contains(local)) {
// add to remote subscriptions
try {
LbryUri uri = LbryUri.parse(local.getUrl());
Map<String, String> options = new HashMap<>();
options.put("claim_id", uri.getChannelClaimId());
options.put("channel_name", local.getChannelName());
Lbryio.call("subscription", "new", options, context);
} catch (LbryUriException | LbryioRequestException | LbryioResponseException ex) {
// pass
Log.e(TAG, String.format("subscription/new failed: %s", ex.getMessage()), ex);
}
}
}
for (int i = 0; i < localSubs.size(); i++) {
Subscription local = localSubs.get(i);
if (!base.contains(local) && !diff.contains(local)) {
diff.add(local);
}
}
for (int i = 0; i < remoteSubs.size(); i++) {
Subscription remote = remoteSubs.get(i);
if (!combined.contains(remote)) {
combined.add(remote);
if (!diff.contains(remote)) {
diff.add(remote);
}
}
}
} catch (ClassCastException | LbryioRequestException | LbryioResponseException | JSONException | SQLiteException ex) {
error = ex;
return null;
} finally {
Helper.closeDatabase(db);
}
return combined;
}
protected void onPostExecute(List<Subscription> subscriptions) {
if (handler != null) {
if (subscriptions != null) {
handler.onSuccess(subscriptions, diff);
} else {
handler.onError(error);
}
}
}
public interface MergeSubscriptionsHandler {
void onSuccess(List<Subscription> subscriptions, List<Subscription> diff);
void onError(Exception error);
}
}

View file

@ -0,0 +1,138 @@
package io.lbry.browser.tasks.wallet;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.AsyncTask;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.model.Tag;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
/*
version: '0.1',
value: {
subscriptions?: Array<string>,
tags?: Array<string>,
blocked?: Array<string>,
settings?: any,
app_welcome_version?: number,
sharing_3P?: boolean,
},
*/
public class LoadSharedUserStateTask extends AsyncTask<Void, Void, Boolean> {
private static final String KEY = "shared";
private Context context;
private LoadSharedUserStateHandler handler;
private Exception error;
private List<Subscription> subscriptions;
private List<Tag> followedTags;
public LoadSharedUserStateTask(Context context, LoadSharedUserStateHandler handler) {
this.context = context;
this.handler = handler;
}
protected Boolean doInBackground(Void... params) {
// data to save
// current subscriptions
// Get the previous saved state
try {
SQLiteDatabase db = null;
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_PREFERENCE_GET, Lbry.buildSingleParam("key", KEY));
if (result != null) {
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getWritableDatabase();
}
JSONObject shared = result.getJSONObject("shared");
if (shared.has("type")
&& "object".equalsIgnoreCase(shared.getString("type"))
&& shared.has("value")) {
JSONObject value = shared.getJSONObject("value");
JSONArray subscriptionUrls =
value.has("subscriptions") && !value.isNull("subscriptions") ? value.getJSONArray("subscriptions") : null;
JSONArray tags =
value.has("tags") && !value.isNull("tags") ? value.getJSONArray("tags") : null;
if (subscriptionUrls != null) {
subscriptions = new ArrayList<>();
for (int i = 0; i < subscriptionUrls.length(); i++) {
String url = subscriptionUrls.getString(i);
try {
LbryUri uri = LbryUri.parse(LbryUri.normalize(url));
Subscription subscription = new Subscription();
subscription.setChannelName(uri.getChannelName());
subscription.setUrl(url);
subscriptions.add(subscription);
if (db != null) {
DatabaseHelper.createOrUpdateSubscription(subscription, db);
}
} catch (LbryUriException | SQLiteException ex) {
// pass
}
}
}
if (tags != null) {
if (db != null && tags.length() > 0) {
DatabaseHelper.setAllTagsUnfollowed(db);
}
followedTags = new ArrayList<>();
for (int i = 0; i < tags.length(); i++) {
String tagName = tags.getString(i);
Tag tag = new Tag(tagName);
tag.setFollowed(true);
followedTags.add(tag);
try {
if (db != null) {
DatabaseHelper.createOrUpdateTag(tag, db);
}
} catch (SQLiteException ex) {
// pass
}
}
}
}
}
return true;
} catch (ApiCallException | JSONException ex) {
// failed
error = ex;
}
return false;
}
protected void onPostExecute(Boolean result) {
if (handler != null) {
if (result) {
handler.onSuccess(subscriptions, followedTags);
} else {
handler.onError(error);
}
}
}
public interface LoadSharedUserStateHandler {
void onSuccess(List<Subscription> subscriptions, List<Tag> followedTags);
void onError(Exception error);
}
}

View file

@ -0,0 +1,108 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.Lbryio;
/*
version: '0.1',
value: {
subscriptions?: Array<string>,
tags?: Array<string>,
blocked?: Array<string>,
settings?: any,
app_welcome_version?: number,
sharing_3P?: boolean,
},
*/
public class SaveSharedUserStateTask extends AsyncTask<Void, Void, Boolean> {
private static final String KEY = "shared";
private static final String VERSION = "0.1";
private SaveSharedUserStateHandler handler;
private Exception error;
public SaveSharedUserStateTask(SaveSharedUserStateHandler handler) {
this.handler = handler;
}
protected Boolean doInBackground(Void... params) {
// data to save
// current subscriptions
List<String> subscriptionUrls = new ArrayList<>();
for (Subscription subscription : Lbryio.subscriptions) {
subscriptionUrls.add(subscription.getUrl());
}
// followed tags
List<String> followedTags = Helper.getTagsForTagObjects(Lbry.followedTags);
// Get the previous saved state
try {
boolean isExistingValid = false;
JSONObject sharedObject = null;
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_PREFERENCE_GET, Lbry.buildSingleParam("key", KEY));
if (result != null) {
JSONObject shared = result.getJSONObject("shared");
if (shared.has("type")
&& "object".equalsIgnoreCase(shared.getString("type"))
&& shared.has("value")) {
isExistingValid = true;
JSONObject value = shared.getJSONObject("value");
value.put("subscriptions", Helper.jsonArrayFromList(subscriptionUrls));
value.put("tags", Helper.jsonArrayFromList(followedTags));
sharedObject = shared;
}
}
if (!isExistingValid) {
// build a new object
JSONObject value = new JSONObject();
value.put("subscriptions", Helper.jsonArrayFromList(subscriptionUrls));
value.put("tags", Helper.jsonArrayFromList(followedTags));
sharedObject = new JSONObject();
sharedObject.put("type", "object");
sharedObject.put("value", value);
sharedObject.put("version", VERSION);
}
Map<String, Object> options = new HashMap<>();
options.put("key", KEY);
options.put("value", sharedObject.toString());
Lbry.genericApiCall(Lbry.METHOD_PREFERENCE_SET, options);
return true;
} catch (ApiCallException | JSONException ex) {
// failed
error = ex;
}
return false;
}
protected void onPostExecute(Boolean result) {
if (handler != null) {
if (result) {
handler.onSuccess();
} else {
handler.onError(error);
}
}
}
public interface SaveSharedUserStateHandler {
void onSuccess();
void onError(Exception error);
}
}

View file

@ -3,6 +3,7 @@ package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.view.View; import android.view.View;
import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.HashMap; import java.util.HashMap;
@ -24,8 +25,9 @@ public class SyncApplyTask extends AsyncTask<Void, Void, Boolean> {
private String syncHash; private String syncHash;
private String syncData; private String syncData;
public SyncApplyTask(boolean fetch, SyncTaskHandler handler) { public SyncApplyTask(boolean fetch, String password, SyncTaskHandler handler) {
this.fetch = fetch; this.fetch = fetch;
this.password = password;
this.handler = handler; this.handler = handler;
} }

View file

@ -48,7 +48,7 @@ public class SyncGetTask extends AsyncTask<Void, Void, WalletSync> {
boolean unlockSuccessful = boolean unlockSuccessful =
!isLocked || (boolean) Lbry.genericApiCall(Lbry.METHOD_WALLET_UNLOCK, Lbry.buildSingleParam("password", password)); !isLocked || (boolean) Lbry.genericApiCall(Lbry.METHOD_WALLET_UNLOCK, Lbry.buildSingleParam("password", password));
if (!unlockSuccessful) { if (!unlockSuccessful) {
throw new WalletException("The wallet could be unlocked with the provided password."); throw new WalletException("The wallet could not be unlocked with the provided password.");
} }
String hash = (String) Lbry.genericApiCall(Lbry.METHOD_SYNC_HASH); String hash = (String) Lbry.genericApiCall(Lbry.METHOD_SYNC_HASH);

View file

@ -33,14 +33,19 @@ import io.lbry.browser.dialog.CustomizeTagsDialogFragment;
import io.lbry.browser.model.Claim; import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Tag; import io.lbry.browser.model.Tag;
import io.lbry.browser.tasks.ClaimSearchTask; import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.tasks.FollowUnfollowTagTask;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.tasks.wallet.SaveSharedUserStateTask;
import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry; import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.Predefined; import io.lbry.browser.utils.Predefined;
import lombok.Getter;
// TODO: Similar code to FollowingFragment and Channel page fragment. Probably make common operations (sorting/filtering) into a control // TODO: Similar code to FollowingFragment and Channel page fragment. Probably make common operations (sorting/filtering) into a control
public class AllContentFragment extends BaseFragment implements SharedPreferences.OnSharedPreferenceChangeListener { public class AllContentFragment extends BaseFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@Getter
private boolean singleTagView; private boolean singleTagView;
private List<String> tags; private List<String> tags;
private View layoutFilterContainer; private View layoutFilterContainer;
@ -55,6 +60,7 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
private RecyclerView contentList; private RecyclerView contentList;
private int currentSortBy; private int currentSortBy;
private int currentContentFrom; private int currentContentFrom;
@Getter
private int currentContentScope; private int currentContentScope;
private String contentReleaseTime; private String contentReleaseTime;
private List<String> contentSortOrder; private List<String> contentSortOrder;
@ -184,7 +190,6 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
}); });
checkParams(false); checkParams(false);
return root; return root;
} }
@ -204,7 +209,12 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
titleView.setText(Helper.capitalize(tagName)); titleView.setText(Helper.capitalize(tagName));
} else { } else {
singleTagView = false; singleTagView = false;
tags = null; // default to followed Tags scope if any tags are followed
tags = Helper.getTagsForTagObjects(Lbry.followedTags);
if (tags.size() > 0) {
currentContentScope = ContentScopeDialogFragment.ITEM_TAGS;
Helper.setViewVisibility(customizeLink, View.VISIBLE);
}
titleView.setText(getString(R.string.all_content)); titleView.setText(getString(R.string.all_content));
} }
@ -255,12 +265,16 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
public void onTagAdded(Tag tag) { public void onTagAdded(Tag tag) {
// heavy-lifting // heavy-lifting
// save to local, save to wallet and then sync // save to local, save to wallet and then sync
FollowUnfollowTagTask task = new FollowUnfollowTagTask(tag, false, getContext(), followUnfollowHandler);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
@Override @Override
public void onTagRemoved(Tag tag) { public void onTagRemoved(Tag tag) {
// heavy-lifting // heavy-lifting
// save to local, save to wallet and then sync // save to local, save to wallet and then sync
FollowUnfollowTagTask task = new FollowUnfollowTagTask(tag, true, getContext(), followUnfollowHandler);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
}); });
Context context = getContext(); Context context = getContext();
@ -270,6 +284,30 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
} }
} }
private FollowUnfollowTagTask.FollowUnfollowTagHandler followUnfollowHandler = new FollowUnfollowTagTask.FollowUnfollowTagHandler() {
@Override
public void onSuccess(Tag tag, boolean unfollowing) {
if (tags != null) {
if (unfollowing) {
tags.remove(tag.getLowercaseName());
} else {
tags.add(tag.getLowercaseName());
}
fetchClaimSearchContent(true);
}
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).saveSharedUserState();
}
}
@Override
public void onError(Exception error) {
// pass
}
};
private void onSortByChanged(int sortBy) { private void onSortByChanged(int sortBy) {
currentSortBy = sortBy; currentSortBy = sortBy;
@ -364,7 +402,7 @@ public class AllContentFragment extends BaseFragment implements SharedPreference
fetchClaimSearchContent(false); fetchClaimSearchContent(false);
} }
private void fetchClaimSearchContent(boolean reset) { public void fetchClaimSearchContent(boolean reset) {
if (reset && contentListAdapter != null) { if (reset && contentListAdapter != null) {
contentListAdapter.clearItems(); contentListAdapter.clearItems();
currentClaimSearchPage = 1; currentClaimSearchPage = 1;

View file

@ -0,0 +1,6 @@
package io.lbry.browser.ui.editorschoice;
import io.lbry.browser.ui.BaseFragment;
public class EditorsChoiceFragment extends BaseFragment {
}

View file

@ -12,7 +12,6 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -333,16 +332,8 @@ public class FollowingFragment extends BaseFragment implements
showSuggestedChannels(); showSuggestedChannels();
} }
if (Lbryio.cacheSubscriptions != null && Lbryio.cacheSubscriptions.size() > 0) { if (Lbryio.subscriptions != null && Lbryio.subscriptions.size() > 0) {
subscriptionsList = new ArrayList<>(Lbryio.cacheSubscriptions); fetchLoadedSubscriptions();
buildChannelIdsAndUrls();
if (Lbryio.cacheResolvedSubscriptions.size() > 0) {
updateChannelFilterListAdapter(Lbryio.cacheResolvedSubscriptions);
} else {
fetchAndResolveChannelList();
}
fetchClaimSearchContent();
showSubscribedContent();
} else { } else {
fetchSubscriptions(); fetchSubscriptions();
} }
@ -351,6 +342,17 @@ public class FollowingFragment extends BaseFragment implements
PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this);
super.onPause(); super.onPause();
} }
public void fetchLoadedSubscriptions() {
subscriptionsList = new ArrayList<>(Lbryio.subscriptions);
buildChannelIdsAndUrls();
if (Lbryio.cacheResolvedSubscriptions.size() > 0) {
updateChannelFilterListAdapter(Lbryio.cacheResolvedSubscriptions);
} else {
fetchAndResolveChannelList();
}
fetchClaimSearchContent();
showSubscribedContent();
}
public void loadFollowing() { public void loadFollowing() {
// wrapper to just re-fetch subscriptions (upon user sign in, for example) // wrapper to just re-fetch subscriptions (upon user sign in, for example)
@ -662,7 +664,7 @@ public class FollowingFragment extends BaseFragment implements
fetchSuggestedChannels(); fetchSuggestedChannels();
showSuggestedChannels(); showSuggestedChannels();
} else { } else {
Lbryio.cacheSubscriptions = subscriptions; Lbryio.subscriptions = subscriptions;
subscriptionsList = new ArrayList<>(subscriptions); subscriptionsList = new ArrayList<>(subscriptions);
showSubscribedContent(); showSubscribedContent();
fetchAndResolveChannelList(); fetchAndResolveChannelList();
@ -686,6 +688,7 @@ public class FollowingFragment extends BaseFragment implements
if (discoverDialog != null) { if (discoverDialog != null) {
fetchSubscriptions(); fetchSubscriptions();
} }
saveSharedUserState();
} }
@Override @Override
@ -709,6 +712,7 @@ public class FollowingFragment extends BaseFragment implements
if (discoverDialog != null) { if (discoverDialog != null) {
fetchSubscriptions(); fetchSubscriptions();
} }
saveSharedUserState();
} }
@Override @Override
@ -729,6 +733,13 @@ public class FollowingFragment extends BaseFragment implements
Helper.setViewVisibility(noContentView, noContent ? View.VISIBLE : View.GONE); Helper.setViewVisibility(noContentView, noContent ? View.VISIBLE : View.GONE);
} }
private void saveSharedUserState() {
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).saveSharedUserState();
}
}
public void onSharedPreferenceChanged(SharedPreferences sp, String key) { public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) { if (key.equalsIgnoreCase(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT)) {
fetchClaimSearchContent(true); fetchClaimSearchContent(true);

View file

@ -188,7 +188,7 @@ public class WalletVerificationFragment extends Fragment {
} }
public void processNewWallet() { public void processNewWallet() {
SyncApplyTask fetchTask = new SyncApplyTask(true, new DefaultSyncTaskHandler() { SyncApplyTask fetchTask = new SyncApplyTask(true, null, new DefaultSyncTaskHandler() {
@Override @Override
public void onSyncApplySuccess(String hash, String data) { createNewRemoteSync(hash, data); } public void onSyncApplySuccess(String hash, String data) { createNewRemoteSync(hash, data); }
@Override @Override

View file

@ -308,8 +308,6 @@ public class WalletFragment extends BaseFragment implements SdkStatusListener, W
} }
public void onWalletSyncEnabled() { public void onWalletSyncEnabled() {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, true).apply();
switchSyncStatus.setText(R.string.on); switchSyncStatus.setText(R.string.on);
switchSyncStatus.setChecked(true); switchSyncStatus.setChecked(true);
textWalletHintSyncStatus.setText(R.string.backup_synced); textWalletHintSyncStatus.setText(R.string.backup_synced);

View file

@ -316,7 +316,7 @@ public final class Helper {
} }
public static List<Tag> filterFollowedTags(List<Tag> tags) { public static List<Tag> filterFollowedTags(List<Tag> tags) {
List<Tag> followedTags = new ArrayList<>(); List<Tag> followedTags = new ArrayList<>();
for (Tag tag : followedTags) { for (Tag tag : tags) {
if (tag.isFollowed()) { if (tag.isFollowed()) {
followedTags.add(tag); followedTags.add(tag);
} }

View file

@ -41,6 +41,7 @@ import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
public final class Lbry { public final class Lbry {
private static final Object lock = new Object();
public static LinkedHashMap<ClaimCacheKey, Claim> claimCache = new LinkedHashMap<>(); public static LinkedHashMap<ClaimCacheKey, Claim> claimCache = new LinkedHashMap<>();
public static LinkedHashMap<Map<String, Object>, ClaimSearchCacheValue> claimSearchCache = new LinkedHashMap<>(); public static LinkedHashMap<Map<String, Object>, ClaimSearchCacheValue> claimSearchCache = new LinkedHashMap<>();
public static WalletBalance walletBalance = new WalletBalance(); public static WalletBalance walletBalance = new WalletBalance();
@ -85,7 +86,6 @@ public final class Lbry {
public static final String METHOD_PREFERENCE_GET = "preference_get"; public static final String METHOD_PREFERENCE_GET = "preference_get";
public static final String METHOD_PREFERENCE_SET = "preference_set"; public static final String METHOD_PREFERENCE_SET = "preference_set";
public static KeyStore KEYSTORE; public static KeyStore KEYSTORE;
public static boolean SDK_READY = false; public static boolean SDK_READY = false;
@ -165,8 +165,10 @@ public final class Lbry {
JSONObject json = new JSONObject(responseString); JSONObject json = new JSONObject(responseString);
if (response.code() >= 200 && response.code() < 300) { if (response.code() >= 200 && response.code() < 300) {
if (json.has("result")) { if (json.has("result")) {
Object result = json.get("result"); if (json.isNull("result")) {
return result; return null;
}
return json.get("result");
} else { } else {
processErrorJson(json); processErrorJson(json);
} }
@ -417,4 +419,23 @@ public final class Lbry {
public static Object genericApiCall(String method) throws ApiCallException { public static Object genericApiCall(String method) throws ApiCallException {
return genericApiCall(method, null); return genericApiCall(method, null);
} }
public static void addFollowedTag(Tag tag) {
synchronized (lock) {
if (!followedTags.contains(tag)) {
followedTags.add(tag);
}
}
}
public static void removeFollowedTag(Tag tag) {
synchronized (lock) {
followedTags.remove(tag);
}
}
public static void addKnownTag(Tag tag) {
synchronized (lock) {
if (!knownTags.contains(tag)) {
knownTags.add(tag);
}
}
}
} }

View file

@ -51,7 +51,7 @@ public final class Lbryio {
public static final String TAG = "Lbryio"; public static final String TAG = "Lbryio";
public static final String CONNECTION_STRING = "https://api.lbry.com"; public static final String CONNECTION_STRING = "https://api.lbry.com";
public static final String AUTH_TOKEN_PARAM = "auth_token"; public static final String AUTH_TOKEN_PARAM = "auth_token";
public static List<Subscription> cacheSubscriptions = new ArrayList<>(); public static List<Subscription> subscriptions = new ArrayList<>();
public static List<Claim> cacheResolvedSubscriptions = new ArrayList<>(); public static List<Claim> cacheResolvedSubscriptions = new ArrayList<>();
public static double LBCUSDRate = 0; public static double LBCUSDRate = 0;
public static String AUTH_TOKEN; public static String AUTH_TOKEN;
@ -280,4 +280,17 @@ public final class Lbryio {
lastRemoteHash = hash; lastRemoteHash = hash;
} }
} }
public static void addSubscription(Subscription subscription) {
synchronized (lock) {
if (!subscriptions.contains(subscription)) {
subscriptions.add(subscription);
}
}
}
public static void removeSubscription(Subscription subscription) {
synchronized (lock) {
subscriptions.remove(subscription);
}
}
} }

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/editors_choice_content_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/editors_choice_header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:textSize="28sp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp" />
<androidx.cardview.widget.CardView
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/editors_choice_content_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:textSize="20sp" />
<RelativeLayout
android:layout_marginTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/editors_choice_content_thumbnail"
android:layout_width="160dp"
android:layout_height="90dp"
android:layout_centerVertical="true"/>
<TextView
android:id="@+id/editors_choice_content_description"
android:layout_toRightOf="@id/editors_choice_content_thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:fontFamily="@font/inter"
android:ellipsize="end"
android:maxLines="6"
android:textFontWeight="300"
android:textSize="14sp" />
</RelativeLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>

View file

@ -16,12 +16,14 @@
android:id="@+id/tag_action" android:id="@+id/tag_action"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="16dp" android:layout_height="16dp"
android:layout_gravity="center_vertical"
android:layout_marginRight="4dp" android:layout_marginRight="4dp"
android:tint="@color/darkForeground" /> android:tint="@color/darkForeground" />
<TextView <TextView
android:id="@+id/tag_name" android:id="@+id/tag_name"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:fontFamily="@font/inter" android:fontFamily="@font/inter"
android:layout_marginLeft="4dp" android:layout_marginLeft="4dp"
android:textSize="12sp" android:textSize="12sp"