From 244d54ed39cb90f1cbf74ebd38b33e2000b50f2d Mon Sep 17 00:00:00 2001 From: Akinwale Ariwodola Date: Fri, 15 May 2020 20:07:05 +0100 Subject: [PATCH] Library page. URL and view history. --- .../io/lbry/browser/FileViewActivity.java | 32 +- .../java/io/lbry/browser/MainActivity.java | 117 ++++- .../browser/adapter/ClaimListAdapter.java | 106 +++++ .../io/lbry/browser/data/DatabaseHelper.java | 41 ++ .../listener/DownloadActionListener.java | 5 + .../java/io/lbry/browser/model/Claim.java | 9 + .../java/io/lbry/browser/model/LbryFile.java | 38 ++ .../io/lbry/browser/model/ViewHistory.java | 6 +- .../lbry/browser/tasks/file/FileListTask.java | 14 +- .../tasks/localdata/FetchViewHistoryTask.java | 46 ++ .../tasks/localdata/SaveViewHistoryTask.java | 43 +- .../ui/allcontent/AllContentFragment.java | 35 +- .../ui/channel/ChannelContentFragment.java | 39 +- .../ui/following/FollowingFragment.java | 36 +- .../browser/ui/library/LibraryFragment.java | 423 ++++++++++++++++++ .../browser/ui/search/SearchFragment.java | 38 +- .../browser/ui/wallet/WalletFragment.java | 6 +- .../java/io/lbry/browser/utils/Helper.java | 83 ++++ .../main/java/io/lbry/browser/utils/Lbry.java | 12 +- app/src/main/res/layout/app_bar_main.xml | 193 ++++++++ app/src/main/res/layout/fragment_library.xml | 347 ++++++++++++++ app/src/main/res/layout/list_item_stream.xml | 66 ++- app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 25 ++ 25 files changed, 1711 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java create mode 100644 app/src/main/java/io/lbry/browser/tasks/localdata/FetchViewHistoryTask.java create mode 100644 app/src/main/java/io/lbry/browser/ui/library/LibraryFragment.java create mode 100644 app/src/main/res/layout/fragment_library.xml diff --git a/app/src/main/java/io/lbry/browser/FileViewActivity.java b/app/src/main/java/io/lbry/browser/FileViewActivity.java index 83a90954..29c708dd 100644 --- a/app/src/main/java/io/lbry/browser/FileViewActivity.java +++ b/app/src/main/java/io/lbry/browser/FileViewActivity.java @@ -169,9 +169,9 @@ public class FileViewActivity extends AppCompatActivity { ClaimCacheKey key = new ClaimCacheKey(); key.setClaimId(claimId); key.setUrl(url); // use the same url for the key so that we can match the key for any value that's the same - if (Lbry.claimCache.containsKey(key)) { claim = Lbry.claimCache.get(key); + Helper.saveViewHistory(url, claim); checkAndResetNowPlayingClaim(); if (claim.getFile() == null) { loadFile(); @@ -314,6 +314,7 @@ public class FileViewActivity extends AppCompatActivity { if (Lbry.claimCache.containsKey(key)) { claim = Lbry.claimCache.get(key); Helper.saveUrlHistory(url, claim.getTitle(), UrlSuggestion.TYPE_FILE); + Helper.saveViewHistory(url, claim); checkAndResetNowPlayingClaim(); if (claim.getFile() == null) { loadFile(); @@ -423,7 +424,7 @@ public class FileViewActivity extends AppCompatActivity { String claimId = claim.getClaimId(); FileListTask task = new FileListTask(claimId, null, new FileListTask.FileListResultHandler() { @Override - public void onSuccess(List files) { + public void onSuccess(List files, boolean hasReachedEnd) { if (files.size() > 0) { claim.setFile(files.get(0)); checkIsFileComplete(); @@ -445,7 +446,7 @@ public class FileViewActivity extends AppCompatActivity { if (initialFileLoadDone) { restoreMainActionButton(); } - if (claim != null && claim.isFree()) { + if (claim != null && claim.isFree() && (claim.isPlayable() || claim.isViewable())) { onMainActionButtonClicked(); } } @@ -491,6 +492,8 @@ public class FileViewActivity extends AppCompatActivity { Helper.saveUrlHistory(url, claim.getTitle(), UrlSuggestion.TYPE_FILE); // also save view history + Helper.saveViewHistory(url, claim); + checkAndResetNowPlayingClaim(); loadFile(); renderClaim(); @@ -1576,9 +1579,11 @@ public class FileViewActivity extends AppCompatActivity { String uri = intent.getStringExtra("uri"); String outpoint = intent.getStringExtra("outpoint"); String fileInfoJson = intent.getStringExtra("file_info"); + double progress = intent.getDoubleExtra("progress", 0); if (claim == null || uri == null || outpoint == null || (fileInfoJson == null && !"abort".equals(downloadAction))) { return; } + onRelatedDownloadAction(downloadAction, uri, outpoint, fileInfoJson, progress); if (claim != null && !claim.getPermanentUrl().equalsIgnoreCase(uri)) { return; } @@ -1604,7 +1609,6 @@ public class FileViewActivity extends AppCompatActivity { } else if (DownloadManager.ACTION_UPDATE.equals(downloadAction)) { // handle download updated downloadInProgress = true; - double progress = intent.getDoubleExtra("progress", 0); Helper.setViewVisibility(downloadProgressView, View.VISIBLE); downloadProgressView.setProgress(Double.valueOf(progress).intValue()); downloadIconView.setImageResource(R.drawable.ic_stop); @@ -1692,6 +1696,26 @@ public class FileViewActivity extends AppCompatActivity { } } + private void onRelatedDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (relatedContentAdapter != null) { + relatedContentAdapter.clearFileForClaimOrUrl(outpoint, uri); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (relatedContentAdapter != null) { + relatedContentAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri); + } + } catch (JSONException ex) { + // invalid file info for download + } + } + private void bringMainTaskToFront() { if (backStackLost) { ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index b9775219..9ef78c08 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -15,6 +15,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.TypedArray; import android.database.sqlite.SQLiteDatabase; +import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; @@ -32,11 +33,13 @@ import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.offline.Download; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.tasks.OnCompleteListener; @@ -78,6 +81,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; @@ -89,6 +93,7 @@ import io.lbry.browser.adapter.UrlSuggestionListAdapter; import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.dialog.ContentScopeDialogFragment; import io.lbry.browser.exceptions.LbryUriException; +import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.listener.FetchChannelsListener; import io.lbry.browser.listener.SdkStatusListener; import io.lbry.browser.listener.WalletBalanceListener; @@ -121,6 +126,7 @@ import io.lbry.browser.ui.channel.ChannelFragment; import io.lbry.browser.ui.channel.ChannelManagerFragment; import io.lbry.browser.ui.editorschoice.EditorsChoiceFragment; import io.lbry.browser.ui.following.FollowingFragment; +import io.lbry.browser.ui.library.LibraryFragment; import io.lbry.browser.ui.other.AboutFragment; import io.lbry.browser.ui.search.SearchFragment; import io.lbry.browser.ui.other.SettingsFragment; @@ -133,6 +139,7 @@ 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.lbrysdk.DownloadManager; import io.lbry.lbrysdk.LbrynetService; import io.lbry.lbrysdk.ServiceHelper; import io.lbry.lbrysdk.Utils; @@ -246,6 +253,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener @Getter private DatabaseHelper dbHelper; private int selectedMenuItemId = -1; + private List downloadActionListeners; private List sdkStatusListeners; private List walletBalanceListeners; private List fetchChannelsListeners; @@ -278,6 +286,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // your content NavMenuItem.ID_ITEM_CHANNELS, + NavMenuItem.ID_ITEM_LIBRARY, // wallet NavMenuItem.ID_ITEM_WALLET, @@ -336,6 +345,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // other openNavFragments = new HashMap<>(); + downloadActionListeners = new ArrayList<>(); sdkStatusListeners = new ArrayList<>(); walletBalanceListeners = new ArrayList<>(); fetchChannelsListeners = new ArrayList<>(); @@ -355,6 +365,14 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener shouldOpenUserSelectedMenuItem = false; } } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + if (slideOffset != 0) { + clearWunderbarFocus(findViewById(R.id.wunderbar)); + } + super.onDrawerSlide(drawerView, slideOffset); + } }; drawer.addDrawerListener(toggle); toggle.syncState(); @@ -435,7 +453,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener specialRouteFragmentClassMap.put("channels", ChannelManagerFragment.class); specialRouteFragmentClassMap.put("invite", InvitesFragment.class); specialRouteFragmentClassMap.put("invites", InvitesFragment.class); - //specialRouteFragmentClassMap.put("library", LibraryFragment.class); + specialRouteFragmentClassMap.put("library", LibraryFragment.class); //specialRouteFragmentClassMap.put("publish", PublishFragment.class); //specialRouteFragmentClassMap.put("publishes", PublishesFragment.class); specialRouteFragmentClassMap.put("following", FollowingFragment.class); @@ -452,6 +470,16 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener checkNotificationOpenIntent(intent); } + public void addDownloadActionListener(DownloadActionListener listener) { + if (!downloadActionListeners.contains(listener)) { + downloadActionListeners.add(listener); + } + } + + public void removeDownloadActionListener(DownloadActionListener listener) { + downloadActionListeners.remove(listener); + } + public void addSdkStatusListener(SdkStatusListener listener) { if (!sdkStatusListeners.contains(listener)) { sdkStatusListeners.add(listener); @@ -504,6 +532,9 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener case NavMenuItem.ID_ITEM_CHANNELS: openFragment(ChannelManagerFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS); break; + case NavMenuItem.ID_ITEM_LIBRARY: + openFragment(LibraryFragment.class, true, NavMenuItem.ID_ITEM_LIBRARY); + break; case NavMenuItem.ID_ITEM_WALLET: openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET); @@ -1094,6 +1125,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } else if (!appStarted) { // first run completed, startup startup(); + } else if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + openFragment(FollowingFragment.class, false, NavMenuItem.ID_ITEM_FOLLOWING); } } @@ -1146,9 +1179,12 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE); + + syncWalletAndLoadPreferences(); scheduleWalletBalanceUpdate(); scheduleWalletSyncTask(); fetchChannels(); + initFloatingWalletBalance(); } @@ -1317,7 +1353,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener return walletSyncEnabled && Lbryio.isSignedIn(); } - private void syncWalletAndLoadPreferences() { + public void syncWalletAndLoadPreferences() { if (!userSyncEnabled()) { return; } @@ -1629,6 +1665,26 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener actionBar.show(); } } + private void renderStartupFailed(Map startupStages) { + Map startupStageIconIds = new HashMap<>(); + startupStageIconIds.put(STARTUP_STAGE_INSTALL_ID_LOADED, R.id.startup_stage_icon_install_id); + startupStageIconIds.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, R.id.startup_stage_icon_known_tags); + startupStageIconIds.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, R.id.startup_stage_icon_exchange_rate); + startupStageIconIds.put(STARTUP_STAGE_USER_AUTHENTICATED, R.id.startup_stage_icon_user_authenticated); + startupStageIconIds.put(STARTUP_STAGE_NEW_INSTALL_DONE, R.id.startup_stage_icon_install_new); + startupStageIconIds.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, R.id.startup_stage_icon_subscriptions_loaded); + startupStageIconIds.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, R.id.startup_stage_icon_subscriptions_resolved); + + for (Integer key : startupStages.keySet()) { + boolean stageDone = startupStages.get(key); + ImageView icon = findViewById(startupStageIconIds.get(key)); + icon.setImageResource(stageDone ? R.drawable.ic_check : R.drawable.ic_close); + icon.setColorFilter(stageDone ? Color.WHITE : Color.RED); + } + + findViewById(R.id.splash_view_loading_container).setVisibility(View.GONE); + findViewById(R.id.splash_view_error_container).setVisibility(View.VISIBLE); + } private void startup() { final Context context = this; @@ -1636,11 +1692,23 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener // perform some tasks before launching (new AsyncTask() { + private Map startupStages = new HashMap<>(); + + private void initStartupStages() { + startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, false); + startupStages.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, false); + startupStages.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, false); + startupStages.put(STARTUP_STAGE_USER_AUTHENTICATED, false); + startupStages.put(STARTUP_STAGE_NEW_INSTALL_DONE, false); + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, false); + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, false); + } protected void onPreExecute() { hideActionBar(); lockDrawer(); findViewById(R.id.splash_view).setVisibility(View.VISIBLE); LbryAnalytics.setCurrentScreen(MainActivity.this, "Splash", "Splash"); + initStartupStages(); } protected Boolean doInBackground(Void... params) { BufferedReader reader = null; @@ -1652,29 +1720,35 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener String installId = reader.readLine(); if (Helper.isNullOrEmpty(installId)) { // no install_id found (first run didn't start the sdk successfully?) + startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, false); return false; } + Lbry.INSTALLATION_ID = installId; + startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, true); + SQLiteDatabase db = dbHelper.getReadableDatabase(); List fetchedTags = DatabaseHelper.getTags(db); Lbry.knownTags = Helper.mergeKnownTags(fetchedTags); Collections.sort(Lbry.knownTags, new Tag()); Lbry.followedTags = Helper.filterFollowedTags(Lbry.knownTags); + startupStages.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, true); // load the exchange rate + Lbryio.loadExchangeRate(); if (Lbryio.LBCUSDRate == 0) { - Lbryio.loadExchangeRate(); + return false; } + startupStages.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, true); - Lbry.INSTALLATION_ID = installId; - if (Lbryio.currentUser == null) { - Lbryio.authenticate(context); - } + Lbryio.authenticate(context); if (Lbryio.currentUser == null) { throw new Exception("Did not retrieve authenticated user."); } + startupStages.put(STARTUP_STAGE_USER_AUTHENTICATED, true); Lbryio.newInstall(context); + startupStages.put(STARTUP_STAGE_NEW_INSTALL_DONE, true); // (light) fetch subscriptions if (Lbryio.subscriptions.size() == 0) { @@ -1701,6 +1775,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener Lbryio.cacheResolvedSubscriptions = resolvedSubs; } } + + // if no exceptions occurred here, subscriptions have been loaded and resolved + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true); + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true); + } else { + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true); + startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true); } } catch (Exception ex) { // nope @@ -1714,8 +1795,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } protected void onPostExecute(Boolean startupSuccessful) { if (!startupSuccessful) { - Toast.makeText(context, R.string.startup_failed, Toast.LENGTH_LONG).show(); - finish(); + // show which startup stage failed + renderStartupFailed(startupStages); return; } @@ -1901,6 +1982,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener private void registerServiceActionsReceiver() { IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT); intentFilter.addAction(LbrynetService.LBRY_SDK_SERVICE_STARTED); intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE); serviceActionsReceiver = new BroadcastReceiver() { @@ -1920,6 +2002,19 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener notificationManager.notify(1, svcNotification); } }, 1000); + } else if (DownloadManager.ACTION_DOWNLOAD_EVENT.equalsIgnoreCase(action)) { + String downloadAction = intent.getStringExtra("action"); + String uri = intent.getStringExtra("uri"); + String outpoint = intent.getStringExtra("outpoint"); + String fileInfoJson = intent.getStringExtra("file_info"); + double progress = intent.getDoubleExtra("progress", 0); + if (uri == null || outpoint == null || (fileInfoJson == null && !"abort".equals(downloadAction))) { + return; + } + + for (DownloadActionListener listener : downloadActionListeners) { + listener.onDownloadAction(downloadAction, uri, outpoint, fileInfoJson, progress); + } } } }; @@ -1968,8 +2063,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener yourContentGroup.setItems(Arrays.asList( //new NavMenuItem(NavMenuItem.ID_ITEM_NEW_PUBLISH, R.string.fa_upload, R.string.new_publish, "NewPublish", context), - new NavMenuItem(NavMenuItem.ID_ITEM_CHANNELS, R.string.fa_at, R.string.channels, "Channels", context) - //new NavMenuItem(NavMenuItem.ID_ITEM_LIBRARY, R.string.fa_download, R.string.library, "Library", context) + new NavMenuItem(NavMenuItem.ID_ITEM_CHANNELS, R.string.fa_at, R.string.channels, "Channels", context), + new NavMenuItem(NavMenuItem.ID_ITEM_LIBRARY, R.string.fa_download, R.string.library, "Library", context) //new NavMenuItem(NavMenuItem.ID_ITEM_PUBLISHES, R.string.fa_cloud_upload, R.string.publishes, "Publishes", context) )); diff --git a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java index b0f8bbca..51ac8733 100644 --- a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java @@ -6,6 +6,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; @@ -17,11 +18,14 @@ import com.google.android.material.snackbar.Snackbar; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import io.lbry.browser.R; import io.lbry.browser.listener.SelectionModeListener; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.LbryUri; import io.lbry.browser.utils.Lbryio; @@ -33,6 +37,11 @@ public class ClaimListAdapter extends RecyclerView.Adapter quickClaimIdMap; + private Map quickClaimUrlMap; + private Map notFoundClaimIdMap; + private Map notFoundClaimUrlMap; + @Setter private boolean canEnterSelectionMode; private Context context; @@ -50,6 +59,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter(items); this.selectedItems = new ArrayList<>(); + quickClaimIdMap = new HashMap<>(); + quickClaimUrlMap = new HashMap<>(); + notFoundClaimIdMap = new HashMap<>(); + notFoundClaimUrlMap = new HashMap<>(); } public List getSelectedItems() { @@ -77,6 +90,10 @@ public class ClaimListAdapter extends RecyclerView.Adapter claims) { @@ -119,6 +139,9 @@ public class ClaimListAdapter extends RecyclerView.Adapter -1) { + Claim removed = items.remove(claimIndex); + selectedItems.remove(removed); + } + + notifyDataSetChanged(); + } + @Override public ClaimListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { int viewResourceId = -1; @@ -290,6 +378,24 @@ public class ClaimListAdapter extends RecyclerView.Adapter 0 ? View.VISIBLE : View.GONE); vh.durationView.setText(Helper.formatDuration(duration)); + + LbryFile claimFile = item.getFile(); + boolean isDownloading = false; + int progress = 0; + String fileSizeString = claimFile == null ? null : Helper.formatBytes(claimFile.getTotalBytes(), false); + if (claimFile != null && !claimFile.isCompleted() && claimFile.getWrittenBytes() < claimFile.getTotalBytes()) { + isDownloading = true; + progress = claimFile.getTotalBytes() > 0 ? + Double.valueOf(((double) claimFile.getWrittenBytes() / (double) claimFile.getTotalBytes()) * 100.0).intValue() : 0; + fileSizeString = String.format("%s / %s", + Helper.formatBytes(claimFile.getWrittenBytes(), false), + Helper.formatBytes(claimFile.getTotalBytes(), false)); + } + + Helper.setViewText(vh.fileSizeView, fileSizeString); + Helper.setViewVisibility(vh.downloadProgressView, isDownloading ? View.VISIBLE : View.INVISIBLE); + Helper.setViewProgress(vh.downloadProgressView, progress); + Helper.setViewText(vh.deviceView, item.getDevice()); } else if (Claim.TYPE_CHANNEL.equalsIgnoreCase(item.getValueType())) { if (!Helper.isNullOrEmpty(thumbnailUrl)) { Glide.with(context.getApplicationContext()). diff --git a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java index 19293702..86c8fc1d 100644 --- a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java +++ b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java @@ -4,7 +4,10 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.opengl.Visibility; +import java.math.BigDecimal; +import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; @@ -66,6 +69,9 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String SQL_INSERT_VIEW_HISTORY = "REPLACE INTO view_history (url, claim_id, claim_name, cost, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + private static final String SQL_GET_VIEW_HISTORY = + "SELECT url, claim_id, claim_name, cost, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp " + + "FROM view_history WHERE '' = ? OR timestamp < ? ORDER BY timestamp DESC LIMIT %d"; private static final String SQL_CLEAR_VIEW_HISTORY = "DELETE FROM view_history"; private static final String SQL_CLEAR_VIEW_HISTORY_BY_DEVICE = "DELETE FROM view_history WHERE device = ?"; private static final String SQL_CLEAR_VIEW_HISTORY_BEFORE_TIME = "DELETE FROM view_history WHERE timestamp < ?"; @@ -147,6 +153,41 @@ public class DatabaseHelper extends SQLiteOpenHelper { }); } + public static List getViewHistory(String lastTimestamp, int pageLimit, SQLiteDatabase db) { + List history = new ArrayList<>(); + Cursor cursor = null; + try { + String arg = lastTimestamp == null ? "" : lastTimestamp; + cursor = db.rawQuery(String.format(SQL_GET_VIEW_HISTORY, pageLimit), new String[] { arg, arg }); + while (cursor.moveToNext()) { + ViewHistory item = new ViewHistory(); + int cursorIndex = 0; + item.setUri(LbryUri.tryParse(cursor.getString(cursorIndex++))); + item.setClaimId(cursor.getString(cursorIndex++)); + item.setClaimName(cursor.getString(cursorIndex++)); + item.setCost(new BigDecimal(cursor.getDouble(cursorIndex++))); + item.setTitle(cursor.getString(cursorIndex++)); + item.setPublisherClaimId(cursor.getString(cursorIndex++)); + item.setPublisherName(cursor.getString(cursorIndex++)); + item.setPublisherTitle(cursor.getString(cursorIndex++)); + item.setThumbnailUrl(cursor.getString(cursorIndex++)); + item.setDevice(cursor.getString(cursorIndex++)); + item.setReleaseTime(cursor.getLong(cursorIndex++)); + try { + item.setTimestamp(new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).parse(cursor.getString(cursorIndex))); + } catch (ParseException ex) { + // invalid timestamp (which shouldn't happen). Skip this item + continue; + } + + history.add(item); + } + } finally { + Helper.closeCursor(cursor); + } + return history; + } + public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) { db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 }); } diff --git a/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java b/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java new file mode 100644 index 00000000..c93f950e --- /dev/null +++ b/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java @@ -0,0 +1,5 @@ +package io.lbry.browser.listener; + +public interface DownloadActionListener { + void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress); +} diff --git a/app/src/main/java/io/lbry/browser/model/Claim.java b/app/src/main/java/io/lbry/browser/model/Claim.java index 5aaaf21d..1df6f71c 100644 --- a/app/src/main/java/io/lbry/browser/model/Claim.java +++ b/app/src/main/java/io/lbry/browser/model/Claim.java @@ -82,6 +82,9 @@ public class Claim { private GenericMetadata value; private LbryFile file; // associated file if it exists + // device it was viewed on (for view history) + private String device; + public static Claim claimFromOutput(JSONObject item) { // we only need name, permanent_url, txid and nout Claim claim = new Claim(); @@ -93,6 +96,10 @@ public class Claim { return claim; } + public String getOutpoint() { + return String.format("%s:%d", txid, nout); + } + public boolean isFree() { if (!(value instanceof StreamMetadata)) { return true; @@ -246,6 +253,8 @@ public class Claim { claim.setName(viewHistory.getClaimName()); claim.setValueType(TYPE_STREAM); claim.setPermanentUrl(viewHistory.getUri().toString()); + claim.setDevice(viewHistory.getDevice()); + claim.setConfirmations(1); StreamMetadata value = new StreamMetadata(); value.setTitle(viewHistory.getTitle()); diff --git a/app/src/main/java/io/lbry/browser/model/LbryFile.java b/app/src/main/java/io/lbry/browser/model/LbryFile.java index b9df418d..ed9cb1fb 100644 --- a/app/src/main/java/io/lbry/browser/model/LbryFile.java +++ b/app/src/main/java/io/lbry/browser/model/LbryFile.java @@ -9,9 +9,12 @@ import org.json.JSONObject; import java.lang.reflect.Type; +import io.lbry.browser.utils.LbryUri; import lombok.Data; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class LbryFile { private Claim.StreamMetadata metadata; private long addedOn; @@ -20,6 +23,7 @@ public class LbryFile { private int blobsRemaining; private String channelClaimId; private String channelName; + @EqualsAndHashCode.Include private String claimId; private String claimName; private boolean completed; @@ -39,16 +43,50 @@ public class LbryFile { private String streamName; private String streamingUrl; private String suggestedFileName; + private long timestamp; private long totalBytes; private long totalBytesLowerBound; private String txid; private long writtenBytes; + private Claim generatedClaim; + + public Claim getClaim() { + if (generatedClaim != null) { + return generatedClaim; + } + + generatedClaim = new Claim(); + generatedClaim.setValueType(Claim.TYPE_STREAM); + generatedClaim.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString()); + generatedClaim.setClaimId(claimId); + generatedClaim.setName(claimName); + generatedClaim.setValue(metadata); + generatedClaim.setConfirmations(1); + generatedClaim.setTxid(txid); + generatedClaim.setNout(nout); + generatedClaim.setFile(this); + + if (channelClaimId != null) { + Claim signingChannel = new Claim(); + signingChannel.setClaimId(channelClaimId); + signingChannel.setName(channelName); + signingChannel.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString()); + generatedClaim.setSigningChannel(signingChannel); + } + + return generatedClaim; + } + public static LbryFile fromJSONObject(JSONObject fileObject) { String fileJson = fileObject.toString(); Type type = new TypeToken(){}.getType(); Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); LbryFile file = gson.fromJson(fileJson, type); + + if (file.getMetadata() != null && file.getMetadata().getReleaseTime() == 0) { + file.getMetadata().setReleaseTime(file.getTimestamp()); + } return file; } } diff --git a/app/src/main/java/io/lbry/browser/model/ViewHistory.java b/app/src/main/java/io/lbry/browser/model/ViewHistory.java index 6bcb75dd..0940cc3a 100644 --- a/app/src/main/java/io/lbry/browser/model/ViewHistory.java +++ b/app/src/main/java/io/lbry/browser/model/ViewHistory.java @@ -5,6 +5,7 @@ import java.util.Date; import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.utils.LbryUri; +import io.lbry.browser.utils.Lbryio; import lombok.Data; @Data @@ -39,9 +40,12 @@ public class ViewHistory { Claim.StreamMetadata value = (Claim.StreamMetadata) metadata; history.setReleaseTime(value.getReleaseTime()); if (value.getFee() != null) { - history.setCost(new BigDecimal(value.getFee().getAmount())); + history.setCost(claim.getActualCost(Lbryio.LBCUSDRate)); } } + if (history.getReleaseTime() == 0) { + history.setReleaseTime(claim.getTimestamp()); + } Claim signingChannel = claim.getSigningChannel(); if (signingChannel != null) { diff --git a/app/src/main/java/io/lbry/browser/tasks/file/FileListTask.java b/app/src/main/java/io/lbry/browser/tasks/file/FileListTask.java index 58ef1435..0b442d77 100644 --- a/app/src/main/java/io/lbry/browser/tasks/file/FileListTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/file/FileListTask.java @@ -12,12 +12,18 @@ import io.lbry.browser.utils.Lbry; public class FileListTask extends AsyncTask> { private String claimId; + private boolean downloads; + private int page; + private int pageSize; private FileListResultHandler handler; private View progressView; private ApiCallException error; - public FileListTask(View progressView, FileListResultHandler handler) { + public FileListTask(int page, int pageSize, boolean downloads, View progressView, FileListResultHandler handler) { this(null, progressView, handler); + this.page = page; + this.pageSize = pageSize; + this.downloads = downloads; } public FileListTask(String claimId, View progressView, FileListResultHandler handler) { @@ -30,7 +36,7 @@ public class FileListTask extends AsyncTask> { } protected List doInBackground(Void... params) { try { - return Lbry.fileList(claimId); + return Lbry.fileList(claimId, downloads, page, pageSize); } catch (ApiCallException ex) { error = ex; return null; @@ -40,7 +46,7 @@ public class FileListTask extends AsyncTask> { Helper.setViewVisibility(progressView, View.GONE); if (handler != null) { if (files != null) { - handler.onSuccess(files); + handler.onSuccess(files, files.size() < pageSize); } else { handler.onError(error); } @@ -48,7 +54,7 @@ public class FileListTask extends AsyncTask> { } public interface FileListResultHandler { - void onSuccess(List files); + void onSuccess(List files, boolean hasReachedEnd); void onError(Exception error); } } diff --git a/app/src/main/java/io/lbry/browser/tasks/localdata/FetchViewHistoryTask.java b/app/src/main/java/io/lbry/browser/tasks/localdata/FetchViewHistoryTask.java new file mode 100644 index 00000000..fd0d4b21 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/localdata/FetchViewHistoryTask.java @@ -0,0 +1,46 @@ +package io.lbry.browser.tasks.localdata; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.os.AsyncTask; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import io.lbry.browser.data.DatabaseHelper; +import io.lbry.browser.model.UrlSuggestion; +import io.lbry.browser.model.ViewHistory; +import io.lbry.browser.utils.Helper; + +public class FetchViewHistoryTask extends AsyncTask> { + private DatabaseHelper dbHelper; + private FetchViewHistoryHandler handler; + private int pageSize; + private Date lastDate; + public FetchViewHistoryTask(Date lastDate, int pageSize, DatabaseHelper dbHelper, FetchViewHistoryHandler handler) { + this.lastDate = lastDate; + this.pageSize = pageSize; + this.dbHelper = dbHelper; + this.handler = handler; + } + protected List doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getReadableDatabase(); + return DatabaseHelper.getViewHistory( + lastDate == null ? null : new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(lastDate), pageSize, db); + } catch (SQLiteException ex) { + return new ArrayList<>(); + } + } + protected void onPostExecute(List history) { + if (handler != null) { + handler.onSuccess(history, history.size() < pageSize); + } + } + + public interface FetchViewHistoryHandler { + void onSuccess(List history, boolean hasReachedEnd); + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/localdata/SaveViewHistoryTask.java b/app/src/main/java/io/lbry/browser/tasks/localdata/SaveViewHistoryTask.java index 097d5a13..bb7e0f64 100644 --- a/app/src/main/java/io/lbry/browser/tasks/localdata/SaveViewHistoryTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/localdata/SaveViewHistoryTask.java @@ -1,4 +1,45 @@ package io.lbry.browser.tasks.localdata; -public class SaveViewHistoryTask { +import android.database.sqlite.SQLiteDatabase; +import android.os.AsyncTask; + +import io.lbry.browser.data.DatabaseHelper; +import io.lbry.browser.model.ViewHistory; + +public class SaveViewHistoryTask extends AsyncTask { + private DatabaseHelper dbHelper; + private ViewHistory history; + private SaveViewHistoryHandler handler; + private Exception error; + + public SaveViewHistoryTask(ViewHistory history, DatabaseHelper dbHelper, SaveViewHistoryHandler handler) { + this.history = history; + this.dbHelper = dbHelper; + this.handler = handler; + } + protected Boolean doInBackground(Void... params) { + try { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + DatabaseHelper.createOrUpdateViewHistoryItem(history, db); + } catch (Exception ex) { + error = ex; + return false; + } + + return true; + } + protected void onPostExecute(Boolean result) { + if (handler != null) { + if (result) { + handler.onSuccess(history); + } else { + handler.onError(error); + } + } + } + + public interface SaveViewHistoryHandler { + void onSuccess(ViewHistory item); + void onError(Exception error); + } } diff --git a/app/src/main/java/io/lbry/browser/ui/allcontent/AllContentFragment.java b/app/src/main/java/io/lbry/browser/ui/allcontent/AllContentFragment.java index 006f9475..db11452e 100644 --- a/app/src/main/java/io/lbry/browser/ui/allcontent/AllContentFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/allcontent/AllContentFragment.java @@ -15,8 +15,12 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.exoplayer2.offline.Download; import com.google.android.material.snackbar.Snackbar; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,8 +34,10 @@ import io.lbry.browser.dialog.ContentFromDialogFragment; import io.lbry.browser.dialog.ContentScopeDialogFragment; import io.lbry.browser.dialog.ContentSortDialogFragment; import io.lbry.browser.dialog.CustomizeTagsDialogFragment; +import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.listener.TagListener; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.model.Tag; import io.lbry.browser.tasks.claim.ClaimSearchTask; import io.lbry.browser.tasks.FollowUnfollowTagTask; @@ -43,7 +49,7 @@ 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 -public class AllContentFragment extends BaseFragment implements SharedPreferences.OnSharedPreferenceChangeListener { +public class AllContentFragment extends BaseFragment implements DownloadActionListener, SharedPreferences.OnSharedPreferenceChangeListener { @Getter private boolean singleTagView; @@ -374,6 +380,7 @@ public class AllContentFragment extends BaseFragment implements SharedPreference } else { LbryAnalytics.setCurrentScreen(activity, "All Content", "AllContent"); } + activity.addDownloadActionListener(this); } PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); @@ -384,7 +391,11 @@ public class AllContentFragment extends BaseFragment implements SharedPreference } public void onPause() { - PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); + Context context = getContext(); + if (context != null) { + ((MainActivity) context).removeDownloadActionListener(this); + } + PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @@ -491,4 +502,24 @@ public class AllContentFragment extends BaseFragment implements SharedPreference fetchClaimSearchContent(true); } } + + public void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (contentListAdapter != null) { + contentListAdapter.clearFileForClaimOrUrl(outpoint, uri); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (contentListAdapter != null) { + contentListAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri); + } + } catch (JSONException ex) { + // invalid file info for download + } + } } diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelContentFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelContentFragment.java index c9213510..4cd7d997 100644 --- a/app/src/main/java/io/lbry/browser/ui/channel/ChannelContentFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelContentFragment.java @@ -16,6 +16,9 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -27,14 +30,16 @@ import io.lbry.browser.R; import io.lbry.browser.adapter.ClaimListAdapter; import io.lbry.browser.dialog.ContentFromDialogFragment; import io.lbry.browser.dialog.ContentSortDialogFragment; +import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.tasks.claim.ClaimSearchTask; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Lbry; import io.lbry.browser.utils.Predefined; import lombok.Setter; -public class ChannelContentFragment extends Fragment implements SharedPreferences.OnSharedPreferenceChangeListener { +public class ChannelContentFragment extends Fragment implements DownloadActionListener, SharedPreferences.OnSharedPreferenceChangeListener { @Setter private String channelId; @@ -189,12 +194,20 @@ public class ChannelContentFragment extends Fragment implements SharedPreference public void onResume() { super.onResume(); - PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + Context context = getContext(); + if (context != null) { + ((MainActivity) context).addDownloadActionListener(this); + } + PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this); fetchClaimSearchContent(); } public void onPause() { - PreferenceManager.getDefaultSharedPreferences(getContext()).registerOnSharedPreferenceChangeListener(this); + Context context = getContext(); + if (context != null) { + ((MainActivity) context).removeDownloadActionListener(this); + } + PreferenceManager.getDefaultSharedPreferences(context).registerOnSharedPreferenceChangeListener(this); super.onPause(); } @@ -300,4 +313,24 @@ public class ChannelContentFragment extends Fragment implements SharedPreference fetchClaimSearchContent(true); } } + + public void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (contentListAdapter != null) { + contentListAdapter.clearFileForClaimOrUrl(outpoint, uri); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (contentListAdapter != null) { + contentListAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri); + } + } catch (JSONException ex) { + // invalid file info for download + } + } } diff --git a/app/src/main/java/io/lbry/browser/ui/following/FollowingFragment.java b/app/src/main/java/io/lbry/browser/ui/following/FollowingFragment.java index ac7c9a64..67e74430 100644 --- a/app/src/main/java/io/lbry/browser/ui/following/FollowingFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/following/FollowingFragment.java @@ -20,6 +20,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; import com.google.android.material.snackbar.Snackbar; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -35,7 +38,9 @@ import io.lbry.browser.dialog.ContentFromDialogFragment; import io.lbry.browser.dialog.ContentSortDialogFragment; import io.lbry.browser.dialog.DiscoverDialogFragment; import io.lbry.browser.exceptions.LbryUriException; +import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.tasks.lbryinc.ChannelSubscribeTask; import io.lbry.browser.tasks.claim.ClaimListResultHandler; @@ -53,7 +58,9 @@ import io.lbry.browser.utils.Predefined; public class FollowingFragment extends BaseFragment implements FetchSubscriptionsTask.FetchSubscriptionsHandler, - ChannelItemSelectionListener, SharedPreferences.OnSharedPreferenceChangeListener { + ChannelItemSelectionListener, + DownloadActionListener, + SharedPreferences.OnSharedPreferenceChangeListener { public static boolean resetClaimSearchContent; private static final int SUGGESTED_PAGE_SIZE = 45; @@ -334,6 +341,7 @@ public class FollowingFragment extends BaseFragment implements if (context instanceof MainActivity) { MainActivity activity = (MainActivity) context; LbryAnalytics.setCurrentScreen(activity, "Subscriptions", "Subscriptions"); + activity.addDownloadActionListener(this); } // check if subscriptions exist @@ -351,7 +359,11 @@ public class FollowingFragment extends BaseFragment implements } } public void onPause() { - PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).removeDownloadActionListener(this); + } + PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } public void fetchLoadedSubscriptions() { @@ -767,4 +779,24 @@ public class FollowingFragment extends BaseFragment implements fetchClaimSearchContent(true); } } + + public void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (contentListAdapter != null) { + contentListAdapter.clearFileForClaimOrUrl(outpoint, uri); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (contentListAdapter != null) { + contentListAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri); + } + } catch (JSONException ex) { + // invalid file info for download + } + } } diff --git a/app/src/main/java/io/lbry/browser/ui/library/LibraryFragment.java b/app/src/main/java/io/lbry/browser/ui/library/LibraryFragment.java new file mode 100644 index 00000000..ec1d11b2 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/ui/library/LibraryFragment.java @@ -0,0 +1,423 @@ +package io.lbry.browser.ui.library; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import io.lbry.browser.MainActivity; +import io.lbry.browser.R; +import io.lbry.browser.adapter.ClaimListAdapter; +import io.lbry.browser.data.DatabaseHelper; +import io.lbry.browser.listener.DownloadActionListener; +import io.lbry.browser.listener.SdkStatusListener; +import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; +import io.lbry.browser.model.ViewHistory; +import io.lbry.browser.tasks.file.FileListTask; +import io.lbry.browser.tasks.localdata.FetchViewHistoryTask; +import io.lbry.browser.ui.BaseFragment; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; +import io.lbry.browser.utils.LbryAnalytics; + +public class LibraryFragment extends BaseFragment implements DownloadActionListener, SdkStatusListener { + + private static final int FILTER_DOWNLOADS = 1; + private static final int FILTER_HISTORY = 2; + private static final int PAGE_SIZE = 50; + + private int currentFilter; + private List currentFiles; + private View layoutSdkInitializing; + private RecyclerView contentList; + private ClaimListAdapter contentListAdapter; + private ProgressBar listLoading; + private TextView linkFilterDownloads; + private TextView linkFilterHistory; + private View layoutListEmpty; + private TextView textListEmpty; + private int currentPage; + private Date lastDate; + private boolean listReachedEnd; + + private CardView cardStats; + private TextView linkStats; + private TextView linkHide; + private View viewStatsDistribution; + private View viewVideoStatsBar; + private View viewAudioStatsBar; + private View viewImageStatsBar; + private View viewOtherStatsBar; + private TextView textStatsTotalSize; + private TextView textStatsTotalSizeUnits; + private TextView textStatsVideoSize; + private TextView textStatsAudioSize; + private TextView textStatsImageSize; + private TextView textStatsOtherSize; + private View legendVideo; + private View legendAudio; + private View legendImage; + private View legendOther; + + private long totalBytes; + private long totalVideoBytes; + private long totalAudioBytes; + private long totalImageBytes; + private long totalOtherBytes; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_library, container, false); + + layoutSdkInitializing = root.findViewById(R.id.container_library_sdk_initializing); + + LinearLayoutManager llm = new LinearLayoutManager(getContext()); + contentList = root.findViewById(R.id.library_list); + contentList.setLayoutManager(llm); + + listLoading = root.findViewById(R.id.library_list_loading); + linkFilterDownloads = root.findViewById(R.id.library_filter_link_downloads); + linkFilterHistory = root.findViewById(R.id.library_filter_link_history); + + layoutListEmpty = root.findViewById(R.id.library_empty_container); + textListEmpty = root.findViewById(R.id.library_list_empty_text); + + currentFilter = FILTER_DOWNLOADS; + linkFilterDownloads.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDownloads(); + } + }); + linkFilterHistory.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showHistory(); + } + }); + + // stats + linkStats = root.findViewById(R.id.library_show_stats); + linkHide = root.findViewById(R.id.library_hide_stats); + cardStats = root.findViewById(R.id.library_storage_stats_card); + viewStatsDistribution = root.findViewById(R.id.library_storage_stat_distribution); + viewVideoStatsBar = root.findViewById(R.id.library_storage_stat_video_bar); + viewAudioStatsBar = root.findViewById(R.id.library_storage_stat_audio_bar); + viewImageStatsBar = root.findViewById(R.id.library_storage_stat_image_bar); + viewOtherStatsBar = root.findViewById(R.id.library_storage_stat_other_bar); + textStatsTotalSize = root.findViewById(R.id.library_storage_stat_used); + textStatsTotalSizeUnits = root.findViewById(R.id.library_storage_stat_unit); + textStatsVideoSize = root.findViewById(R.id.library_storage_stat_video_size); + textStatsAudioSize = root.findViewById(R.id.library_storage_stat_audio_size); + textStatsImageSize = root.findViewById(R.id.library_storage_stat_image_size); + textStatsOtherSize = root.findViewById(R.id.library_storage_stat_other_size); + legendVideo = root.findViewById(R.id.library_storage_legend_video); + legendAudio = root.findViewById(R.id.library_storage_legend_audio); + legendImage = root.findViewById(R.id.library_storage_legend_image); + legendOther = root.findViewById(R.id.library_storage_legend_other); + + linkStats.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + updateStats(); + cardStats.setVisibility(View.VISIBLE); + checkStatsLink(); + } + }); + linkHide.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + cardStats.setVisibility(View.GONE); + checkStatsLink(); + } + }); + + return root; + } + + public void onResume() { + super.onResume(); + + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + LbryAnalytics.setCurrentScreen(activity, "Library", "Library"); + activity.addDownloadActionListener(this); + } + + layoutSdkInitializing.setVisibility( + !Lbry.SDK_READY && currentFilter == FILTER_DOWNLOADS ? View.VISIBLE : View.GONE); + if (!Lbry.SDK_READY) { + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.addSdkStatusListener(this); + } + } else { + onSdkReady(); + } + } + + public void onPause() { + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.removeSdkStatusListener(this); + activity.removeDownloadActionListener(this); + } + super.onPause(); + } + + public void onSdkReady() { + layoutSdkInitializing.setVisibility(View.GONE); + if (currentFilter == FILTER_DOWNLOADS) { + showDownloads(); + } else if (currentFilter == FILTER_HISTORY) { + showHistory(); + } + } + + private void showDownloads() { + currentFilter = FILTER_DOWNLOADS; + linkFilterDownloads.setTypeface(null, Typeface.BOLD); + linkFilterHistory.setTypeface(null, Typeface.NORMAL); + if (contentListAdapter != null) { + contentListAdapter.clearItems(); + contentListAdapter.setCanEnterSelectionMode(true); + } + + checkStatsLink(); + layoutSdkInitializing.setVisibility(Lbry.SDK_READY ? View.GONE : View.VISIBLE); + currentPage = 1; + if (Lbry.SDK_READY) { + fetchDownloads(); + } + } + + private void showHistory() { + currentFilter = FILTER_HISTORY; + linkFilterDownloads.setTypeface(null, Typeface.NORMAL); + linkFilterHistory.setTypeface(null, Typeface.BOLD); + if (contentListAdapter != null) { + contentListAdapter.clearItems(); + contentListAdapter.setCanEnterSelectionMode(false); + } + + cardStats.setVisibility(View.GONE); + checkStatsLink(); + + layoutSdkInitializing.setVisibility(View.GONE); + lastDate = null; + fetchHistory(); + } + + private void initContentListAdapter(List claims) { + contentListAdapter = new ClaimListAdapter(claims, getContext()); + contentListAdapter.setCanEnterSelectionMode(true); + contentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() { + @Override + public void onClaimClicked(Claim claim) { + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) getContext(); + if (claim.getName().startsWith("@")) { + activity.openChannelUrl(claim.getPermanentUrl()); + } else { + MainActivity.openFileUrl(claim.getPermanentUrl(), context); + } + } + } + }); + } + + private void fetchDownloads() { + Helper.setViewVisibility(linkStats, View.GONE); + Helper.setViewVisibility(layoutListEmpty, View.GONE); + FileListTask task = new FileListTask(currentPage, PAGE_SIZE, true, listLoading, new FileListTask.FileListResultHandler() { + @Override + public void onSuccess(List files, boolean hasReachedEnd) { + listReachedEnd = hasReachedEnd; + List filteredFiles = Helper.filterDownloads(files); + List claims = Helper.claimsFromFiles(filteredFiles); + + addFiles(filteredFiles); + updateStats(); + checkStatsLink(); + + if (contentListAdapter == null) { + initContentListAdapter(claims); + } else { + contentListAdapter.addItems(claims); + } + contentList.setAdapter(contentListAdapter); + checkListEmpty(); + } + + @Override + public void onError(Exception error) { + // pass + checkStatsLink(); + checkListEmpty(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void fetchHistory() { + Helper.setViewVisibility(layoutListEmpty, View.GONE); + DatabaseHelper dbHelper = DatabaseHelper.getInstance(); + if (dbHelper != null) { + FetchViewHistoryTask task = new FetchViewHistoryTask(lastDate, PAGE_SIZE, dbHelper, new FetchViewHistoryTask.FetchViewHistoryHandler() { + @Override + public void onSuccess(List history, boolean hasReachedEnd) { + listReachedEnd = hasReachedEnd; + List claims = Helper.claimsFromViewHistory(history); + if (contentListAdapter == null) { + initContentListAdapter(claims); + } else { + contentListAdapter.addItems(claims); + } + contentList.setAdapter(contentListAdapter); + checkListEmpty(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + checkListEmpty(); + } + } + + public void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (contentListAdapter != null) { + contentListAdapter.clearFileForClaimOrUrl(outpoint, uri, currentFilter == FILTER_DOWNLOADS); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (contentListAdapter != null) { + contentListAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri, true); + } + } catch (JSONException ex) { + // invalid file info for download + } + } + + private void checkListEmpty() { + layoutListEmpty.setVisibility(contentListAdapter == null || contentListAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + textListEmpty.setText(currentFilter == FILTER_DOWNLOADS ? R.string.library_no_downloads : R.string.library_no_history); + } + + private void addFiles(List files) { + if (currentFiles == null) { + currentFiles = new ArrayList<>(); + } + for (LbryFile file : files) { + if (!currentFiles.contains(file)) { + currentFiles.add(file); + } + } + } + + private void updateStats() { + totalBytes = 0; + totalVideoBytes = 0; + totalAudioBytes = 0; + totalImageBytes = 0; + totalOtherBytes = 0; + if (currentFiles != null) { + for (LbryFile file : currentFiles) { + long writtenBytes = file.getWrittenBytes(); + String mime = file.getMimeType(); + if (mime != null) { + if (mime.startsWith("video/")) { + totalVideoBytes += writtenBytes; + } else if (mime.startsWith("audio/")) { + totalAudioBytes += writtenBytes; + } else if (mime.startsWith("image/")) { + totalImageBytes += writtenBytes; + } else { + totalOtherBytes += writtenBytes; + } + } + + totalBytes += writtenBytes; + } + } + + renderStats(); + } + + private void renderStats() { + String[] totalSizeParts = Helper.formatBytesParts(totalBytes, false); + textStatsTotalSize.setText(totalSizeParts[0]); + textStatsTotalSizeUnits.setText(totalSizeParts[1]); + + viewStatsDistribution.setVisibility(totalBytes > 0 ? View.VISIBLE : View.GONE); + + int percentVideo = normalizePercent((double) totalVideoBytes / (double) totalBytes * 100.0); + legendVideo.setVisibility(totalVideoBytes > 0 ? View.VISIBLE : View.GONE); + textStatsVideoSize.setText(Helper.formatBytes(totalVideoBytes, false)); + applyLayoutWeight(viewVideoStatsBar, percentVideo); + + int percentAudio = normalizePercent((double) totalAudioBytes / (double) totalBytes * 100.0); + legendAudio.setVisibility(totalAudioBytes > 0 ? View.VISIBLE : View.GONE); + textStatsAudioSize.setText(Helper.formatBytes(totalAudioBytes, false)); + applyLayoutWeight(viewAudioStatsBar, percentAudio); + + int percentImage = normalizePercent((double) totalImageBytes / (double) totalBytes * 100.0); + legendImage.setVisibility(totalImageBytes > 0 ? View.VISIBLE : View.GONE); + textStatsImageSize.setText(Helper.formatBytes(totalImageBytes, false)); + applyLayoutWeight(viewImageStatsBar, percentImage); + + int percentOther = normalizePercent((double) totalOtherBytes / (double) totalBytes * 100.0); + legendOther.setVisibility(totalOtherBytes > 0 ? View.VISIBLE : View.GONE); + textStatsOtherSize.setText(Helper.formatBytes(totalOtherBytes, false)); + applyLayoutWeight(viewOtherStatsBar, percentOther); + + // We have to get to 100 (or adjust the container accordingly) + int totalPercent = percentVideo + percentAudio + percentImage + percentOther; + ((LinearLayout) viewStatsDistribution).setWeightSum(totalPercent); + } + + private void applyLayoutWeight(View view, int weight) { + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) view.getLayoutParams(); + params.weight = weight; + } + + private static int normalizePercent(double value) { + if (value > 0 && value < 1) { + return 1; + } + return Double.valueOf(Math.floor(value)).intValue(); + } + + private void checkStatsLink() { + linkStats.setVisibility(cardStats.getVisibility() == View.VISIBLE || + listLoading.getVisibility() == View.VISIBLE || + currentFilter == FILTER_HISTORY ? + View.GONE : View.VISIBLE); + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/search/SearchFragment.java b/app/src/main/java/io/lbry/browser/ui/search/SearchFragment.java index 0b148416..bcc43bfa 100644 --- a/app/src/main/java/io/lbry/browser/ui/search/SearchFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/search/SearchFragment.java @@ -15,13 +15,20 @@ import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.exoplayer2.offline.Download; + +import org.json.JSONException; +import org.json.JSONObject; + import java.util.List; import io.lbry.browser.MainActivity; import io.lbry.browser.R; import io.lbry.browser.adapter.ClaimListAdapter; +import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.model.Claim; import io.lbry.browser.model.ClaimCacheKey; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.tasks.claim.ClaimListResultHandler; import io.lbry.browser.tasks.claim.ClaimSearchTask; import io.lbry.browser.tasks.LighthouseSearchTask; @@ -34,10 +41,10 @@ import io.lbry.browser.utils.LbryUri; import lombok.Setter; public class SearchFragment extends BaseFragment implements - ClaimListAdapter.ClaimListItemListener, SharedPreferences.OnSharedPreferenceChangeListener { - private ClaimListAdapter resultListAdapter; + ClaimListAdapter.ClaimListItemListener, DownloadActionListener, SharedPreferences.OnSharedPreferenceChangeListener { private static final int PAGE_SIZE = 25; + private ClaimListAdapter resultListAdapter; private ProgressBar loadingView; private RecyclerView resultList; private TextView noQueryView; @@ -95,6 +102,7 @@ public class SearchFragment extends BaseFragment implements if (context instanceof MainActivity) { MainActivity activity = (MainActivity) context; LbryAnalytics.setCurrentScreen(activity, "Search", "Search"); + activity.addDownloadActionListener(this); } if (!Helper.isNullOrEmpty(currentQuery)) { logSearch(currentQuery); @@ -106,7 +114,11 @@ public class SearchFragment extends BaseFragment implements } public void onPause() { - PreferenceManager.getDefaultSharedPreferences(getContext()).unregisterOnSharedPreferenceChangeListener(this); + Context context = getContext(); + if (context != null) { + ((MainActivity) context).removeDownloadActionListener(this); + } + PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @@ -262,4 +274,24 @@ public class SearchFragment extends BaseFragment implements search(currentQuery, currentFrom); } } + + public void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress) { + if ("abort".equals(downloadAction)) { + if (resultListAdapter != null) { + resultListAdapter.clearFileForClaimOrUrl(outpoint, uri); + } + return; + } + + try { + JSONObject fileInfo = new JSONObject(fileInfoJson); + LbryFile claimFile = LbryFile.fromJSONObject(fileInfo); + String claimId = claimFile.getClaimId(); + if (resultListAdapter != null) { + resultListAdapter.updateFileForClaimByIdOrUrl(claimFile, claimId, uri); + } + } catch (JSONException ex) { + // invalid file info for download + } + } } diff --git a/app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java b/app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java index 3475378b..5be6cb42 100644 --- a/app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/wallet/WalletFragment.java @@ -401,11 +401,11 @@ public class WalletFragment extends BaseFragment implements SdkStatusListener, W LbryAnalytics.setCurrentScreen(activity, "Wallet", "Wallet"); } + Helper.setViewVisibility(layoutAccountRecommended, hasSkippedAccount() || Lbryio.isSignedIn() ? View.GONE : View.VISIBLE); if (!Lbry.SDK_READY) { if (context instanceof MainActivity) { MainActivity activity = (MainActivity) context; activity.addSdkStatusListener(this); - activity.addWalletBalanceListener(this); } checkReceiveAddress(); @@ -441,7 +441,9 @@ public class WalletFragment extends BaseFragment implements SdkStatusListener, W public void onSdkReady() { Context context = getContext(); if (context instanceof MainActivity) { - ((MainActivity) context).removeSdkStatusListener(this); + MainActivity activity = (MainActivity) context; + activity.syncWalletAndLoadPreferences(); + activity.addWalletBalanceListener(this); } // update view diff --git a/app/src/main/java/io/lbry/browser/utils/Helper.java b/app/src/main/java/io/lbry/browser/utils/Helper.java index 4a7165d1..31d61220 100644 --- a/app/src/main/java/io/lbry/browser/utils/Helper.java +++ b/app/src/main/java/io/lbry/browser/utils/Helper.java @@ -19,6 +19,7 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.method.LinkMovementMethod; import android.view.View; +import android.widget.ProgressBar; import android.widget.TextView; import androidx.core.content.ContextCompat; @@ -45,9 +46,12 @@ import io.lbry.browser.data.DatabaseHelper; import io.lbry.browser.dialog.ContentFromDialogFragment; import io.lbry.browser.dialog.ContentSortDialogFragment; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.LbryFile; import io.lbry.browser.model.Tag; import io.lbry.browser.model.UrlSuggestion; +import io.lbry.browser.model.ViewHistory; import io.lbry.browser.tasks.localdata.SaveUrlHistoryTask; +import io.lbry.browser.tasks.localdata.SaveViewHistoryTask; import okhttp3.MediaType; public final class Helper { @@ -61,6 +65,7 @@ public final class Helper { public static final int CONTENT_PAGE_SIZE = 25; public static final double MIN_DEPOSIT = 0.05; public static final String LBC_CURRENCY_FORMAT_PATTERN = "#,###.##"; + public static final String FILE_SIZE_FORMAT_PATTERN = "#,###.#"; public static final DecimalFormat LBC_CURRENCY_FORMAT = new DecimalFormat(LBC_CURRENCY_FORMAT_PATTERN); public static final DecimalFormat USD_CURRENCY_FORMAT = new DecimalFormat("#,##0.00"); public static final String EXPLORER_TX_PREFIX = "https://explorer.lbry.com/tx"; @@ -121,6 +126,12 @@ public final class Helper { } } + public static void setViewProgress(ProgressBar progressBar, int progress) { + if (progressBar != null) { + progressBar.setProgress(progress); + } + } + public static void setViewText(TextView view, int stringResourceId) { if (view != null) { view.setText(stringResourceId); @@ -183,6 +194,43 @@ public final class Helper { } return String.format("%d:%02d", minutes, seconds); } + public static String[] formatBytesParts(long bytes, boolean showTB) { + DecimalFormat formatter = new DecimalFormat(FILE_SIZE_FORMAT_PATTERN); + if (bytes < 1048576) { + // less than 1MB + return new String[] { formatter.format(bytes / 1024.0), "KB" }; + } + if (bytes < 1073741824) { + // less than 1GB + return new String[] { formatter.format(bytes / (1024.0 * 1024.0)), "MB" }; + } + if (showTB) { + if (bytes < (1073741824L * 1024L)) { + return new String[] { formatter.format(bytes / (1024.0 * 1024.0 * 1024.0)), "GB" }; + } + return new String[] { formatter.format(bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0)), "TB" }; + } + return new String[] { formatter.format(bytes / (1024.0 * 1024.0 * 1024.0)), "GB" }; + } + + public static String formatBytes(long bytes, boolean showTB) { + DecimalFormat formatter = new DecimalFormat(FILE_SIZE_FORMAT_PATTERN); + if (bytes < 1048576) { + // less than 1MB + return String.format("%sKB", formatter.format(bytes / 1024.0)); + } + if (bytes < 1073741824) { + // less than 1GB + return String.format("%sMB", formatter.format(bytes / (1024.0 * 1024.0))); + } + if (showTB) { + if (bytes < (1073741824L * 1024L)) { + return String.format("%sGB", formatter.format(bytes / (1024.0 * 1024.0 * 1024.0))); + } + return String.format("%sTB", formatter.format(bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0))); + } + return String.format("%sGB", formatter.format(bytes / (1024.0 * 1024.0 * 1024.0))); + } public static JSONObject getJSONObject(String name, JSONObject object) { try { @@ -571,6 +619,33 @@ public final class Helper { return filtered; } + public static List filterDownloads(List files) { + List filtered = new ArrayList<>(); + for (int i = 0; i < files.size(); i++) { + LbryFile file = files.get(i); + if (!Helper.isNullOrEmpty(file.getDownloadPath())) { + filtered.add(file); + } + } + return filtered; + } + + public static List claimsFromFiles(List files) { + List claims = new ArrayList<>(); + for (LbryFile file : files) { + claims.add(file.getClaim()); + } + return claims; + } + + public static List claimsFromViewHistory(List history) { + List claims = new ArrayList<>(); + for (ViewHistory item : history) { + claims.add(Claim.fromViewHistory(item)); + } + return claims; + } + public static void saveUrlHistory(String url, String title, int type) { DatabaseHelper dbHelper = DatabaseHelper.getInstance(); if (dbHelper != null) { @@ -581,4 +656,12 @@ public final class Helper { new SaveUrlHistoryTask(suggestion, dbHelper, null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } + + public static void saveViewHistory(String url, Claim claim) { + DatabaseHelper dbHelper = DatabaseHelper.getInstance(); + if (dbHelper != null) { + ViewHistory viewHistory = ViewHistory.fromClaimWithUrlAndDeviceName(claim, url, getDeviceName()); + new SaveViewHistoryTask(viewHistory, dbHelper, null).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + } } diff --git a/app/src/main/java/io/lbry/browser/utils/Lbry.java b/app/src/main/java/io/lbry/browser/utils/Lbry.java index 0982ff1d..63bab37f 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbry.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbry.java @@ -307,12 +307,22 @@ public final class Lbry { return file; } - public static List fileList(String claimId) throws ApiCallException { + public static List fileList(String claimId, boolean downloads, int page, int pageSize) throws ApiCallException { List files = new ArrayList<>(); Map params = new HashMap<>(); if (!Helper.isNullOrEmpty(claimId)) { params.put("claim_id", claimId); } + /*if (downloads) { + params.put("file_name", null); + params.put("comparison", "ne"); + }*/ + if (page > 0) { + params.put("page", page); + } + if (pageSize > 0) { + params.put("page_size", pageSize); + } try { JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_FILE_LIST, params)); JSONArray items = result.getJSONArray("items"); diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index 5bfebb94..52ba2d76 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -70,6 +70,7 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml new file mode 100644 index 00000000..c719f7df --- /dev/null +++ b/app/src/main/res/layout/fragment_library.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_stream.xml b/app/src/main/res/layout/list_item_stream.xml index 00855410..d3a612e4 100644 --- a/app/src/main/res/layout/list_item_stream.xml +++ b/app/src/main/res/layout/list_item_stream.xml @@ -168,24 +168,56 @@ android:textColor="@color/lbryGreen" android:fontFamily="@font/inter" android:textSize="12sp" /> - - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 8d01062d..a58380f8 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -48,6 +48,7 @@ #3971DB #2196f3 + @color/nextLbryGreen #F6A637 #FF4A7D #26BCF7 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d47f47c1..2cf458cf 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -47,6 +47,7 @@ #3971DB #2196f3 + @color/nextLbryGreen #F6A637 #FF4A7D #26BCF7 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a73b2432..a6da6208 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,6 +93,16 @@ %1$s followers + + Oops! Something went wrong. + Loaded Installation ID. + Loaded local known and followed tags. + Loaded LBC/USD exchange rate. + User authenticated. + Installation registered. + Loaded subscriptions. + Resolved subscriptions. + Content & User interface Other @@ -363,6 +373,21 @@ Invite link copied. Invite sent to %1$s + + Downloads + History + You have not downloaded any content to this device. + You have not viewed any content on this device. + Hide + Stats + Video + Audio + Images + MB + KB + GB + 0MB + About LBRY Content Freedom