diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f3fdc2a..fa041963 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,7 +26,7 @@ android:supportsPictureInPicture="true" android:theme="@style/AppTheme.NoActionBar" android:launchMode="singleInstance" - android:windowSoftInputMode="adjustResize"> + android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/io/lbry/browser/FileViewActivity.java b/app/src/main/java/io/lbry/browser/FileViewActivity.java index c53237d0..881029b5 100644 --- a/app/src/main/java/io/lbry/browser/FileViewActivity.java +++ b/app/src/main/java/io/lbry/browser/FileViewActivity.java @@ -49,6 +49,7 @@ import io.lbry.browser.model.Claim; import io.lbry.browser.model.ClaimCacheKey; import io.lbry.browser.model.File; import io.lbry.browser.model.Tag; +import io.lbry.browser.tasks.ClaimListResultHandler; import io.lbry.browser.tasks.ClaimSearchTask; import io.lbry.browser.tasks.FileListTask; import io.lbry.browser.tasks.LighthouseSearchTask; @@ -266,7 +267,7 @@ public class FileViewActivity extends AppCompatActivity { private void resolveUrl(String url) { resolving = true; View loadingView = findViewById(R.id.file_view_loading_container); - ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, loadingView, new ResolveTask.ResolveResultHandler() { + ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, loadingView, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { if (claims.size() > 0) { diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index 37cd325b..a587a4ad 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -1,5 +1,6 @@ package io.lbry.browser; +import android.app.Activity; import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationManager; @@ -10,6 +11,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.TypedArray; import android.database.sqlite.SQLiteDatabase; @@ -38,6 +40,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; @@ -47,7 +50,6 @@ import androidx.core.view.GravityCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; -import androidx.navigation.ui.AppBarConfiguration; import androidx.preference.PreferenceManager; import androidx.drawerlayout.widget.DrawerLayout; import androidx.appcompat.app.AppCompatActivity; @@ -63,6 +65,7 @@ import java.io.BufferedReader; import java.io.FileInputStream; import java.io.InputStreamReader; import java.net.ConnectException; +import java.nio.channels.Channel; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -88,6 +91,7 @@ import io.lbry.browser.model.UrlSuggestion; import io.lbry.browser.model.WalletBalance; import io.lbry.browser.model.WalletSync; import io.lbry.browser.model.lbryinc.Subscription; +import io.lbry.browser.tasks.ClaimListResultHandler; import io.lbry.browser.tasks.LighthouseAutoCompleteTask; import io.lbry.browser.tasks.MergeSubscriptionsTask; import io.lbry.browser.tasks.ResolveTask; @@ -99,7 +103,9 @@ 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.ui.BaseFragment; +import io.lbry.browser.ui.channel.ChannelFormFragment; 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.search.SearchFragment; @@ -132,14 +138,23 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener fragmentClassNavIdMap.put(SettingsFragment.class, NavMenuItem.ID_ITEM_SETTINGS); fragmentClassNavIdMap.put(AllContentFragment.class, NavMenuItem.ID_ITEM_ALL_CONTENT); + fragmentClassNavIdMap.put(ChannelManagerFragment.class, NavMenuItem.ID_ITEM_CHANNELS); + + // Internal (sub-)pages fragmentClassNavIdMap.put(ChannelFragment.class, NavMenuItem.ID_ITEM_FOLLOWING); fragmentClassNavIdMap.put(SearchFragment.class, NavMenuItem.ID_ITEM_FOLLOWING); + + //fragmentClassNavIdMap.put(ChannelFormFragment.class, NavMenuItem.ID_ITEM_CHANNELS); } + + public static final int REQUEST_STORAGE_PERMISSION = 1001; public static final int REQUEST_SIMPLE_SIGN_IN = 2001; public static final int REQUEST_WALLET_SYNC_SIGN_IN = 2002; + public static final int REQUEST_FILE_PICKER = 5001; + // broadcast action names public static final String ACTION_SDK_READY = "io.lbry.browser.Broadcast.SdkReady"; public static final String ACTION_AUTH_TOKEN_GENERATED = "io.lbry.browser.Broadcast.AuthTokenGenerated"; @@ -205,24 +220,33 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener private boolean pendingFollowingReload; private final List supportedMenuItemIds = Arrays.asList( + // find content NavMenuItem.ID_ITEM_FOLLOWING, NavMenuItem.ID_ITEM_EDITORS_CHOICE, NavMenuItem.ID_ITEM_ALL_CONTENT, + + // your content + NavMenuItem.ID_ITEM_CHANNELS, + + // wallet NavMenuItem.ID_ITEM_WALLET, NavMenuItem.ID_ITEM_SETTINGS ); + public boolean isDarkMode() { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + return sp.getBoolean(PREFERENCE_KEY_DARK_MODE, false); + } + @Override protected void onCreate(Bundle savedInstanceState) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - boolean darkMode = sp.getBoolean(PREFERENCE_KEY_DARK_MODE, false); - AppCompatDelegate.setDefaultNightMode(darkMode ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); + AppCompatDelegate.setDefaultNightMode(isDarkMode() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO); initKeyStore(); loadAuthToken(); dbHelper = new DatabaseHelper(this); - if (!darkMode) { + if (!isDarkMode()) { getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); } @@ -367,6 +391,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener case NavMenuItem.ID_ITEM_ALL_CONTENT: openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT); break; + + case NavMenuItem.ID_ITEM_CHANNELS: + openFragment(ChannelManagerFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS); + break; + case NavMenuItem.ID_ITEM_WALLET: openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET); break; @@ -384,6 +413,14 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener openFragment(ChannelFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params); } + public void openChannelForm(Claim claim) { + Map params = new HashMap<>(); + if (claim != null) { + params.put("claim", claim); + } + openFragment(ChannelFormFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS, params); + } + public void openChannelUrl(String url) { Map params = new HashMap<>(); params.put("url", url); @@ -667,7 +704,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } private void resolveUrlSuggestions(List urls) { - ResolveTask task = new ResolveTask(urls, Lbry.LBRY_TV_CONNECTION_STRING, null, new ResolveTask.ResolveResultHandler() { + ResolveTask task = new ResolveTask(urls, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) { @@ -1181,23 +1218,50 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener registerReceiver(userActionsReceiver, intentFilter); } + public void showMessage(int stringResourceId) { + Snackbar.make(findViewById(R.id.content_main), stringResourceId, Snackbar.LENGTH_LONG).show(); + } + public void showMessage(String message) { + Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).show(); + } + @Override public void onBackPressed() { DrawerLayout drawer = findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { - // check fragment and nav history - FragmentManager manager = getSupportFragmentManager(); - int backCount = getSupportFragmentManager().getBackStackEntryCount(); - if (backCount > 0) { - // we can pop the stack - manager.popBackStack(); - setSelectedNavMenuItemForFragment(getCurrentFragment()); - } else if (!enterPIPMode()) { - // we're at the top of the stack - moveTaskToBack(true); - return; + boolean handled = false; + if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) { + clearWunderbarFocus(findViewById(R.id.wunderbar)); + handled = true; + } else { + ChannelFormFragment channelFormFragment = null; + for (Fragment fragment : openNavFragments.values()) { + if (fragment instanceof ChannelFormFragment) { + channelFormFragment = ((ChannelFormFragment) fragment); + break; + } + } + if (channelFormFragment != null && channelFormFragment.isSaveInProgress()) { + handled = true; + return; + } + } + + if (!handled) { + // check fragment and nav history + FragmentManager manager = getSupportFragmentManager(); + int backCount = getSupportFragmentManager().getBackStackEntryCount(); + if (backCount > 0) { + // we can pop the stack + manager.popBackStack(); + setSelectedNavMenuItemForFragment(getCurrentFragment()); + } else if (!enterPIPMode()) { + // we're at the top of the stack + moveTaskToBack(true); + return; + } } } } @@ -1215,22 +1279,72 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } @Override - protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == RESULT_OK) { - // user signed in - showSignedInUser(); - - if (requestCode == REQUEST_WALLET_SYNC_SIGN_IN) { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); - sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, true).apply(); - + public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { + switch (requestCode) { + case REQUEST_STORAGE_PERMISSION: + ChannelFormFragment channelFormFragment = null; + //PublishFormFragment publishFormFragment = null; for (Fragment fragment : openNavFragments.values()) { - if (fragment instanceof WalletFragment) { - ((WalletFragment) fragment).onWalletSyncEnabled(); + if (fragment instanceof ChannelFormFragment) { + channelFormFragment = ((ChannelFormFragment) fragment); + break; } } - scheduleWalletSyncTask(); + + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (channelFormFragment != null) { + channelFormFragment.onStoragePermissionGranted(); + } + } else { + if (channelFormFragment != null) { + channelFormFragment.onStoragePermissionRefused(); + } + } + break; + + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_FILE_PICKER) { + ChannelFormFragment channelFormFragment = null; + //PublishFormFragment publishFormFragment = null; + for (Fragment fragment : openNavFragments.values()) { + if (fragment instanceof ChannelFormFragment) { + channelFormFragment = ((ChannelFormFragment) fragment); + break; + } + } + + if (resultCode == RESULT_OK) { + Uri fileUri = data.getData(); + String filePath = Helper.getRealPathFromURI_API19(this, fileUri); + if (channelFormFragment != null) { + channelFormFragment.onFilePicked(filePath); + } + } else { + if (channelFormFragment != null) { + channelFormFragment.onFilePicked(null); + } + } + } else if (requestCode == REQUEST_SIMPLE_SIGN_IN || requestCode == REQUEST_WALLET_SYNC_SIGN_IN) { + if (resultCode == RESULT_OK) { + // user signed in + showSignedInUser(); + + 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()) { + if (fragment instanceof WalletFragment) { + ((WalletFragment) fragment).onWalletSyncEnabled(); + } + } + scheduleWalletSyncTask(); + } } } } @@ -1765,4 +1879,19 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener }); task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + + public static void requestPermission(String permission, int requestCode, String rationale, Context context, boolean forceRequest) { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { + if (!forceRequest && ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) { + Toast.makeText(context, rationale, Toast.LENGTH_LONG).show(); + } else { + ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode); + } + } + } + + public static boolean hasPermission(String permission, Context context) { + return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED); + } + } 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 8a03e95d..d9125c39 100644 --- a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java @@ -18,9 +18,11 @@ import java.util.ArrayList; import java.util.List; import io.lbry.browser.R; +import io.lbry.browser.listener.SelectionModeListener; import io.lbry.browser.model.Claim; import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.LbryUri; +import lombok.Getter; import lombok.Setter; public class ClaimListAdapter extends RecyclerView.Adapter { @@ -33,6 +35,11 @@ public class ClaimListAdapter extends RecyclerView.Adapter selectedItems; @Setter private ClaimListItemListener listener; + @Getter + @Setter + private boolean inSelectionMode; + @Setter + private SelectionModeListener selectionModeListener; public ClaimListAdapter(List items, Context context) { this.context = context; @@ -43,6 +50,9 @@ public class ClaimListAdapter extends RecyclerView.Adapter getSelectedItems() { return this.selectedItems; } + public int getSelectedCount() { + return selectedItems != null ? selectedItems.size() : 0; + } public void clearSelectedItems() { this.selectedItems.clear(); } @@ -78,6 +88,16 @@ public class ClaimListAdapter extends RecyclerView.Adapter claims) { + items = new ArrayList<>(claims); + notifyDataSetChanged(); + } + + public void removeItem(Claim claim) { + items.remove(claim); + selectedItems.remove(claim); + notifyDataSetChanged(); + } public static class ViewHolder extends RecyclerView.ViewHolder { protected View feeContainer; @@ -92,6 +112,7 @@ public class ClaimListAdapter extends RecyclerView.Adapter>() { - protected List doInBackground(Void... params) { - List tags = new ArrayList<>(); - if (Helper.isNullOrEmpty(filter)) { - Random random = new Random(); - if (suggestedTagsAdapter != null && !clearPrevious) { - tags = new ArrayList<>(suggestedTagsAdapter.getTags()); - } - while (tags.size() < limit) { - Tag randomTag = Lbry.knownTags.get(random.nextInt(Lbry.knownTags.size())); - if (!Lbry.followedTags.contains(randomTag) && (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(randomTag))) { - tags.add(randomTag); - } - } - } else { - Tag filterTag = new Tag(filter); - if (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(filterTag)) { - tags.add(new Tag(filter)); - } - for (int i = 0; i < Lbry.knownTags.size() && tags.size() < SUGGESTED_LIMIT - 1; i++) { - Tag knownTag = Lbry.knownTags.get(i); - if ((knownTag.getLowercaseName().startsWith(filter) || knownTag.getLowercaseName().matches(filter)) && - (!tags.contains(knownTag) && - !Lbry.followedTags.contains(knownTag) && (followedTagsAdapter == null || !followedTagsAdapter.getTags().contains(knownTag)))) { - tags.add(knownTag); - } - } - } - return tags; - } - protected void onPostExecute(List tags) { + UpdateSuggestedTagsTask task = new UpdateSuggestedTagsTask(filter, SUGGESTED_LIMIT, followedTagsAdapter, suggestedTagsAdapter, clearPrevious, new UpdateSuggestedTagsTask.KnownTagsHandler() { + @Override + public void onSuccess(List tags) { if (suggestedTagsAdapter == null) { suggestedTagsAdapter = new TagListAdapter(tags, getContext()); suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD); @@ -201,11 +174,7 @@ public class CustomizeTagsDialogFragment extends BottomSheetDialogFragment { } checkNoResults(); } - }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - public interface TagListener { - void onTagAdded(Tag tag); - void onTagRemoved(Tag tag); + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } diff --git a/app/src/main/java/io/lbry/browser/listener/SelectionModeListener.java b/app/src/main/java/io/lbry/browser/listener/SelectionModeListener.java new file mode 100644 index 00000000..01fa043d --- /dev/null +++ b/app/src/main/java/io/lbry/browser/listener/SelectionModeListener.java @@ -0,0 +1,7 @@ +package io.lbry.browser.listener; + +public interface SelectionModeListener { + void onEnterSelectionMode(); + void onExitSelectionMode(); + void onItemSelectionToggled(); +} diff --git a/app/src/main/java/io/lbry/browser/listener/TagListener.java b/app/src/main/java/io/lbry/browser/listener/TagListener.java new file mode 100644 index 00000000..4e5071bb --- /dev/null +++ b/app/src/main/java/io/lbry/browser/listener/TagListener.java @@ -0,0 +1,8 @@ +package io.lbry.browser.listener; + +import io.lbry.browser.model.Tag; + +public interface TagListener { + void onTagAdded(Tag tag); + void onTagRemoved(Tag tag); +} 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 a8f85379..444958b4 100644 --- a/app/src/main/java/io/lbry/browser/model/Claim.java +++ b/app/src/main/java/io/lbry/browser/model/Claim.java @@ -112,6 +112,14 @@ public class Claim { return (value != null) ? value.getDescription() : null; } + public String getWebsiteUrl() { + return (value instanceof ChannelMetadata) ? ((ChannelMetadata) value).getWebsiteUrl() : null; + } + + public String getEmail() { + return (value instanceof ChannelMetadata) ? ((ChannelMetadata) value).getEmail() : null; + } + public String getPublisherName() { if (signingChannel != null) { return signingChannel.getName(); diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/Subscription.java b/app/src/main/java/io/lbry/browser/model/lbryinc/Subscription.java index d15e262c..30f2412a 100644 --- a/app/src/main/java/io/lbry/browser/model/lbryinc/Subscription.java +++ b/app/src/main/java/io/lbry/browser/model/lbryinc/Subscription.java @@ -28,7 +28,7 @@ public class Subscription { } public boolean equals(Object o) { - return (o instanceof Subscription) && url.equalsIgnoreCase(((Subscription) o).getUrl()); + return (o instanceof Subscription) && url != null && url.equalsIgnoreCase(((Subscription) o).getUrl()); } public int hashCode() { return url.toLowerCase().hashCode(); diff --git a/app/src/main/java/io/lbry/browser/tasks/ClaimListResultHandler.java b/app/src/main/java/io/lbry/browser/tasks/ClaimListResultHandler.java new file mode 100644 index 00000000..2f4ce404 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/ClaimListResultHandler.java @@ -0,0 +1,10 @@ +package io.lbry.browser.tasks; + +import java.util.List; + +import io.lbry.browser.model.Claim; + +public interface ClaimListResultHandler { + void onSuccess(List claims); + void onError(Exception error); +} diff --git a/app/src/main/java/io/lbry/browser/tasks/ClaimListTask.java b/app/src/main/java/io/lbry/browser/tasks/ClaimListTask.java new file mode 100644 index 00000000..67a33d34 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/ClaimListTask.java @@ -0,0 +1,67 @@ +package io.lbry.browser.tasks; + +import android.os.AsyncTask; +import android.view.View; + +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.exceptions.ApiCallException; +import io.lbry.browser.model.Claim; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; + +public class ClaimListTask extends AsyncTask> { + private String type; + private View progressView; + private ClaimListResultHandler handler; + private Exception error; + + public ClaimListTask(String type, View progressView, ClaimListResultHandler handler) { + this.type = type; + this.progressView = progressView; + this.handler = handler; + } + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + } + protected List doInBackground(Void... params) { + List claims = null; + + try { + Map options = new HashMap<>(); + if (!Helper.isNullOrEmpty(type)) { + options.put("claim_type", type); + } + options.put("page", 1); + options.put("page_size", 999); + options.put("resolve", true); + + JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_CLAIM_LIST, options); + JSONArray items = result.getJSONArray("items"); + claims = new ArrayList<>(); + for (int i = 0; i < items.length(); i++) { + claims.add(Claim.fromJSONObject(items.getJSONObject(i))); + } + } catch (ApiCallException | JSONException ex) { + error = ex; + } + return claims; + } + protected void onPostExecute(List claims) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (claims != null) { + handler.onSuccess(claims); + } else { + handler.onError(error); + } + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/ResolveTask.java b/app/src/main/java/io/lbry/browser/tasks/ResolveTask.java index 4472b585..52e3725d 100644 --- a/app/src/main/java/io/lbry/browser/tasks/ResolveTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/ResolveTask.java @@ -14,15 +14,15 @@ import io.lbry.browser.utils.Lbry; public class ResolveTask extends AsyncTask> { private List urls; private String connectionString; - private ResolveResultHandler handler; + private ClaimListResultHandler handler; private View progressView; private ApiCallException error; - public ResolveTask(String url, String connectionString, View progressView, ResolveResultHandler handler) { + public ResolveTask(String url, String connectionString, View progressView, ClaimListResultHandler handler) { this(Arrays.asList(url), connectionString, progressView, handler); } - public ResolveTask(List urls, String connectionString, View progressView, ResolveResultHandler handler) { + public ResolveTask(List urls, String connectionString, View progressView, ClaimListResultHandler handler) { this.urls = urls; this.connectionString = connectionString; this.progressView = progressView; @@ -50,8 +50,4 @@ public class ResolveTask extends AsyncTask> { } } - public interface ResolveResultHandler { - void onSuccess(List claims); - void onError(Exception error); - } } diff --git a/app/src/main/java/io/lbry/browser/tasks/UpdateSuggestedTagsTask.java b/app/src/main/java/io/lbry/browser/tasks/UpdateSuggestedTagsTask.java new file mode 100644 index 00000000..c856027b --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/UpdateSuggestedTagsTask.java @@ -0,0 +1,70 @@ +package io.lbry.browser.tasks; + +import android.os.AsyncTask; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import io.lbry.browser.adapter.TagListAdapter; +import io.lbry.browser.model.Tag; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; + +public class UpdateSuggestedTagsTask extends AsyncTask> { + + private boolean clearPrevious; + private int limit; + private String filter; + private TagListAdapter addedTagsAdapter; + private TagListAdapter suggestedTagsAdapter; + private KnownTagsHandler handler; + + public UpdateSuggestedTagsTask(String filter, int limit, TagListAdapter addedTagsAdapter, TagListAdapter suggestedTagsAdapter, boolean clearPrevious, KnownTagsHandler handler) { + this.filter = filter; + this.limit = limit; + this.addedTagsAdapter = addedTagsAdapter; + this.suggestedTagsAdapter = suggestedTagsAdapter; + this.clearPrevious = clearPrevious; + this.handler = handler; + } + + protected List doInBackground(Void... params) { + List tags = new ArrayList<>(); + if (Helper.isNullOrEmpty(filter)) { + Random random = new Random(); + if (suggestedTagsAdapter != null && !clearPrevious) { + tags = new ArrayList<>(suggestedTagsAdapter.getTags()); + } + while (tags.size() < limit) { + Tag randomTag = Lbry.knownTags.get(random.nextInt(Lbry.knownTags.size())); + if (!Lbry.followedTags.contains(randomTag) && (addedTagsAdapter == null || !addedTagsAdapter.getTags().contains(randomTag))) { + tags.add(randomTag); + } + } + } else { + Tag filterTag = new Tag(filter); + if (addedTagsAdapter == null || !addedTagsAdapter.getTags().contains(filterTag)) { + tags.add(new Tag(filter)); + } + for (int i = 0; i < Lbry.knownTags.size() && tags.size() < limit - 1; i++) { + Tag knownTag = Lbry.knownTags.get(i); + if ((knownTag.getLowercaseName().startsWith(filter) || knownTag.getLowercaseName().matches(filter)) && + (!tags.contains(knownTag) && + !Lbry.followedTags.contains(knownTag) && (addedTagsAdapter == null || !addedTagsAdapter.getTags().contains(knownTag)))) { + tags.add(knownTag); + } + } + } + return tags; + } + protected void onPostExecute(List tags) { + if (handler != null) { + handler.onSuccess(tags); + } + } + + public interface KnownTagsHandler { + void onSuccess(List tags); + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java b/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java new file mode 100644 index 00000000..df6bb320 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java @@ -0,0 +1,101 @@ +package io.lbry.browser.tasks; + +import android.os.AsyncTask; +import android.view.View; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Random; + +import java.io.File; + +import io.lbry.browser.exceptions.LbryResponseException; +import io.lbry.browser.utils.Helper; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class UploadImageTask extends AsyncTask { + private String filePath; + private View progressView; + private UploadThumbnailHandler handler; + private Exception error; + + public UploadImageTask(String filePath, View progressView, UploadThumbnailHandler handler) { + this.filePath = filePath; + this.progressView = progressView; + this.handler = handler; + } + + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + } + protected String doInBackground(Void... params) { + String thumbnailUrl = null; + try { + File file = new File(filePath); + String fileName = file.getName(); + int dotIndex = fileName.lastIndexOf('.'); + String extension = "jpg"; + if (dotIndex > -1) { + extension = fileName.substring(dotIndex + 1); + } + String fileType = String.format("image/%s", extension); + RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM). + addFormDataPart("name", makeid()). + addFormDataPart("file", fileName, RequestBody.create(file, MediaType.parse(fileType))). + build(); + Request request = new Request.Builder().url("https://spee.ch/api/claim/publish").post(body).build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + JSONObject json = new JSONObject(response.body().string()); + if (json.has("success")) { + JSONObject data = json.getJSONObject("data"); + String url = Helper.getJSONString("url", null, data); + if (Helper.isNullOrEmpty(url)) { + throw new LbryResponseException("Invalid thumbnail url returned after upload."); + } + + thumbnailUrl = String.format("%s.%s", url, extension); + } else if (json.has("error")) { + JSONObject error = json.getJSONObject("error"); + String message = Helper.getJSONString("message", null, error); + throw new LbryResponseException(Helper.isNullOrEmpty(message) ? "The image failed to upload." : message); + } + } catch (IOException | JSONException | LbryResponseException ex) { + error = ex; + } + + return thumbnailUrl; + } + protected void onPostExecute(String thumbnailUrl) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (!Helper.isNullOrEmpty(thumbnailUrl)) { + handler.onSuccess(thumbnailUrl); + } else { + handler.onError(error); + } + } + } + + private static String makeid() { + Random random = new Random(); + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder id = new StringBuilder(); + for (int i = 0; i < 24; i++) { + id.append(chars.charAt(random.nextInt(chars.length()))); + } + return id.toString(); + } + + public interface UploadThumbnailHandler { + void onSuccess(String url); + void onError(Exception error); + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/content/ChannelCreateUpdateTask.java b/app/src/main/java/io/lbry/browser/tasks/content/ChannelCreateUpdateTask.java new file mode 100644 index 00000000..88cca734 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/content/ChannelCreateUpdateTask.java @@ -0,0 +1,78 @@ +package io.lbry.browser.tasks.content; + +import android.os.AsyncTask; +import android.view.View; + +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +import io.lbry.browser.exceptions.ApiCallException; +import io.lbry.browser.model.Claim; +import io.lbry.browser.tasks.GenericTaskHandler; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; + +public class ChannelCreateUpdateTask extends AsyncTask { + private Claim claim; + private BigDecimal deposit; + private boolean update; + private Exception error; + private GenericTaskHandler handler; + private View progressView; + + public ChannelCreateUpdateTask(Claim claim, BigDecimal deposit, boolean update, View progressView, GenericTaskHandler handler) { + this.claim = claim; + this.deposit = deposit; + this.update = update; + this.progressView = progressView; + this.handler = handler; + } + + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + if (handler != null) { + handler.beforeStart(); + } + } + protected Boolean doInBackground(Void... params) { + Map options = new HashMap<>(); + if (!update) { + options.put("name", claim.getName()); + } else { + options.put("claim_id", claim.getClaimId()); + } + options.put("bid", new DecimalFormat(Helper.SDK_AMOUNT_FORMAT).format(deposit.doubleValue())); + options.put("title", claim.getTitle()); + options.put("cover_url", claim.getCoverUrl()); + options.put("thumbnail_url", claim.getThumbnailUrl()); + options.put("description", claim.getDescription()); + options.put("website_url", claim.getWebsiteUrl()); + options.put("email", claim.getEmail()); + options.put("tags", claim.getTags()); + options.put("blocking", true); + + String method = !update ? Lbry.METHOD_CHANNEL_CREATE : Lbry.METHOD_CHANNEL_UPDATE; + try { + Lbry.genericApiCall(method, options); + } catch (ApiCallException | ClassCastException ex) { + error = ex; + return false; + } + + return true; + } + protected void onPostExecute(Boolean result) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (result) { + handler.onSuccess(); + } else { + handler.onError(error); + } + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/content/LogPublishTask.java b/app/src/main/java/io/lbry/browser/tasks/content/LogPublishTask.java new file mode 100644 index 00000000..e1b1911f --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/content/LogPublishTask.java @@ -0,0 +1,4 @@ +package io.lbry.browser.tasks.content; + +public class LogPublishTask { +} 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 aa6dd836..d30c4b9c 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 @@ -30,12 +30,11 @@ 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.TagListener; import io.lbry.browser.model.Claim; import io.lbry.browser.model.Tag; 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.utils.Helper; import io.lbry.browser.utils.Lbry; @@ -260,7 +259,7 @@ public class AllContentFragment extends BaseFragment implements SharedPreference private void showCustomizeTagsDialog() { CustomizeTagsDialogFragment dialog = CustomizeTagsDialogFragment.newInstance(); - dialog.setListener(new CustomizeTagsDialogFragment.TagListener() { + dialog.setListener(new TagListener() { @Override public void onTagAdded(Tag tag) { // heavy-lifting @@ -373,8 +372,12 @@ public class AllContentFragment extends BaseFragment implements SharedPreference } private Map buildContentOptions() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + Context context = getContext(); + boolean canShowMatureContent = false; + if (context != null) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + } return Lbry.buildClaimSearchOptions( null, 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 12f8da71..6f7a3ae0 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 @@ -199,8 +199,12 @@ public class ChannelContentFragment extends Fragment implements SharedPreference } private Map buildContentOptions() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + Context context = getContext(); + boolean canShowMatureContent = false; + if (context != null) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + } return Lbry.buildClaimSearchOptions( null, diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java new file mode 100644 index 00000000..0f89752f --- /dev/null +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java @@ -0,0 +1,597 @@ +package io.lbry.browser.ui.channel; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.google.android.flexbox.FlexboxLayoutManager; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputEditText; + +import java.io.File; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.lbry.browser.MainActivity; +import io.lbry.browser.R; +import io.lbry.browser.adapter.TagListAdapter; +import io.lbry.browser.listener.WalletBalanceListener; +import io.lbry.browser.model.Claim; +import io.lbry.browser.model.Tag; +import io.lbry.browser.model.WalletBalance; +import io.lbry.browser.tasks.GenericTaskHandler; +import io.lbry.browser.tasks.UpdateSuggestedTagsTask; +import io.lbry.browser.tasks.UploadImageTask; +import io.lbry.browser.tasks.content.ChannelCreateUpdateTask; +import io.lbry.browser.ui.BaseFragment; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; +import io.lbry.browser.utils.LbryUri; +import lombok.Getter; + +public class ChannelFormFragment extends BaseFragment implements WalletBalanceListener, TagListAdapter.TagClickListener { + + private static final int SUGGESTED_LIMIT = 8; + + @Getter + private boolean saveInProgress; + private boolean uploading; + private Claim currentClaim; + private boolean editFieldsLoaded; + private boolean editMode; + private View linkCancel; + private TextView linkShowOptional; + private MaterialButton buttonSave; + + private TextView inlineBalanceValue; + private View uploadProgress; + private View containerOptionalFields; + private ImageView imageCover; + private ImageView imageThumbnail; + private TextInputEditText inputTitle; + private TextInputEditText inputChannelName; + private TextInputEditText inputDeposit; + private TextInputEditText inputDescription; + private TextInputEditText inputWebsite; + private TextInputEditText inputEmail; + + private TextInputEditText inputTagFilter; + private RecyclerView addedTagsList; + private RecyclerView suggestedTagsList; + private TagListAdapter addedTagsAdapter; + private TagListAdapter suggestedTagsAdapter; + private View noTagsView; + private View noTagResultsView; + + private View coverEditArea; + private View iconContainer; + private View channelSaveProgress; + + private boolean launchCoverSelectPending; + private boolean launchThumbnailSelectPending; + private boolean coverFilePickerActive; + private boolean thumbnailFilePickerActive; + + private String currentFilter; + + private String coverUrl; + private String thumbnailUrl; + private String lastSelectedCoverFile; + private String lastSelectedThumbnailFile; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_channel_form, container, false); + + linkCancel = root.findViewById(R.id.channel_form_cancel_link); + linkShowOptional = root.findViewById(R.id.channel_form_toggle_optional); + buttonSave = root.findViewById(R.id.channel_form_save_button); + + containerOptionalFields = root.findViewById(R.id.channel_form_optional_fields_container); + inputTitle = root.findViewById(R.id.channel_form_input_title); + inputChannelName = root.findViewById(R.id.channel_form_input_channel_name); + inputDeposit = root.findViewById(R.id.channel_form_input_deposit); + inputDescription = root.findViewById(R.id.channel_form_input_description); + inputWebsite = root.findViewById(R.id.channel_form_input_website); + inputEmail = root.findViewById(R.id.channel_form_input_email); + inputTagFilter = root.findViewById(R.id.form_tag_filter_input); + + coverEditArea = root.findViewById(R.id.channel_form_cover_edit_area); + iconContainer = root.findViewById(R.id.channel_form_icon_container); + imageCover = root.findViewById(R.id.channel_form_cover_image); + imageThumbnail = root.findViewById(R.id.channel_form_thumbnail); + inlineBalanceValue = root.findViewById(R.id.channel_form_inline_balance_value); + uploadProgress = root.findViewById(R.id.channel_form_upload_progress); + channelSaveProgress = root.findViewById(R.id.channel_form_save_progress); + + Context context = getContext(); + FlexboxLayoutManager flm1 = new FlexboxLayoutManager(context); + FlexboxLayoutManager flm2 = new FlexboxLayoutManager(context); + addedTagsList = root.findViewById(R.id.form_added_tags); + addedTagsList.setLayoutManager(flm1); + suggestedTagsList = root.findViewById(R.id.form_suggested_tags); + suggestedTagsList.setLayoutManager(flm2); + + addedTagsAdapter = new TagListAdapter(new ArrayList<>(), context); + addedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_REMOVE); + addedTagsAdapter.setClickListener(this); + addedTagsList.setAdapter(addedTagsAdapter); + suggestedTagsAdapter = new TagListAdapter(new ArrayList<>(), getContext()); + suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD); + suggestedTagsAdapter.setClickListener(this); + suggestedTagsList.setAdapter(suggestedTagsAdapter); + + noTagsView = root.findViewById(R.id.form_no_added_tags); + noTagResultsView = root.findViewById(R.id.form_no_tag_results); + + buttonSave = root.findViewById(R.id.channel_form_save_button); + + linkShowOptional.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (containerOptionalFields.getVisibility() != View.VISIBLE) { + containerOptionalFields.setVisibility(View.VISIBLE); + linkShowOptional.setText(R.string.hide_optional_fields); + } else { + containerOptionalFields.setVisibility(View.GONE); + linkShowOptional.setText(R.string.show_optional_fields); + } + } + }); + linkCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).onBackPressed(); + } + } + }); + coverEditArea.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (uploading) { + Snackbar.make(getView(), R.string.wait_for_upload, Snackbar.LENGTH_LONG).show(); + return; + } + + checkPermissionsAndLaunchFilePicker(true); + } + }); + iconContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (uploading) { + Snackbar.make(getView(), R.string.wait_for_upload, Snackbar.LENGTH_LONG).show(); + return; + } + + checkPermissionsAndLaunchFilePicker(false); + } + }); + + buttonSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Claim claimToSave = buildChannelClaimToSave(); + validateAndSaveClaim(claimToSave); + } + }); + inputTagFilter.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + String value = Helper.getValue(charSequence); + setFilter(value); + } + + @Override + public void afterTextChanged(Editable editable) { + + } + }); + + return root; + } + + private void checkParams() { + Map params = getParams(); + if (params.containsKey("claim")) { + Claim claim = (Claim) params.get("claim"); + if (claim != null && !claim.equals(this.currentClaim)) { + this.currentClaim = claim; + editFieldsLoaded = false; + } + } + } + + private void updateFieldsFromCurrentClaim() { + if (currentClaim != null && !editFieldsLoaded) { + inputTitle.setText(currentClaim.getTitle()); + inputChannelName.setText(currentClaim.getName()); + inputDeposit.setText(currentClaim.getAmount()); + inputEmail.setText(currentClaim.getEmail()); + inputWebsite.setText(currentClaim.getWebsiteUrl()); + inputDescription.setText(currentClaim.getDescription()); + if (currentClaim.getTagObjects() != null) { + addedTagsAdapter.addTags(currentClaim.getTagObjects()); + } + + Context context = getContext(); + if (context != null) { + if (!Helper.isNullOrEmpty(currentClaim.getCoverUrl())) { + Glide.with(context.getApplicationContext()).load(currentClaim.getCoverUrl()).centerCrop().into(imageCover); + coverUrl = currentClaim.getCoverUrl(); + } + if (!Helper.isNullOrEmpty(currentClaim.getThumbnailUrl())) { + Glide.with(context.getApplicationContext()).load(currentClaim.getThumbnailUrl()).apply(RequestOptions.circleCropTransform()).into(imageThumbnail); + thumbnailUrl = currentClaim.getThumbnailUrl(); + } + } + + inputChannelName.setEnabled(false); + editMode = true; + editFieldsLoaded = true; + } + } + + private void validateAndSaveClaim(Claim claim) { + if (!editMode) { + String channelName = claim.getName().startsWith("@") ? claim.getName().substring(1) : claim.getName(); + if (Helper.isNullOrEmpty(channelName)) { + showError(getString(R.string.please_enter_channel_name)); + return; + } + if (!LbryUri.isNameValid(channelName)) { + showError(getString(R.string.channel_name_invalid_characters)); + return; + } + if (Helper.channelExists(channelName)) { + showError(getString(R.string.channel_name_already_created)); + return; + } + } + + String depositString = Helper.getValue(inputDeposit.getText()); + double depositAmount = 0; + try { + depositAmount = Double.valueOf(depositString); + } catch (NumberFormatException ex) { + // pass + showError(getString(R.string.please_enter_valid_deposit)); + return; + } + if (depositAmount == 0) { + String error = getResources().getQuantityString(R.plurals.min_deposit_required, depositAmount == 1 ? 1 : 2, String.valueOf(Helper.MIN_DEPOSIT)); + showError(error); + return; + } + if (Lbry.walletBalance == null || Lbry.walletBalance.getAvailable().doubleValue() < depositAmount) { + showError(getString(R.string.deposit_more_than_balance)); + return; + } + + ChannelCreateUpdateTask task = new ChannelCreateUpdateTask(claim, new BigDecimal(depositString), editMode, channelSaveProgress, new GenericTaskHandler() { + @Override + public void beforeStart() { + preSave(); + } + + @Override + public void onSuccess() { + postSave(); + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.showMessage(R.string.channel_save_successful); + activity.onBackPressed(); + } + } + + @Override + public void onError(Exception error) { + showError(error != null ? error.getMessage() : getString(R.string.channel_save_failed)); + postSave(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + + private void showError(String message) { + Context context = getContext(); + if (context != null) { + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint( + ContextCompat.getColor(context, R.color.red)).show(); + } + } + + public void checkPermissionsAndLaunchFilePicker(boolean isCover) { + Context context = getContext(); + if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) { + launchCoverSelectPending = false; + launchThumbnailSelectPending = false; + + coverFilePickerActive = isCover; + thumbnailFilePickerActive = !isCover; + launchFilePicker(); + } else { + launchCoverSelectPending = isCover; + launchThumbnailSelectPending = !isCover; + MainActivity.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, + MainActivity.REQUEST_STORAGE_PERMISSION, + getString(R.string.storage_permission_rationale_images), + context, + true); + } + } + + private void launchFilePicker() { + Context context = getContext(); + if (context instanceof MainActivity) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("image/*"); + ((MainActivity) context).startActivityForResult( + Intent.createChooser(intent, getString(coverFilePickerActive ? R.string.select_cover : R.string.select_thumbnail)), + MainActivity.REQUEST_FILE_PICKER); + } + + } + + public void onFilePicked(String filePath) { + if (Helper.isNullOrEmpty(filePath)) { + Snackbar.make(getView(), R.string.undetermined_image_filepath, Snackbar.LENGTH_LONG).setBackgroundTint( + ContextCompat.getColor(getContext(), R.color.red)).show(); + return; + } + + Context context = getContext(); + if (context != null) { + Uri fileUri = Uri.fromFile(new File(filePath)); + if (coverFilePickerActive) { + // cover selected + if (filePath.equalsIgnoreCase(lastSelectedCoverFile)) { + // previous selected cover was uploaded successfully + return; + } + Glide.with(context.getApplicationContext()).load(fileUri).centerCrop().into(imageCover); + } else if (thumbnailFilePickerActive) { + if (filePath.equalsIgnoreCase(lastSelectedThumbnailFile)) { + // previous selected thumbnail was uploaded successfully + return; + } + // thumbnail selected + Glide.with(context.getApplicationContext()).load(fileUri).apply(RequestOptions.circleCropTransform()).into(imageThumbnail); + } + + // Upload the image + uploading = true; + UploadImageTask task = new UploadImageTask(filePath, uploadProgress, new UploadImageTask.UploadThumbnailHandler() { + @Override + public void onSuccess(String url) { + if (coverFilePickerActive) { + coverUrl = url; + lastSelectedCoverFile = filePath; + } else if (thumbnailFilePickerActive) { + thumbnailUrl = url; + lastSelectedThumbnailFile = filePath; + } + + coverFilePickerActive = false; + thumbnailFilePickerActive = false; + uploading = false; + } + + @Override + public void onError(Exception error) { + Snackbar.make(getView(), R.string.image_upload_failed, Snackbar.LENGTH_LONG).setBackgroundTint( + ContextCompat.getColor(context, R.color.red) + ).show(); + coverFilePickerActive = false; + thumbnailFilePickerActive = false; + uploading = false; + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + coverFilePickerActive = false; + thumbnailFilePickerActive = false; + } + } + + public void onStoragePermissionGranted() { + if (launchCoverSelectPending) { + checkPermissionsAndLaunchFilePicker(true); + } else if (launchThumbnailSelectPending) { + checkPermissionsAndLaunchFilePicker(false); + } + } + public void onStoragePermissionRefused() { + Snackbar.make(getView(), R.string.storage_permission_rationale_images, Snackbar.LENGTH_LONG).setBackgroundTint( + ContextCompat.getColor(getContext(), R.color.red) + ).show(); + } + + @Override + public void onStart() { + super.onStart(); + MainActivity activity = (MainActivity) getContext(); + if (activity != null) { + activity.hideSearchBar(); + activity.showNavigationBackIcon(); + activity.lockDrawer(); + activity.hideFloatingWalletBalance(); + activity.addWalletBalanceListener(this); + + ActionBar actionBar = activity.getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(editMode ? R.string.edit_channel : R.string.create_a_channel); + } + } + } + + @Override + public void onStop() { + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) getContext(); + activity.removeWalletBalanceListener(this); + activity.restoreToggle(); + activity.showFloatingWalletBalance(); + } + super.onStop(); + } + + public void onResume() { + super.onResume(); + checkParams(); + updateFieldsFromCurrentClaim(); + + String filterText = Helper.getValue(inputTagFilter.getText()); + updateSuggestedTags(filterText, SUGGESTED_LIMIT, true); + } + + private void checkNoAddedTags() { + Helper.setViewVisibility(noTagsView, addedTagsAdapter == null || addedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + private void checkNoTagResults() { + Helper.setViewVisibility(noTagResultsView, suggestedTagsAdapter == null || suggestedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + public void addTag(Tag tag) { + if (addedTagsAdapter.getTags().contains(tag)) { + Snackbar.make(getView(), getString(R.string.tag_already_added, tag.getName()), Snackbar.LENGTH_LONG).show(); + return; + } + + tag.setFollowed(true); + addedTagsAdapter.addTag(tag); + if (suggestedTagsAdapter != null) { + suggestedTagsAdapter.removeTag(tag); + } + updateSuggestedTags(currentFilter, SUGGESTED_LIMIT, false); + + checkNoAddedTags(); + checkNoTagResults(); + } + public void removeTag(Tag tag) { + tag.setFollowed(false); + addedTagsAdapter.removeTag(tag); + updateSuggestedTags(currentFilter, SUGGESTED_LIMIT, false); + checkNoAddedTags(); + checkNoTagResults(); + } + + @Override + public void onWalletBalanceUpdated(WalletBalance walletBalance) { + if (walletBalance != null && inlineBalanceValue != null) { + inlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue())); + } + } + + public void setFilter(String filter) { + currentFilter = filter; + updateSuggestedTags(currentFilter, SUGGESTED_LIMIT, true); + } + + private void updateSuggestedTags(String filter, int limit, boolean clearPrevious) { + UpdateSuggestedTagsTask task = new UpdateSuggestedTagsTask(filter, limit, addedTagsAdapter, suggestedTagsAdapter, clearPrevious, new UpdateSuggestedTagsTask.KnownTagsHandler() { + @Override + public void onSuccess(List tags) { + if (suggestedTagsAdapter == null) { + suggestedTagsAdapter = new TagListAdapter(tags, getContext()); + suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD); + suggestedTagsAdapter.setClickListener(ChannelFormFragment.this); + if (suggestedTagsList != null) { + suggestedTagsList.setAdapter(suggestedTagsAdapter); + } + } else { + suggestedTagsAdapter.setTags(tags); + } + + checkNoAddedTags(); + checkNoTagResults(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private Claim buildChannelClaimToSave() { + Claim claim = new Claim(); + if (!editMode) { + String name = Helper.getValue(inputChannelName.getText()); + if (!name.startsWith("@")) { + name = String.format("@%s", name); + } + claim.setName(name); + } else if (currentClaim != null) { + claim.setClaimId(currentClaim.getClaimId()); + } + + Claim.ChannelMetadata metadata = new Claim.ChannelMetadata(); + metadata.setTitle(Helper.getValue(inputTitle.getText())); + metadata.setDescription(Helper.getValue(inputDescription.getText())); + metadata.setWebsiteUrl(Helper.getValue(inputWebsite.getText())); + metadata.setEmail(Helper.getValue(inputEmail.getText())); + + Claim.Resource cover = new Claim.Resource(); + cover.setUrl(coverUrl == null ? "" : coverUrl); + Claim.Resource thumbnail = new Claim.Resource(); + thumbnail.setUrl(thumbnailUrl == null ? "" : thumbnailUrl); + metadata.setThumbnail(thumbnail); + metadata.setCover(cover); + + List addedTags = addedTagsAdapter != null ? new ArrayList<>(addedTagsAdapter.getTags()) : new ArrayList<>(); + metadata.setTags(Helper.getTagsForTagObjects(addedTags)); + + claim.setValue(metadata); + return claim; + } + + private void preSave() { + saveInProgress = true; + Helper.setViewVisibility(linkShowOptional, View.GONE); + Helper.setViewEnabled(linkCancel, false); + Helper.setViewEnabled(buttonSave, false); + } + + private void postSave() { + Helper.setViewVisibility(linkShowOptional, View.VISIBLE); + Helper.setViewEnabled(linkCancel, true); + Helper.setViewEnabled(buttonSave, true); + saveInProgress = false; + } + + @Override + public void onTagClicked(Tag tag, int customizeMode) { + if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_ADD) { + addTag(tag); + } else if (customizeMode == TagListAdapter.CUSTOMIZE_MODE_REMOVE) { + removeTag(tag); + } + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java index 6c2881c2..92d39450 100644 --- a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFragment.java @@ -14,8 +14,6 @@ import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; import androidx.viewpager2.adapter.FragmentStateAdapter; import androidx.viewpager2.widget.ViewPager2; @@ -37,6 +35,7 @@ import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.model.Claim; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.tasks.ChannelSubscribeTask; +import io.lbry.browser.tasks.ClaimListResultHandler; import io.lbry.browser.tasks.ResolveTask; import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.ui.controls.SolidIconView; @@ -196,7 +195,10 @@ public class ChannelFragment extends BaseFragment { boolean isFollowing = Lbryio.isFollowing(claim); if (iconFollowUnfollow != null) { iconFollowUnfollow.setText(isFollowing ? R.string.fa_heart_broken : R.string.fa_heart); - iconFollowUnfollow.setTextColor(ContextCompat.getColor(getContext(), isFollowing ? R.color.foreground : R.color.red)); + Context context = getContext(); + if (context != null) { + iconFollowUnfollow.setTextColor(ContextCompat.getColor(context, isFollowing ? R.color.foreground : R.color.red)); + } } } } @@ -244,7 +246,7 @@ public class ChannelFragment extends BaseFragment { private void resolveUrl() { layoutDisplayArea.setVisibility(View.INVISIBLE); - ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, layoutResolving, new ResolveTask.ResolveResultHandler() { + ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, layoutResolving, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { if (claims.size() > 0) { @@ -300,7 +302,9 @@ public class ChannelFragment extends BaseFragment { int bgColor = Helper.generateRandomColorForValue(claim.getClaimId()); Helper.setIconViewBackgroundColor(noThumbnailView, bgColor, false, getContext()); noThumbnailView.setVisibility(View.VISIBLE); - textAlpha.setText(claim.getName().substring(1, 2)); + if (claim.getName() != null) { + textAlpha.setText(claim.getName().substring(1, 2)); + } } try { @@ -332,16 +336,20 @@ public class ChannelFragment extends BaseFragment { switch (position) { case 0: ChannelContentFragment contentFragment = ChannelContentFragment.class.newInstance(); - contentFragment.setChannelId(channelClaim.getClaimId()); + if (channelClaim != null) { + contentFragment.setChannelId(channelClaim.getClaimId()); + } return contentFragment; case 1: ChannelAboutFragment aboutFragment = ChannelAboutFragment.class.newInstance(); try { Claim.ChannelMetadata metadata = (Claim.ChannelMetadata) channelClaim.getValue(); - aboutFragment.setDescription(metadata.getDescription()); - aboutFragment.setEmail(metadata.getEmail()); - aboutFragment.setWebsite(metadata.getWebsiteUrl()); + if (metadata != null) { + aboutFragment.setDescription(metadata.getDescription()); + aboutFragment.setEmail(metadata.getEmail()); + aboutFragment.setWebsite(metadata.getWebsiteUrl()); + } } catch (ClassCastException ex) { // pass } diff --git a/app/src/main/java/io/lbry/browser/ui/channel/ChannelManagerFragment.java b/app/src/main/java/io/lbry/browser/ui/channel/ChannelManagerFragment.java new file mode 100644 index 00000000..a36ddad0 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelManagerFragment.java @@ -0,0 +1,273 @@ +package io.lbry.browser.ui.channel; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.ActionMode; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.text.DecimalFormat; +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.R; +import io.lbry.browser.adapter.ClaimListAdapter; +import io.lbry.browser.listener.SdkStatusListener; +import io.lbry.browser.listener.SelectionModeListener; +import io.lbry.browser.model.Claim; +import io.lbry.browser.model.NavMenuItem; +import io.lbry.browser.tasks.ClaimListResultHandler; +import io.lbry.browser.tasks.ClaimListTask; +import io.lbry.browser.ui.BaseFragment; +import io.lbry.browser.utils.Helper; +import io.lbry.browser.utils.Lbry; + +public class ChannelManagerFragment extends BaseFragment implements ActionMode.Callback, SelectionModeListener, SdkStatusListener { + + private Button buttonNewChannel; + private FloatingActionButton fabNewChannel; + private ActionMode actionMode; + private View emptyView; + private View layoutSdkInitializing; + private ProgressBar loading; + private ProgressBar bigLoading; + private RecyclerView channelList; + private ClaimListAdapter adapter; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_channel_manager, container, false); + + + buttonNewChannel = root.findViewById(R.id.channel_manager_create_button); + fabNewChannel = root.findViewById(R.id.channel_manager_fab_new_channel); + buttonNewChannel.setOnClickListener(newChannelClickListener); + fabNewChannel.setOnClickListener(newChannelClickListener); + + emptyView = root.findViewById(R.id.channel_manager_empty_container); + layoutSdkInitializing = root.findViewById(R.id.container_sdk_initializing); + channelList = root.findViewById(R.id.channel_manager_list); + LinearLayoutManager llm = new LinearLayoutManager(getContext()); + channelList.setLayoutManager(llm); + loading = root.findViewById(R.id.channel_manager_list_loading); + bigLoading = root.findViewById(R.id.channel_manager_list_big_loading); + + layoutSdkInitializing.setVisibility(Lbry.SDK_READY ? View.GONE : View.VISIBLE); + + return root; + } + + private View.OnClickListener newChannelClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).openChannelForm(null); + } + } + }; + + @Override + public void onStart() { + super.onStart(); + Context context = getContext(); + if (context != null) { + MainActivity activity = (MainActivity) context; + activity.hideFloatingWalletBalance(); + } + } + + @Override + public void onStop() { + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.showFloatingWalletBalance(); + } + super.onStop(); + } + + public void onResume() { + super.onResume(); + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).setWunderbarValue(null); + } + + if (!Lbry.SDK_READY) { + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.addSdkStatusListener(this); + } + } else { + onSdkReady(); + } + } + + public void onSdkReady() { + Helper.setViewVisibility(layoutSdkInitializing, View.GONE); + Helper.setViewVisibility(fabNewChannel, View.VISIBLE); + if (adapter != null && channelList != null) { + channelList.setAdapter(adapter); + } + fetchChannels(); + } + + public View getLoading() { + return (adapter == null || adapter.getItemCount() == 0) ? bigLoading : loading; + } + + private void checkNoChannels() { + Helper.setViewVisibility(emptyView, adapter == null || adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + + private void fetchChannels() { + Helper.setViewVisibility(emptyView, View.GONE); + ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, getLoading(), new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + Lbry.ownChannels = new ArrayList<>(claims); + Context context = getContext(); + if (adapter == null) { + adapter = new ClaimListAdapter(claims, context); + adapter.setSelectionModeListener(ChannelManagerFragment.this); + adapter.setListener(new ClaimListAdapter.ClaimListItemListener() { + @Override + public void onClaimClicked(Claim claim) { + if (context instanceof MainActivity) { + ((MainActivity) context).openChannelClaim(claim); + } + } + }); + if (channelList != null) { + channelList.setAdapter(adapter); + } + } else { + adapter.setItems(claims); + } + + checkNoChannels(); + } + + @Override + public void onError(Exception error) { + checkNoChannels(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + public void onEnterSelectionMode() { + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.startSupportActionMode(this); + } + } + public void onItemSelectionToggled() { + if (actionMode != null) { + actionMode.setTitle(String.valueOf(adapter.getSelectedCount())); + actionMode.invalidate(); + } + } + public void onExitSelectionMode() { + if (actionMode != null) { + actionMode.finish(); + } + } + + @Override + public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { + this.actionMode = actionMode; + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + if (!activity.isDarkMode()) { + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } + } + + actionMode.getMenuInflater().inflate(R.menu.menu_claim_list, menu); + return true; + } + @Override + public void onDestroyActionMode(ActionMode actionMode) { + if (adapter != null) { + adapter.clearSelectedItems(); + adapter.setInSelectionMode(false); + adapter.notifyDataSetChanged(); + } + Context context = getContext(); + if (context != null) { + MainActivity activity = (MainActivity) context; + if (!activity.isDarkMode()) { + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + } + this.actionMode = null; + } + + @Override + public boolean onPrepareActionMode(androidx.appcompat.view.ActionMode actionMode, Menu menu) { + int selectionCount = adapter != null ? adapter.getSelectedCount() : 0; + menu.findItem(R.id.action_edit).setVisible(selectionCount == 1); + return true; + } + + @Override + public boolean onActionItemClicked(androidx.appcompat.view.ActionMode actionMode, MenuItem menuItem) { + if (R.id.action_edit == menuItem.getItemId()) { + if (adapter != null && adapter.getSelectedCount() > 0) { + Claim claim = adapter.getSelectedItems().get(0); + // start channel editor with the claim + Context context = getContext(); + if (context instanceof MainActivity) { + Map params = new HashMap<>(); + params.put("claim", claim); + ((MainActivity) context).openFragment(ChannelFormFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS, params); + } + + actionMode.finish(); + return true; + } + } + if (R.id.action_delete == menuItem.getItemId()) { + if (adapter != null && adapter.getSelectedCount() > 0) { + final List selectedClaims = new ArrayList<>(adapter.getSelectedItems()); + String message = getResources().getQuantityString(R.plurals.confirm_delete_channels, selectedClaims.size()); + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()). + setTitle(R.string.delete_selection). + setMessage(message) + .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialogInterface, int i) { + //handleDeleteSelectedClaims(selectedClaims); + } + }).setNegativeButton(R.string.no, null); + builder.show(); + return true; + } + } + + return false; + } +} diff --git a/app/src/main/java/io/lbry/browser/ui/editorschoice/EditorsChoiceFragment.java b/app/src/main/java/io/lbry/browser/ui/editorschoice/EditorsChoiceFragment.java index 20b7b444..ca9b15d4 100644 --- a/app/src/main/java/io/lbry/browser/ui/editorschoice/EditorsChoiceFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/editorschoice/EditorsChoiceFragment.java @@ -65,8 +65,13 @@ public class EditorsChoiceFragment extends BaseFragment { } private Map buildContentOptions() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + Context context = getContext(); + boolean canShowMatureContent = false; + if (context != null) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + } + return Lbry.buildClaimSearchOptions( Claim.TYPE_REPOST, null, 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 28a4d92b..80c1142e 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 @@ -38,6 +38,7 @@ import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.model.Claim; import io.lbry.browser.model.lbryinc.Subscription; import io.lbry.browser.tasks.ChannelSubscribeTask; +import io.lbry.browser.tasks.ClaimListResultHandler; import io.lbry.browser.tasks.ClaimSearchTask; import io.lbry.browser.tasks.FetchSubscriptionsTask; import io.lbry.browser.tasks.ResolveTask; @@ -266,7 +267,6 @@ public class FollowingFragment extends BaseFragment implements } }); - Context context = getContext(); if (context instanceof MainActivity) { MainActivity activity = (MainActivity) context; @@ -389,8 +389,12 @@ public class FollowingFragment extends BaseFragment implements } private Map buildContentOptions() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); - boolean canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + Context context = getContext(); + boolean canShowMatureContent = false; + if (context != null) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext()); + canShowMatureContent = sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_MATURE_CONTENT, false); + } return Lbry.buildClaimSearchOptions( Claim.TYPE_STREAM, @@ -467,7 +471,7 @@ public class FollowingFragment extends BaseFragment implements private void fetchAndResolveChannelList() { buildChannelIdsAndUrls(); if (channelIds.size() > 0) { - ResolveTask resolveSubscribedTask = new ResolveTask(channelUrls, Lbry.LBRY_TV_CONNECTION_STRING, channelListLoading, new ResolveTask.ResolveResultHandler() { + ResolveTask resolveSubscribedTask = new ResolveTask(channelUrls, Lbry.LBRY_TV_CONNECTION_STRING, channelListLoading, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { updateChannelFilterListAdapter(claims, true); 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 a1f92477..e51172d1 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 @@ -21,6 +21,7 @@ import io.lbry.browser.R; import io.lbry.browser.adapter.ClaimListAdapter; import io.lbry.browser.model.Claim; import io.lbry.browser.model.ClaimCacheKey; +import io.lbry.browser.tasks.ClaimListResultHandler; import io.lbry.browser.tasks.ClaimSearchTask; import io.lbry.browser.tasks.LighthouseSearchTask; import io.lbry.browser.tasks.ResolveTask; @@ -138,7 +139,7 @@ public class SearchFragment extends BaseFragment implements return; } - ResolveTask task = new ResolveTask(vanityUrl, Lbry.LBRY_TV_CONNECTION_STRING, null, new ResolveTask.ResolveResultHandler() { + ResolveTask task = new ResolveTask(vanityUrl, Lbry.LBRY_TV_CONNECTION_STRING, null, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { if (claims.size() > 0) { diff --git a/app/src/main/java/io/lbry/browser/ui/wallet/TransactionHistoryFragment.java b/app/src/main/java/io/lbry/browser/ui/wallet/TransactionHistoryFragment.java index dd44bd12..86ee9982 100644 --- a/app/src/main/java/io/lbry/browser/ui/wallet/TransactionHistoryFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/wallet/TransactionHistoryFragment.java @@ -122,6 +122,7 @@ public class TransactionHistoryFragment extends BaseFragment implements Transact MainActivity activity = (MainActivity) getContext(); if (activity != null) { activity.hideSearchBar(); + activity.hideFloatingWalletBalance(); activity.showNavigationBackIcon(); activity.lockDrawer(); @@ -136,7 +137,9 @@ public class TransactionHistoryFragment extends BaseFragment implements Transact public void onStop() { Context context = getContext(); if (context instanceof MainActivity) { - ((MainActivity) context).restoreToggle(); + MainActivity activity = (MainActivity) context; + activity.restoreToggle(); + activity.showFloatingWalletBalance(); } super.onStop(); } 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 d7680cea..0a9df179 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 @@ -51,8 +51,6 @@ import io.lbry.browser.utils.Lbryio; public class WalletFragment extends BaseFragment implements SdkStatusListener, WalletBalanceListener { - private boolean sdkReady; - private View layoutAccountRecommended; private View layoutSdkInitializing; private View linkSkipAccount; @@ -96,7 +94,7 @@ public class WalletFragment extends BaseFragment implements SdkStatusListener, W loadingRecentContainer = root.findViewById(R.id.wallet_loading_recent_container); layoutAccountRecommended = root.findViewById(R.id.wallet_account_recommended_container); - layoutSdkInitializing = root.findViewById(R.id.wallet_sdk_initializing_container); + layoutSdkInitializing = root.findViewById(R.id.container_sdk_initializing); linkSkipAccount = root.findViewById(R.id.wallet_skip_account_link); buttonSignUp = root.findViewById(R.id.wallet_sign_up_button); @@ -441,7 +439,6 @@ public class WalletFragment extends BaseFragment implements SdkStatusListener, W } public void onSdkReady() { - sdkReady = true; Context context = getContext(); if (context instanceof MainActivity) { ((MainActivity) context).removeSdkStatusListener(this); 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 53f46162..84baa684 100644 --- a/app/src/main/java/io/lbry/browser/utils/Helper.java +++ b/app/src/main/java/io/lbry/browser/utils/Helper.java @@ -1,6 +1,8 @@ package io.lbry.browser.utils; +import android.annotation.SuppressLint; import android.content.BroadcastReceiver; +import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; @@ -9,7 +11,11 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.ShapeDrawable; +import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; import android.view.View; import android.widget.TextView; @@ -20,6 +26,7 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; @@ -46,6 +53,7 @@ public final class Helper { public static final MediaType FORM_MEDIA_TYPE = MediaType.parse("application/x-www-form-urlencoded"); public static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8"); public static final int CONTENT_PAGE_SIZE = 25; + public static final double MIN_DEPOSIT = 0.05; public static boolean isNull(String value) { return value == null; @@ -340,4 +348,192 @@ public final class Helper { } return String.format("%s %s", Build.MANUFACTURER, Build.MODEL); } + + public static boolean channelExists(String channelName) { + for (Claim claim : Lbry.ownChannels) { + if (channelName.equalsIgnoreCase(claim.getName())) { + return true; + } + } + return false; + } + + public static String getRealPathFromURI_API19(final Context context, final Uri uri) { + return getRealPathFromURI_API19(context, uri, false); + } + /** + * https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8 + */ + @SuppressLint("NewApi") + public static String getRealPathFromURI_API19(final Context context, final Uri uri, boolean folderPath) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + // This is for checking Main Memory + if ("primary".equalsIgnoreCase(type)) { + if (split.length > 1) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else { + return Environment.getExternalStorageDirectory() + "/"; + } + // This is for checking SD Card + } else { + return "storage" + "/" + docId.replace(":", "/"); + } + + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + String fileName = getFilePath(context, uri); + if (fileName != null) { + String extStorageDirectory = Environment.getExternalStorageDirectory().toString(); + return folderPath ? + String.format("%s/Download", extStorageDirectory) : + String.format("%s/Download/%s", extStorageDirectory, fileName); + } + + String id = DocumentsContract.getDocumentId(uri); + if (id.startsWith("raw:")) { + id = id.replaceFirst("raw:", ""); + File file = new File(id); + if (file.exists()) + return id; + } + + String[] contentUriPrefixesToTry = new String[]{ + "content://downloads/public_downloads", + "content://downloads/my_downloads", + "content://downloads/all_downloads" + }; + + for (String contentUriPrefix : contentUriPrefixesToTry) { + Uri contentUri = Helper.parseInt(id, -1) > 0 ? + ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id)) : + Uri.parse(contentUriPrefix); + try { + String path = getDataColumn(context, contentUri, null, null); + if (path != null) { + return path; + } + } catch (Exception ex) { + // pass + } + } + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + public static String getFilePath(Context context, Uri uri) { + Cursor cursor = null; + final String[] projection = { MediaStore.MediaColumns.DISPLAY_NAME }; + + try { + cursor = context.getContentResolver().query(uri, projection, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); + return cursor.getString(index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(index); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } } 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 bd697b96..0cb95e66 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbry.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbry.java @@ -47,6 +47,8 @@ public final class Lbry { public static WalletBalance walletBalance = new WalletBalance(); public static List knownTags = new ArrayList<>(); public static List followedTags = new ArrayList<>(); + public static List ownClaims = new ArrayList<>(); + public static List ownChannels = new ArrayList<>(); // Make this a subset of ownClaims? public static final int TTL_CLAIM_SEARCH_VALUE = 120000; // 2-minute TTL for cache public static final String SDK_CONNECTION_STRING = "http://127.0.0.1:5279"; @@ -87,6 +89,12 @@ public final class Lbry { public static final String METHOD_PREFERENCE_GET = "preference_get"; public static final String METHOD_PREFERENCE_SET = "preference_set"; + public static final String METHOD_CHANNEL_ABANDON = "channel_abandon"; + public static final String METHOD_CHANNEL_CREATE = "channel_create"; + public static final String METHOD_CHANNEL_UPDATE = "channel_update"; + + public static final String METHOD_CLAIM_LIST = "claim_list"; + public static KeyStore KEYSTORE; public static boolean SDK_READY = false; diff --git a/app/src/main/java/io/lbry/browser/utils/LbryUri.java b/app/src/main/java/io/lbry/browser/utils/LbryUri.java index cf8929bf..32d23612 100644 --- a/app/src/main/java/io/lbry/browser/utils/LbryUri.java +++ b/app/src/main/java/io/lbry/browser/utils/LbryUri.java @@ -43,6 +43,10 @@ public class LbryUri { return (!Helper.isNullOrEmpty(channelName) && Helper.isNullOrEmpty(streamName)) || (!Helper.isNullOrEmpty(claimName) && claimName.startsWith("@")); } + public static boolean isNameValid(String name) { + return !name.matches(REGEX_INVALID_URI); + } + public static LbryUri tryParse(String url) { try { return parse(url, false); diff --git a/app/src/main/res/drawable/bg_channel_overlay_icon.xml b/app/src/main/res/drawable/bg_channel_overlay_icon.xml new file mode 100644 index 00000000..554923d7 --- /dev/null +++ b/app/src/main/res/drawable/bg_channel_overlay_icon.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_selected_list_item.xml b/app/src/main/res/drawable/bg_selected_list_item.xml new file mode 100644 index 00000000..9f32fbbe --- /dev/null +++ b/app/src/main/res/drawable/bg_selected_list_item.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_transparent.xml b/app/src/main/res/drawable/bg_stream_overlay_icon.xml similarity index 69% rename from app/src/main/res/drawable/bg_transparent.xml rename to app/src/main/res/drawable/bg_stream_overlay_icon.xml index ed2c1e08..4f311895 100644 --- a/app/src/main/res/drawable/bg_transparent.xml +++ b/app/src/main/res/drawable/bg_stream_overlay_icon.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/container_sdk_initializing.xml b/app/src/main/res/layout/container_sdk_initializing.xml new file mode 100644 index 00000000..63f32959 --- /dev/null +++ b/app/src/main/res/layout/container_sdk_initializing.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/form_tag_search.xml b/app/src/main/res/layout/form_tag_search.xml new file mode 100644 index 00000000..55e5ab15 --- /dev/null +++ b/app/src/main/res/layout/form_tag_search.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_form.xml b/app/src/main/res/layout/fragment_channel_form.xml new file mode 100644 index 00000000..01a3740a --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_form.xml @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_manager.xml b/app/src/main/res/layout/fragment_channel_manager.xml new file mode 100644 index 00000000..1f3fcf06 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_manager.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index bdf2fd70..61e5e0ae 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -30,35 +30,7 @@ - - - - - - + + android:foreground="?attr/selectableItemBackground" + android:background="@drawable/bg_selected_list_item"> + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d8b8c8b0..014095f2 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -25,6 +25,7 @@ #40B887 #2F9176 #38D9A9 + #3338D9A9 #329A7E #77D510B8 @@ -49,4 +50,5 @@ #D5D5D5 #CAEDB9 + #CC333333 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c05dbb9b..71ea513c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -24,6 +24,7 @@ #40B887 #2F9176 #38D9A9 + #3338D9A9 #E3F6F1 #77F255DA @@ -48,4 +49,5 @@ #D5D5D5 #CAEDB9 + #CC333333 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dfa23654..27e148a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -194,7 +194,7 @@ Search for more tags You have not followed any tags yet. Get started by adding tags that you are interested in! We could not find new tags that you\'re not following. - The \'%1$s\' tag has already been added. + The \'%1$s\' tag has already been added. Send a tip Send a tip to %1$s This will appear as a tip for %1$s, which will boost its ability to be discovered while active. <a href="https://lbry.com/faq/tipping">Learn more</a>. @@ -225,6 +225,52 @@ Enable sync The wallet sync operation could not be completed at this time. Please try again later. If this problem persists, please send an email to hello@lbry.com. + + You have not added any tags yet. Add tags to improve discovery. + We could not find new tags that have not been added yet. + + + You have not created a channel.\nStart now by creating a new channel! + Create a channel + Edit channel + Delete selection? + Description + Yes + No + Show extra + Hide extra + Show optional fields + Hide optional fields + Save + Channel name + Title + \@ + Deposit + This LBC remains yours. It is a deposit to reserve the name and can be undone at any time. + LBRY requires access to load images from your device storage. + Select thumbnail + Select cover image + The file path could not be determined for the selected image. Please select an image in a different location. + Please wait for the current upload to finish. + The image upload request failed. Please try again. + Uploading... + Please enter a channel name. + Your channel name contains invalid characters. + You have already created a channel with the same name. + Please enter a valid deposit amount. + Deposit cannot be higher than your balance. + The channel save request failed. Please try again. + The channel was successfully saved. + The channel is pending publish on the blockchain. You will be able to access or edit the channel in a few moments. + + A minimum deposit of %1$s credit is required. + A minimum deposit of %1$s credits is required. + + + Are you sure you want to delete the selected channel? + Are you sure you want to delete the selected channels? + +