diff --git a/app/build.gradle b/app/build.gradle index 5f651471..5f0e2a56 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:2.3.0' implementation 'com.atlassian.commonmark:commonmark:0.14.0' - implementation 'com.arthenica:mobile-ffmpeg-full:4.3.1.LTS' + implementation 'com.arthenica:mobile-ffmpeg-full-gpl:4.3.1.LTS' compileOnly 'org.projectlombok:lombok:1.18.10' annotationProcessor 'org.projectlombok:lombok:1.18.10' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f682d17c..28eca8a7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:requestLegacyExternalStorage="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java index cde2651a..0c05f7c8 100644 --- a/app/src/main/java/io/lbry/browser/MainActivity.java +++ b/app/src/main/java/io/lbry/browser/MainActivity.java @@ -22,6 +22,7 @@ import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.provider.MediaStore; import android.text.Editable; import android.text.TextWatcher; import android.util.Base64; @@ -29,7 +30,6 @@ import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.Menu; -import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; @@ -57,6 +57,7 @@ import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.view.GravityCompat; @@ -78,12 +79,14 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; +import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.net.ConnectException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -101,6 +104,7 @@ import io.lbry.browser.exceptions.LbryUriException; import io.lbry.browser.listener.CameraPermissionListener; import io.lbry.browser.listener.DownloadActionListener; import io.lbry.browser.listener.FetchChannelsListener; +import io.lbry.browser.listener.FilePickerListener; import io.lbry.browser.listener.SdkStatusListener; import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.listener.WalletBalanceListener; @@ -173,9 +177,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public static boolean startingShareActivity = false; public static boolean startingPermissionRequest = false; public static boolean startingSignInFlowActivity = false; + public static boolean startingCameraRequest = false; private boolean enteringPIPMode = false; private boolean fullSyncInProgress = false; private int queuedSyncCount = 0; + private String cameraOutputFilename; @Setter private BackPressInterceptor backPressInterceptor; @@ -217,6 +223,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener public static final int REQUEST_REWARDS_VERIFY_SIGN_IN = 2003; public static final int REQUEST_FILE_PICKER = 5001; + public static final int REQUEST_VIDEO_CAPTURE = 5002; + public static final int REQUEST_TAKE_PHOTO = 5003; // broadcast action names public static final String ACTION_SDK_READY = "io.lbry.browser.Broadcast.SdkReady"; @@ -282,6 +290,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener private int selectedMenuItemId = -1; private List cameraPermissionListeners; private List downloadActionListeners; + private List filePickerListeners; private List sdkStatusListeners; private List storagePermissionListeners; private List walletBalanceListeners; @@ -399,6 +408,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener openNavFragments = new HashMap<>(); cameraPermissionListeners = new ArrayList<>(); downloadActionListeners = new ArrayList<>(); + filePickerListeners = new ArrayList<>(); sdkStatusListeners = new ArrayList<>(); storagePermissionListeners = new ArrayList<>(); walletBalanceListeners = new ArrayList<>(); @@ -535,6 +545,16 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener downloadActionListeners.remove(listener); } + public void addFilePickerListener(FilePickerListener listener) { + if (!filePickerListeners.contains(listener)) { + filePickerListeners.add(listener); + } + } + + public void removeFilePickerListener(FilePickerListener listener) { + filePickerListeners.remove(listener); + } + public void addSdkStatusListener(SdkStatusListener listener) { if (!sdkStatusListeners.contains(listener)) { sdkStatusListeners.add(listener); @@ -641,6 +661,12 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener openFragment(ChannelFormFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS, params); } + public void openPublishesOnSuccessfulPublish() { + // close publish form + getSupportFragmentManager().popBackStack(); + openFragment(PublishesFragment.class, true, NavMenuItem.ID_ITEM_PUBLISHES); + } + public void openPublishForm(Claim claim) { Map params = new HashMap<>(); if (claim != null) { @@ -1355,7 +1381,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener syncWalletAndLoadPreferences(); scheduleWalletBalanceUpdate(); scheduleWalletSyncTask(); - fetchChannels(); + fetchOwnChannels(); + fetchOwnClaims(); initFloatingWalletBalance(); } @@ -1740,6 +1767,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener drawer.closeDrawer(GravityCompat.START); } else { boolean handled = false; + // TODO: Refactor both forms as back press interceptors? ChannelFormFragment channelFormFragment = null; PublishFormFragment publishFormFragment = null; for (Fragment fragment : openNavFragments.values()) { @@ -1756,7 +1784,13 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener handled = true; return; } - //if (publishFormFragment != null && ) + if (publishFormFragment != null && (publishFormFragment.isSaveInProgress() || publishFormFragment.isTranscodeInProgress())) { + if (publishFormFragment.isTranscodeInProgress()) { + showMessage(R.string.transcode_in_progress); + } + handled = true; + return; + } if (!handled) { // check fragment and nav history @@ -1829,24 +1863,15 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_FILE_PICKER) { startingFilePickerActivity = false; - 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); + for (FilePickerListener listener : filePickerListeners) { + listener.onFilePicked(filePath); } } else { - if (channelFormFragment != null) { - channelFormFragment.onFilePickerCancelled(); + for (FilePickerListener listener : filePickerListeners) { + listener.onFilePickerCancelled(); } } } else if (requestCode == REQUEST_SIMPLE_SIGN_IN || requestCode == REQUEST_WALLET_SYNC_SIGN_IN) { @@ -1866,9 +1891,56 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener scheduleWalletSyncTask(); } } + } else if (requestCode == REQUEST_VIDEO_CAPTURE || requestCode == REQUEST_TAKE_PHOTO) { + if (resultCode == RESULT_OK) { + Map params = new HashMap<>(); + params.put("directFilePath", cameraOutputFilename); + openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params); + } + cameraOutputFilename = null; } } + public void requestVideoCapture() { + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + if (intent.resolveActivity(getPackageManager()) != null) { + String outputPath = String.format("%s/record", Utils.getAppInternalStorageDir(this)); + File dir = new File(outputPath); + if (!dir.isDirectory()) { + dir.mkdirs(); + } + + cameraOutputFilename = String.format("%s/VID_%s.mp4", outputPath, Helper.FILESTAMP_FORMAT.format(new Date())); + Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename)); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + startActivityForResult(intent, REQUEST_VIDEO_CAPTURE); + return; + } + + showError(getString(R.string.cannot_capture_video)); + } + + public void requestTakePhoto() { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (intent.resolveActivity(getPackageManager()) != null) { + String outputPath = String.format("%s/photos", Utils.getAppInternalStorageDir(this)); + File dir = new File(outputPath); + if (!dir.isDirectory()) { + dir.mkdirs(); + } + + cameraOutputFilename = String.format("%s/IMG_%s.jpg", outputPath, Helper.FILESTAMP_FORMAT.format(new Date())); + Uri outputUri = FileProvider.getUriForFile(this, String.format("%s.fileprovider", getPackageName()), new File(cameraOutputFilename)); + intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri); + startActivityForResult(intent, REQUEST_TAKE_PHOTO); + return; + } + + showError(getString(R.string.cannot_take_photo)); + } + + + private void applyNavbarSigninPadding() { int statusBarHeight = getStatusBarHeight(); @@ -2550,11 +2622,11 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener } } - public void fetchChannels() { + public void fetchOwnChannels() { ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, null, new ClaimListResultHandler() { @Override public void onSuccess(List claims) { - Lbry.ownChannels = new ArrayList<>(claims); + Lbry.ownChannels = Helper.filterDeletedClaims(new ArrayList<>(claims)); for (FetchChannelsListener listener : fetchChannelsListeners) { listener.onChannelsFetched(claims); } @@ -2568,6 +2640,19 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } + public void fetchOwnClaims() { + ClaimListTask task = new ClaimListTask(Arrays.asList(Claim.TYPE_STREAM, Claim.TYPE_REPOST), null, new ClaimListResultHandler() { + @Override + public void onSuccess(List claims) { + Lbry.ownClaims = Helper.filterDeletedClaims(new ArrayList<>(claims)); + } + + @Override + public void onError(Exception error) { } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + private void checkSyncedWallet() { String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE); // Just check if the current user has a synced wallet, no need to do anything else here diff --git a/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java b/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java index 285f7676..b9edfcc0 100644 --- a/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java +++ b/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java @@ -11,14 +11,11 @@ import android.widget.TextView; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; -import com.bumptech.glide.request.RequestOptions; import java.util.ArrayList; import java.util.List; import io.lbry.browser.R; -import io.lbry.browser.listener.ChannelItemSelectionListener; -import io.lbry.browser.model.Claim; import io.lbry.browser.model.GalleryItem; import io.lbry.browser.utils.Helper; import lombok.Setter; @@ -78,7 +75,7 @@ public class GalleryGridAdapter extends RecyclerView.Adapter 0 ? View.VISIBLE : View.INVISIBLE); vh.durationView.setText(item.getDuration() > 0 ? Helper.formatDuration(Double.valueOf(item.getDuration() / 1000.0).longValue()) : null); diff --git a/app/src/main/java/io/lbry/browser/listener/FilePickerListener.java b/app/src/main/java/io/lbry/browser/listener/FilePickerListener.java new file mode 100644 index 00000000..e4dc8128 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/listener/FilePickerListener.java @@ -0,0 +1,6 @@ +package io.lbry.browser.listener; + +public interface FilePickerListener { + void onFilePicked(String filePath); + void onFilePickerCancelled(); +} diff --git a/app/src/main/java/io/lbry/browser/model/GalleryItem.java b/app/src/main/java/io/lbry/browser/model/GalleryItem.java index 52124d09..43feee58 100644 --- a/app/src/main/java/io/lbry/browser/model/GalleryItem.java +++ b/app/src/main/java/io/lbry/browser/model/GalleryItem.java @@ -8,6 +8,6 @@ public class GalleryItem { private String name; private String filePath; private String type; - private String thumbnailUrl; + private String thumbnailPath; private long duration; } diff --git a/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java b/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java index df6bb320..85be7a0a 100644 --- a/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/UploadImageTask.java @@ -7,7 +7,6 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.util.Random; import java.io.File; @@ -47,14 +46,14 @@ public class UploadImageTask extends AsyncTask { } String fileType = String.format("image/%s", extension); RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM). - addFormDataPart("name", makeid()). + addFormDataPart("name", Helper.makeid(24)). 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")) { + if (json.has("success") && Helper.getJSONBoolean("success", false, json)) { JSONObject data = json.getJSONObject("data"); String url = Helper.getJSONString("url", null, data); if (Helper.isNullOrEmpty(url)) { @@ -62,9 +61,15 @@ public class UploadImageTask extends AsyncTask { } thumbnailUrl = String.format("%s.%s", url, extension); - } else if (json.has("error")) { - JSONObject error = json.getJSONObject("error"); - String message = Helper.getJSONString("message", null, error); + } else if (json.has("error") || json.has("message")) { + JSONObject error = Helper.getJSONObject("error", json); + String message = null; + if (error != null) { + message = Helper.getJSONString("message", null, error); + } + if (Helper.isNullOrEmpty(message)) { + message = Helper.getJSONString("message", null, json); + } throw new LbryResponseException(Helper.isNullOrEmpty(message) ? "The image failed to upload." : message); } } catch (IOException | JSONException | LbryResponseException ex) { @@ -84,16 +89,6 @@ public class UploadImageTask extends AsyncTask { } } - 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/ChannelCreateUpdateTask.java b/app/src/main/java/io/lbry/browser/tasks/claim/ChannelCreateUpdateTask.java similarity index 99% rename from app/src/main/java/io/lbry/browser/tasks/ChannelCreateUpdateTask.java rename to app/src/main/java/io/lbry/browser/tasks/claim/ChannelCreateUpdateTask.java index 96244e23..a21104bd 100644 --- a/app/src/main/java/io/lbry/browser/tasks/ChannelCreateUpdateTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/claim/ChannelCreateUpdateTask.java @@ -1,4 +1,4 @@ -package io.lbry.browser.tasks; +package io.lbry.browser.tasks.claim; import android.os.AsyncTask; import android.view.View; diff --git a/app/src/main/java/io/lbry/browser/tasks/claim/PublishClaimTask.java b/app/src/main/java/io/lbry/browser/tasks/claim/PublishClaimTask.java new file mode 100644 index 00000000..a84ce5c5 --- /dev/null +++ b/app/src/main/java/io/lbry/browser/tasks/claim/PublishClaimTask.java @@ -0,0 +1,99 @@ +package io.lbry.browser.tasks.claim; + +import android.os.AsyncTask; +import android.view.View; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.HashMap; +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 PublishClaimTask extends AsyncTask { + private Claim claim; + private String filePath; + private boolean update; + private View progressView; + private ClaimResultHandler handler; + private Exception error; + public PublishClaimTask(Claim claim, String filePath, boolean update, View progressView, ClaimResultHandler handler) { + this.claim = claim; + this.filePath = filePath; + this.update = update; + this.progressView = progressView; + this.handler = handler; + } + protected void onPreExecute() { + Helper.setViewVisibility(progressView, View.VISIBLE); + if (handler != null) { + handler.beforeStart(); + } + } + protected Claim doInBackground(Void... params) { + Claim.StreamMetadata metadata = (Claim.StreamMetadata) claim.getValue(); + DecimalFormat amountFormat = new DecimalFormat(Helper.SDK_AMOUNT_FORMAT); + + Map options = new HashMap<>(); + options.put("blocking", true); + options.put("name", claim.getName()); + options.put("bid", amountFormat.format(new BigDecimal(claim.getAmount()).doubleValue())); + options.put("title", Helper.isNullOrEmpty(claim.getTitle()) ? "" : claim.getTitle()); + options.put("description", Helper.isNullOrEmpty(claim.getDescription()) ? "" : claim.getDescription()); + options.put("thumbnail_url", Helper.isNullOrEmpty(claim.getThumbnailUrl()) ? "" : claim.getThumbnailUrl()); + + if (!Helper.isNullOrEmpty(filePath)) { + options.put("file_path", filePath); + } + if (claim.getTags() != null && claim.getTags().size() > 0) { + options.put("tags", new ArrayList<>(claim.getTags())); + } + if (metadata.getFee() != null) { + options.put("fee_currency", metadata.getFee().getCurrency()); + options.put("fee_amount", amountFormat.format(new BigDecimal(metadata.getFee().getAmount()).doubleValue())); + } + if (claim.getSigningChannel() != null) { + options.put("channel_id", claim.getSigningChannel().getClaimId()); + } + + // TODO: license, license_url, languages + + Claim claimResult = null; + try { + JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_PUBLISH, options); + if (result.has("outputs")) { + JSONArray outputs = result.getJSONArray("outputs"); + for (int i = 0; i < outputs.length(); i++) { + JSONObject output = outputs.getJSONObject(i); + if (output.has("claim_id") && output.has("claim_op")) { + claimResult = Claim.claimFromOutput(output); + break; + } + } + } + } catch (ApiCallException | ClassCastException | JSONException ex) { + error = ex; + } + + return claimResult; + + } + protected void onPostExecute(Claim result) { + Helper.setViewVisibility(progressView, View.GONE); + if (handler != null) { + if (result != null) { + handler.onSuccess(result); + } else { + handler.onError(error); + } + } + } +} diff --git a/app/src/main/java/io/lbry/browser/tasks/localdata/LoadGalleryItemsTask.java b/app/src/main/java/io/lbry/browser/tasks/localdata/LoadGalleryItemsTask.java index e6a35713..5676f072 100644 --- a/app/src/main/java/io/lbry/browser/tasks/localdata/LoadGalleryItemsTask.java +++ b/app/src/main/java/io/lbry/browser/tasks/localdata/LoadGalleryItemsTask.java @@ -105,7 +105,7 @@ public class LoadGalleryItemsTask extends AsyncTask 0) { - item.setThumbnailUrl(Uri.fromFile(file).toString()); + item.setThumbnailPath(file.getAbsolutePath()); itemsWithThumbnails.add(item); publishProgress(item); } 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 index f87906c1..3f942672 100644 --- a/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/channel/ChannelFormFragment.java @@ -38,6 +38,7 @@ import io.lbry.browser.BuildConfig; import io.lbry.browser.MainActivity; import io.lbry.browser.R; import io.lbry.browser.adapter.TagListAdapter; +import io.lbry.browser.listener.FilePickerListener; import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.listener.WalletBalanceListener; import io.lbry.browser.model.Claim; @@ -46,7 +47,7 @@ import io.lbry.browser.model.Tag; import io.lbry.browser.model.WalletBalance; import io.lbry.browser.tasks.UpdateSuggestedTagsTask; import io.lbry.browser.tasks.UploadImageTask; -import io.lbry.browser.tasks.ChannelCreateUpdateTask; +import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask; import io.lbry.browser.tasks.claim.ClaimResultHandler; import io.lbry.browser.tasks.lbryinc.LogPublishTask; import io.lbry.browser.ui.BaseFragment; @@ -54,11 +55,10 @@ import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Lbry; import io.lbry.browser.utils.LbryAnalytics; import io.lbry.browser.utils.LbryUri; -import io.lbry.browser.utils.Predefined; import lombok.Getter; public class ChannelFormFragment extends BaseFragment implements - StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener { + FilePickerListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener { private static final int SUGGESTED_LIMIT = 8; @@ -289,16 +289,16 @@ public class ChannelFormFragment extends BaseFragment implements } private void validateAndSaveClaim(Claim claim) { + 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 (!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; @@ -368,7 +368,7 @@ public class ChannelFormFragment extends BaseFragment implements private void showError(String message) { Context context = getContext(); if (context != null) { - Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).show(); + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); } } @@ -455,9 +455,9 @@ public class ChannelFormFragment extends BaseFragment implements @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(); + if (getContext() != null) { + showError(getString(R.string.image_upload_failed)); + } if (coverFilePickerActive) { // cover selected imageCover.setImageResource(R.drawable.default_channel_cover); @@ -498,6 +498,8 @@ public class ChannelFormFragment extends BaseFragment implements activity.showNavigationBackIcon(); activity.lockDrawer(); activity.hideFloatingWalletBalance(); + + activity.addFilePickerListener(this); activity.addWalletBalanceListener(this); ActionBar actionBar = activity.getSupportActionBar(); @@ -523,6 +525,7 @@ public class ChannelFormFragment extends BaseFragment implements activity.restoreToggle(); activity.showFloatingWalletBalance(); if (!MainActivity.startingFilePickerActivity) { + activity.removeFilePickerListener(this); activity.removeNavFragment(ChannelFormFragment.class, NavMenuItem.ID_ITEM_CHANNELS); } } diff --git a/app/src/main/java/io/lbry/browser/ui/following/FileViewFragment.java b/app/src/main/java/io/lbry/browser/ui/following/FileViewFragment.java index 9f2bfe50..526bffb5 100644 --- a/app/src/main/java/io/lbry/browser/ui/following/FileViewFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/following/FileViewFragment.java @@ -1249,7 +1249,7 @@ public class FileViewFragment extends BaseFragment implements public void onClick(DialogInterface dialogInterface, int i) { Bundle bundle = new Bundle(); bundle.putString("uri", currentUrl); - bundle.putBoolean("paid", true); + bundle.putString("paid", "true"); bundle.putDouble("amount", Helper.parseDouble(fee.getAmount(), 0)); bundle.putDouble("lbc_amount", cost); bundle.putString("currency", fee.getCurrency()); @@ -1333,6 +1333,7 @@ public class FileViewFragment extends BaseFragment implements // paid is handled differently Bundle bundle = new Bundle(); bundle.putString("uri", currentUrl); + bundle.putString("paid", "false"); LbryAnalytics.logEvent(LbryAnalytics.EVENT_PURCHASE_URI, bundle); } diff --git a/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java b/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java index 69d1e076..705c901d 100644 --- a/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/publish/PublishFormFragment.java @@ -1,11 +1,17 @@ package io.lbry.browser.ui.publish; +import android.Manifest; import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.media.ThumbnailUtils; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.provider.MediaStore; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -14,22 +20,37 @@ import android.view.ViewGroup; import android.webkit.MimeTypeMap; import android.widget.AdapterView; import android.widget.CompoundButton; +import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.AppCompatSpinner; +import androidx.cardview.widget.CardView; +import androidx.core.content.ContextCompat; import androidx.core.widget.NestedScrollView; import androidx.recyclerview.widget.RecyclerView; +import com.arthenica.mobileffmpeg.Config; +import com.arthenica.mobileffmpeg.FFmpeg; +import com.arthenica.mobileffmpeg.FFprobe; + +import com.arthenica.mobileffmpeg.Statistics; +import com.arthenica.mobileffmpeg.StatisticsCallback; +import com.bumptech.glide.Glide; 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.switchmaterial.SwitchMaterial; import com.google.android.material.textfield.TextInputEditText; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.File; +import java.io.FileOutputStream; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -40,19 +61,23 @@ import io.lbry.browser.MainActivity; import io.lbry.browser.R; import io.lbry.browser.adapter.InlineChannelSpinnerAdapter; import io.lbry.browser.adapter.TagListAdapter; +import io.lbry.browser.listener.FilePickerListener; import io.lbry.browser.listener.SdkStatusListener; import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.listener.WalletBalanceListener; import io.lbry.browser.model.Claim; +import io.lbry.browser.model.Fee; import io.lbry.browser.model.GalleryItem; import io.lbry.browser.model.NavMenuItem; import io.lbry.browser.model.Tag; import io.lbry.browser.model.WalletBalance; -import io.lbry.browser.tasks.ChannelCreateUpdateTask; +import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask; import io.lbry.browser.tasks.UpdateSuggestedTagsTask; +import io.lbry.browser.tasks.UploadImageTask; import io.lbry.browser.tasks.claim.ClaimListResultHandler; import io.lbry.browser.tasks.claim.ClaimListTask; import io.lbry.browser.tasks.claim.ClaimResultHandler; +import io.lbry.browser.tasks.claim.PublishClaimTask; import io.lbry.browser.tasks.lbryinc.LogPublishTask; import io.lbry.browser.ui.BaseFragment; import io.lbry.browser.utils.Helper; @@ -60,15 +85,30 @@ import io.lbry.browser.utils.Lbry; import io.lbry.browser.utils.LbryAnalytics; import io.lbry.browser.utils.LbryUri; import io.lbry.browser.utils.Predefined; +import io.lbry.lbrysdk.Utils; +import lombok.Data; +import lombok.Getter; public class PublishFormFragment extends BaseFragment implements - SdkStatusListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener { + FilePickerListener, SdkStatusListener, StoragePermissionListener, TagListAdapter.TagClickListener, WalletBalanceListener { + + private static final String H264_CODEC = "h264"; + private static final int MAX_VIDEO_DIMENSION = 1920; + private static final int MAX_BITRATE = 5000000; // 5mbps private static final int SUGGESTED_LIMIT = 8; private boolean editMode; - private boolean fetchingChannels; + @Getter + private boolean saveInProgress; private String currentFilter; + private boolean publishFileChecked; + private boolean fetchingChannels; + private boolean launchPickerPending; + @Getter + private boolean transcodeInProgress; + private long transcodeStartTime; + private VideoTranscodeTask videoTranscodeTask; private TextInputEditText inputTagFilter; private RecyclerView addedTagsList; @@ -77,6 +117,7 @@ public class PublishFormFragment extends BaseFragment implements private TagListAdapter addedTagsAdapter; private TagListAdapter suggestedTagsAdapter; private TagListAdapter matureTagsAdapter; + private ProgressBar progressPublish; private ProgressBar progressLoadingChannels; private View noTagsView; private View noTagResultsView; @@ -93,6 +134,8 @@ public class PublishFormFragment extends BaseFragment implements private View textNoPrice; private View layoutPrice; private SwitchMaterial switchPrice; + private ImageView imageThumbnail; + private TextView linkGenerateAddress; private TextInputEditText inputTitle; private TextInputEditText inputDescription; @@ -102,7 +145,7 @@ public class PublishFormFragment extends BaseFragment implements private View inlineDepositBalanceContainer; private TextView inlineDepositBalanceValue; - private View linkCancel; + private View linkPublishCancel; private MaterialButton buttonPublish; private View inlineChannelCreator; @@ -114,38 +157,67 @@ public class PublishFormFragment extends BaseFragment implements private View inlineChannelCreatorProgress; private MaterialButton inlineChannelCreatorCreateButton; + private boolean uploading; + private String lastSelectedThumbnailFile; private String uploadedThumbnailUrl; private boolean editFieldsLoaded; private Claim currentClaim; private GalleryItem currentGalleryItem; private String currentFilePath; + private String transcodedFilePath; private boolean fileLoaded; + private View mediaContainer; + private View uploadProgress; + private CardView cardVideoOptimization; + private ProgressBar optimizationRealProgress; + private ProgressBar optimizationProgress; + private TextView textOptimizationProgress; + private TextView textOptimizationStatus; + private TextView textOptimizationElapsed; + + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_publish_form, container, false); scrollView = root.findViewById(R.id.publish_form_scroll_view); progressLoadingChannels = root.findViewById(R.id.publish_form_loading_channels); + progressPublish = root.findViewById(R.id.publish_form_publishing); channelSpinner = root.findViewById(R.id.publish_form_channel_spinner); + mediaContainer = root.findViewById(R.id.publish_form_media_container); inputTagFilter = root.findViewById(R.id.form_tag_filter_input); noTagsView = root.findViewById(R.id.form_no_added_tags); noTagResultsView = root.findViewById(R.id.form_no_tag_results); + inlineDepositBalanceContainer = root.findViewById(R.id.publish_form_inline_balance_container); + inlineDepositBalanceValue = root.findViewById(R.id.publish_form_inline_balance_value); + + cardVideoOptimization = root.findViewById(R.id.publish_form_video_opt_card); + optimizationProgress = root.findViewById(R.id.publish_form_video_opt_progress); + optimizationRealProgress = root.findViewById(R.id.publish_form_video_opt_real_progress); + textOptimizationProgress = root.findViewById(R.id.publish_form_video_opt_progress_text); + textOptimizationStatus = root.findViewById(R.id.publish_form_video_opt_status); + textOptimizationElapsed = root.findViewById(R.id.publish_form_video_opt_elapsed); + layoutExtraFields = root.findViewById(R.id.publish_form_extra_options_container); linkShowExtraFields = root.findViewById(R.id.publish_form_toggle_extra); layoutPrice = root.findViewById(R.id.publish_form_price_container); textNoPrice = root.findViewById(R.id.publish_form_no_price); switchPrice = root.findViewById(R.id.publish_form_price_switch); + uploadProgress = root.findViewById(R.id.publish_form_thumbnail_upload_progress); + imageThumbnail = root.findViewById(R.id.publish_form_thumbnail_preview); + linkGenerateAddress = root.findViewById(R.id.publish_form_generate_address); inputTitle = root.findViewById(R.id.publish_form_input_title); - inputDeposit = root.findViewById(R.id.publish_form_input_description); + inputDescription = root.findViewById(R.id.publish_form_input_description); inputPrice = root.findViewById(R.id.publish_form_input_price); inputAddress = root.findViewById(R.id.publish_form_input_address); inputDeposit = root.findViewById(R.id.publish_form_input_deposit); + priceCurrencySpinner = root.findViewById(R.id.publish_form_currency_spinner); - linkCancel = root.findViewById(R.id.publish_form_cancel); + linkPublishCancel = root.findViewById(R.id.publish_form_cancel); buttonPublish = root.findViewById(R.id.publish_form_publish_button); Context context = getContext(); @@ -192,7 +264,12 @@ public class PublishFormFragment extends BaseFragment implements private void initUi() { - inputAddress.setText(Helper.generateUrl()); + linkGenerateAddress.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + inputAddress.setText(Helper.generateUrl()); + } + }); switchPrice.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override @@ -228,13 +305,10 @@ public class PublishFormFragment extends BaseFragment implements } }); - linkCancel.setOnClickListener(new View.OnClickListener() { + mediaContainer.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - Context context = getContext(); - if (context instanceof MainActivity) { - ((MainActivity) context).onBackPressed(); - } + checkStoragePermissionAndLaunchFilePicker(); } }); @@ -278,10 +352,64 @@ public class PublishFormFragment extends BaseFragment implements } }); + linkPublishCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (transcodeInProgress) { + // show alert confirming the user is sure, and then cancel + FFmpeg.cancel(); + transcodeInProgress = false; + } + + Context context = getContext(); + if (context instanceof MainActivity) { + ((MainActivity) context).onBackPressed(); + } + } + }); + buttonPublish.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { + if (uploading) { + Snackbar.make(view, R.string.publish_no_thumbnail, Snackbar.LENGTH_LONG).show(); + return; + } else if (Helper.isNullOrEmpty(uploadedThumbnailUrl)) { + showError(getString(R.string.publish_thumbnail_in_progress)); + return; + } + // check minimum deposit + 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 < Helper.MIN_DEPOSIT) { + 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; + } + + String priceString = Helper.getValue(inputPrice.getText()); + double priceAmount = Helper.parseDouble(priceString, 0); + if (switchPrice.isChecked() && priceAmount == 0) { + showError(getString(R.string.price_amount_not_set)); + return; + } + + Claim claim = buildPublishClaim(); + if (validatePublishClaim(claim)) { + publishClaim(claim); + } } }); @@ -306,6 +434,8 @@ public class PublishFormFragment extends BaseFragment implements activity.showNavigationBackIcon(); activity.lockDrawer(); activity.hideFloatingWalletBalance(); + + activity.addFilePickerListener(this); activity.addWalletBalanceListener(this); ActionBar actionBar = activity.getSupportActionBar(); @@ -320,13 +450,18 @@ public class PublishFormFragment extends BaseFragment implements Context context = getContext(); if (context instanceof MainActivity) { MainActivity activity = (MainActivity) getContext(); - activity.removeWalletBalanceListener(this); activity.restoreToggle(); activity.showFloatingWalletBalance(); if (!MainActivity.startingFilePickerActivity) { + activity.removeWalletBalanceListener(this); + activity.removeFilePickerListener(this); activity.removeNavFragment(PublishFormFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH); + if (transcodeInProgress) { + FFmpeg.cancel(); + } } } + super.onStop(); } @@ -352,6 +487,33 @@ public class PublishFormFragment extends BaseFragment implements } } + private void checkStoragePermissionAndLaunchFilePicker() { + Context context = getContext(); + if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) { + launchPickerPending = false; + launchFilePicker(); + } else { + launchPickerPending = true; + 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) { + MainActivity.startingFilePickerActivity = true; + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("image/*"); + ((MainActivity) context).startActivityForResult( + Intent.createChooser(intent, getString(R.string.select_thumbnail)), + MainActivity.REQUEST_FILE_PICKER); + } + } + private void updateFieldsFromCurrentClaim() { if (currentClaim != null && !editFieldsLoaded) { @@ -360,15 +522,20 @@ public class PublishFormFragment extends BaseFragment implements } private void checkPublishFile() { + if (publishFileChecked) { + return; + } + String filePath = ""; + String thumbnailPath = null; if (currentGalleryItem != null) { // check gallery item type filePath = currentGalleryItem.getFilePath(); + thumbnailPath = currentGalleryItem.getThumbnailPath(); } else if (currentFilePath != null) { filePath = currentFilePath; } - android.util.Log.d("#HELP", "filePath=" + filePath); File file = new File(filePath); if (!file.exists()) { // file doesn't exist. although this shouldn't happen @@ -376,19 +543,190 @@ public class PublishFormFragment extends BaseFragment implements return; } - - // check content type String type = null; String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString()); if (extension != null) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } - android.util.Log.d("#HELP", "fileType=" + type); + boolean isVideo = false; + boolean isImage = !Helper.isNullOrEmpty(type) && type.startsWith("image"); if (!Helper.isNullOrEmpty(type) && type.startsWith("video")) { // ffmpeg video handling + isVideo = true; + if (!transcodeInProgress) { + probeVideo(filePath); + } } + + if (isVideo || isImage) { + checkAndUploadThumbnail(filePath, thumbnailPath, isVideo ? "video" : "image"); + } + + Helper.setViewVisibility(cardVideoOptimization, isVideo ? View.VISIBLE : View.GONE); + + publishFileChecked = true; + } + + private void checkAndUploadThumbnail(String filePath, String thumbnailPath, String type) { + if (Helper.isNullOrEmpty(thumbnailPath)) { + createAndUploadThumbnail(filePath, type); + } else { + uploadThumbnail(thumbnailPath); + } + } + + private void createAndUploadThumbnail(String filePath, String type) { + Context context = getContext(); + CreateThumbnailTask task = new CreateThumbnailTask(filePath, type, context, new CreateThumbnailTask.CreateThumbnailHandler() { + @Override + public void onSuccess(String thumbnailPath) { + uploadThumbnail(thumbnailPath); + } + + @Override + public void onError(Exception error) { + if (context != null) { + showError(getString(R.string.thumbnail_creation_failed)); + } + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void uploadThumbnail(String thumbnailPath) { + if (uploading) { + Snackbar.make(getView(), R.string.wait_for_upload, Snackbar.LENGTH_LONG).show(); + return; + } + + Context context = getContext(); + if (context != null) { + Glide.with(context.getApplicationContext()).load(thumbnailPath).centerCrop().into(imageThumbnail); + } + + uploading = true; + uploadedThumbnailUrl = null; + UploadImageTask task = new UploadImageTask(thumbnailPath, uploadProgress, new UploadImageTask.UploadThumbnailHandler() { + @Override + public void onSuccess(String url) { + lastSelectedThumbnailFile = thumbnailPath; + uploadedThumbnailUrl = url; + uploading = false; + } + + @Override + public void onError(Exception error) { + View view = getView(); + if (context != null && view != null) { + showError(getString(R.string.image_upload_failed)); + } + lastSelectedThumbnailFile = null; + imageThumbnail.setImageDrawable(null); + uploading = false; + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void probeVideo(String filePath) { + VideoProbeTask task = new VideoProbeTask(filePath, new VideoProbeTask.VideoProbeHandler() { + @Override + public void onVideoProbed(VideoInformation result) { + checkAndTranscodeVideo(filePath, result); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + } + + private void checkAndTranscodeVideo(String filePath, VideoInformation videoInformation) { + boolean transcodeRequired = (videoInformation == null || + !H264_CODEC.equalsIgnoreCase(videoInformation.getCodecName()) || + MAX_VIDEO_DIMENSION < videoInformation.getWidth() || MAX_VIDEO_DIMENSION < videoInformation.getHeight() || + MAX_BITRATE < videoInformation.getBitrate()); + + String scalePart = ""; + if (videoInformation != null) { + // check the max dimension that we need to scale + int videoWidth = videoInformation.getWidth(); + int videoHeight = videoInformation.getHeight(); + // get the highest dimension + int maxDimension = Math.max(videoWidth, videoHeight); + if (maxDimension > MAX_VIDEO_DIMENSION) { + scalePart = maxDimension == videoWidth ? String.format("-vf scale=%d:-2", MAX_VIDEO_DIMENSION) : String.format("-vf scale=-2:%d", MAX_VIDEO_DIMENSION); + } + } + + Context context = getContext(); + String outputPath = String.format("%s/videos", Utils.getAppInternalStorageDir(context)); + File dir = new File(outputPath); + if (!dir.isDirectory()) { + dir.mkdirs(); + } + + boolean hasFullDuration = videoInformation != null && videoInformation.getDurationSeconds() > 0; + Helper.setViewVisibility(optimizationRealProgress, hasFullDuration ? View.VISIBLE : View.GONE); + Helper.setViewVisibility(optimizationProgress, hasFullDuration ? View.GONE : View.VISIBLE); + + File sourceFile = new File(filePath); + String filename = sourceFile.getName(); + if (!filename.endsWith(".mp4")) { + int lastDotIndex = filename.lastIndexOf('.'); + filename = String.format("%s.mp4", lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename); + } + + String videoFilePath = String.format("%s/%s", outputPath, filename); + File targetFile = new File(videoFilePath); + if (targetFile.exists()) { + targetFile.delete(); + } + + transcodeInProgress = true; + videoTranscodeTask = new VideoTranscodeTask(filePath, videoFilePath, scalePart, transcodeRequired, new VideoTranscodeTask.VideoTranscodeHandler() { + @Override + public void onProgress(int time) { + if (context != null) { + int currentDuration = Double.valueOf(time / 1000.0).intValue(); + int fullDuration = videoInformation != null ? videoInformation.getDurationSeconds() : 0; + long elapsed = System.currentTimeMillis() - transcodeStartTime; + String completedDurationText = Helper.formatDuration(currentDuration); + if (fullDuration > 0) { + completedDurationText = String.format("%s / %s", completedDurationText, Helper.formatDuration(fullDuration)); + int percentComplete = Double.valueOf(Math.ceil((double) currentDuration / (double) fullDuration * 100.0)).intValue(); + optimizationRealProgress.setProgress(percentComplete); + } + + + String text = context.getString(R.string.completed_video_duration, completedDurationText); + Helper.setViewText(textOptimizationProgress, text); + Helper.setViewText(textOptimizationElapsed, Helper.formatDuration(Double.valueOf(elapsed / 1000.0).longValue())); + } + } + + @Override + public void onSuccess(String outputFilePath) { + transcodedFilePath = outputFilePath; + transcodeInProgress = false; + Helper.setViewText(textOptimizationStatus, R.string.video_optimized); + Helper.setViewVisibility(optimizationRealProgress, View.GONE); + Helper.setViewVisibility(optimizationProgress, View.GONE); + Helper.setViewVisibility(textOptimizationProgress, View.GONE); + } + + @Override + public void onErrorOrCancelled() { + transcodeInProgress = false; + Helper.setViewText(textOptimizationStatus, R.string.video_optimize_failed); + Helper.setViewVisibility(optimizationRealProgress, View.GONE); + Helper.setViewVisibility(optimizationProgress, View.GONE); + Helper.setViewVisibility(textOptimizationProgress, View.GONE); + } + }); + + transcodeStartTime = System.currentTimeMillis(); + videoTranscodeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private void cancelOnFatalCondition(String message) { @@ -415,7 +753,7 @@ public class PublishFormFragment extends BaseFragment implements checkParams(); updateFieldsFromCurrentClaim(); - if (currentClaim == null && (currentGalleryItem != null || (Helper.isNullOrEmpty(currentFilePath)))) { + if (currentClaim == null && (currentGalleryItem != null || !Helper.isNullOrEmpty(currentFilePath))) { // load file information checkPublishFile(); } @@ -533,11 +871,128 @@ public class PublishFormFragment extends BaseFragment implements private Claim buildPublishClaim() { Claim claim = new Claim(); + claim.setName(Helper.getValue(inputAddress.getText())); + claim.setAmount(Helper.getValue(inputDeposit.getText())); + + Claim.StreamMetadata metadata = new Claim.StreamMetadata(); + metadata.setTitle(Helper.getValue(inputTitle.getText())); + metadata.setDescription(Helper.getValue(inputDescription.getText())); + metadata.setTags(Helper.getTagsForTagObjects(addedTagsAdapter.getTags())); + + Claim selectedChannel = (Claim) channelSpinner.getSelectedItem(); + if (selectedChannel != null && !selectedChannel.isPlaceholder() && !selectedChannel.isPlaceholderAnonymous()) { + claim.setSigningChannel(selectedChannel); + } + if (switchPrice.isChecked()) { + Fee fee = new Fee(); + fee.setCurrency((String) priceCurrencySpinner.getSelectedItem()); + fee.setAmount(Helper.getValue(inputPrice.getText())); + metadata.setFee(fee); + } + + if (!Helper.isNullOrEmpty(uploadedThumbnailUrl)) { + Claim.Resource thumbnail = new Claim.Resource(); + thumbnail.setUrl(uploadedThumbnailUrl); + metadata.setThumbnail(thumbnail); + } + + // TODO: License, LicenseDescription, LicenseUrl, Language + claim.setValueType(Claim.TYPE_STREAM); + claim.setValue(metadata); + return claim; } - private boolean validatePublishClaim() { - return false; + private boolean validatePublishClaim(Claim claim) { + if (Helper.isNullOrEmpty(claim.getTitle())) { + showError(getString(R.string.please_provide_title)); + return false; + } + if (Helper.isNullOrEmpty(claim.getName())) { + showError(getString(R.string.please_specify_address)); + return false; + } + if (!LbryUri.isNameValid(claim.getName())) { + showError(getString(R.string.address_invalid_characters)); + return false; + } + if (Helper.claimNameExists(claim.getName())) { + showError(getString(R.string.address_already_used)); + return false; + } + + String publishFilePath = currentGalleryItem != null ? currentGalleryItem.getFilePath() : currentFilePath; + if (Helper.isNullOrEmpty(publishFilePath) && Helper.isNullOrEmpty(transcodedFilePath)) { + showError(getString(R.string.no_file_selected)); + return false; + } + + return true; + } + + private void publishClaim(Claim claim) { + String finalFilePath = transcodedFilePath; + if (Helper.isNullOrEmpty(finalFilePath)) { + finalFilePath = currentGalleryItem != null ? currentGalleryItem.getFilePath() : currentFilePath; + } + saveInProgress = true; + PublishClaimTask task = new PublishClaimTask(claim, finalFilePath, editMode, progressPublish, new ClaimResultHandler() { + @Override + public void beforeStart() { + preSave(); + } + + @Override + public void onSuccess(Claim claimResult) { + postSave(); + + android.util.Log.d("#HELP", claimResult.toString()); + + // Run the logPublish task + if (!BuildConfig.DEBUG) { + LogPublishTask logPublish = new LogPublishTask(claimResult); + logPublish.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + // publish done + Bundle bundle = new Bundle(); + bundle.putString("claim_id", claimResult.getClaimId()); + bundle.putString("claim_name", claimResult.getName()); + LbryAnalytics.logEvent(LbryAnalytics.EVENT_PUBLISH, bundle); + + Context context = getContext(); + if (context instanceof MainActivity) { + MainActivity activity = (MainActivity) context; + activity.showMessage(R.string.publish_successful); + activity.openPublishesOnSuccessfulPublish(); + } + } + + @Override + public void onError(Exception error) { + showError(error.getMessage()); + postSave(); + } + }); + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void preSave() { + saveInProgress = true; + + // disable input views + + Helper.setViewEnabled(linkShowExtraFields, false); + Helper.setViewEnabled(linkPublishCancel, false); + Helper.setViewEnabled(buttonPublish, false); + } + + private void postSave() { + Helper.setViewEnabled(linkShowExtraFields, true); + Helper.setViewEnabled(linkPublishCancel, true); + Helper.setViewEnabled(buttonPublish, true); + + saveInProgress = false; } @@ -747,17 +1202,270 @@ public class PublishFormFragment extends BaseFragment implements private void showError(String message) { Context context = getContext(); if (context != null) { - Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).setBackgroundTint(Color.RED).show(); + Snackbar.make(getView(), message, Snackbar.LENGTH_LONG). + setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show(); } } + private void checkUploadButton() { + + } + @Override public void onStoragePermissionGranted() { - + if (launchPickerPending) { + launchPickerPending = false; + launchFilePicker(); + } } @Override public void onStoragePermissionRefused() { + showError(getString(R.string.storage_permission_rationale_images)); + launchPickerPending = false; + } + @Override + 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; + } + + android.util.Log.d("#HELP", "FilePicked: " + filePath); + Context context = getContext(); + if (context != null) { + if (filePath.equalsIgnoreCase(lastSelectedThumbnailFile)) { + // previous selected cover was uploaded successfully + android.util.Log.d("#HELP", "lastSelectedThumbnailFile the same"); + return; + } + android.util.Log.d("#HELP", "PickedFilePath=" + filePath); + + Uri fileUri = Uri.fromFile(new File(filePath)); + Glide.with(context.getApplicationContext()).load(fileUri).centerCrop().into(imageThumbnail); + uploadThumbnail(filePath); + } + } + + @Override + public void onFilePickerCancelled() { + // nothing to do here + // At some point in the future, allow file picking for publish file? + } + + private static class VideoProbeTask extends AsyncTask { + private String filePath; + private VideoProbeHandler handler; + public VideoProbeTask(String filePath, VideoProbeHandler handler) { + this.filePath = filePath; + this.handler = handler; + } + protected VideoInformation doInBackground(Void... params) { + try { + int code = FFprobe.execute(String.format("-v quiet -show_streams -select_streams v -print_format json -i \"%s\"", filePath)); + if (code == Config.RETURN_CODE_SUCCESS) { + String json = Config.getLastCommandOutput(); + JSONObject result = new JSONObject(json); + if (result.has("streams")) { + JSONArray streams = result.getJSONArray("streams"); + if (streams.length() > 0) { + JSONObject stream = streams.getJSONObject(0); + VideoInformation videoInformation = VideoInformation.fromJSONObject(stream); + return videoInformation; + } + } + } + } catch (JSONException ex) { + // pass + } + return null; + } + protected void onPostExecute(VideoInformation result) { + if (handler != null) { + handler.onVideoProbed(result); + } + } + + public interface VideoProbeHandler { + void onVideoProbed(VideoInformation result); + } + } + + private static class VideoTranscodeTask extends AsyncTask { + + private String filePath; + private String scaleFlag; + private String outputFilePath; + private boolean transcodeRequired; + private VideoTranscodeHandler handler; + + public VideoTranscodeTask(String filePath, String outputFilePath, String scaleFlag, boolean transcodeRequired, VideoTranscodeHandler handler) { + this.handler = handler; + this.filePath = filePath; + this.outputFilePath = outputFilePath; + this.scaleFlag = scaleFlag; + this.transcodeRequired = transcodeRequired; + } + + protected Boolean doInBackground(Void... params) { + String movFlagsCommand = String.format("-i \"%s\" -movflags +faststart \"%s\"", filePath, outputFilePath); + String command = transcodeRequired ? String.format( + "-i \"%s\" " + + "-c:v libx264 " + + "-c:a aac -b:a 128k " + + "%s " + + "-crf 27 -preset ultrafast " + + "-pix_fmt yuv420p " + + "-maxrate 5000K -bufsize 5000K " + + "-movflags +faststart \"%s\"", filePath, scaleFlag, outputFilePath) : movFlagsCommand; + android.util.Log.d("#HELP", command); + + Config.enableStatisticsCallback(new StatisticsCallback() { + @Override + public void apply(Statistics statistics) { + publishProgress(statistics.getTime()); + } + }); + int code = FFmpeg.execute(command); + return code == Config.RETURN_CODE_SUCCESS; + } + + protected void onProgressUpdate(Integer... times) { + if (handler != null) { + for (Integer time : times) { + handler.onProgress(time); + } + } + } + + protected void onPostExecute(Boolean result) { + if (handler != null) { + if (result) { + handler.onSuccess(outputFilePath); + } else { + handler.onErrorOrCancelled(); + } + } + } + + public interface VideoTranscodeHandler { + void onProgress(int time); + void onSuccess(String outputFilePath); + void onErrorOrCancelled(); + } + } + + @Data + private static class VideoInformation { + private String codecName; + private int width; + private int height; + private int durationSeconds; + private long bitrate; + + private static int tryParseDuration(JSONObject streamObject) { + String durationString = Helper.getJSONString("duration", "0", streamObject); + double parsedDuration = Helper.parseDouble(durationString, 0); + if (parsedDuration > 0) { + return Double.valueOf(parsedDuration).intValue(); + } + + try { + if (streamObject.has("tags") && !streamObject.isNull("tags")) { + JSONObject tags = streamObject.getJSONObject("tags"); + String tagDurationString = Helper.getJSONString("DURATION", null, tags); + if (Helper.isNull(tagDurationString)) { + tagDurationString = Helper.getJSONString("duration", null, tags); + } + if (!Helper.isNullOrEmpty(tagDurationString) && tagDurationString.indexOf(':') > -1) { + String[] parts = tagDurationString.split(":"); + if (parts.length == 3) { + int hours = Helper.parseInt(parts[0], 0); + int minutes = Helper.parseInt(parts[1], 0); + int seconds = Helper.parseDouble(parts[2], 0).intValue(); + return (hours * 60 * 60) + (minutes * 60) + seconds; + } + } + + } + } catch (JSONException ex) { + return 0; + } + + return 0; + } + + public static VideoInformation fromJSONObject(JSONObject streamObject) { + VideoInformation info = new VideoInformation(); + info.setCodecName(Helper.getJSONString("codec_name", null, streamObject)); + info.setWidth(Helper.getJSONInt("width", 0, streamObject)); + info.setHeight(Helper.getJSONInt("height", 0, streamObject)); + info.setBitrate(Helper.getJSONLong("bit_rate", 0, streamObject)); + info.setDurationSeconds(tryParseDuration(streamObject)); + + return info; + } + } + + private static class CreateThumbnailTask extends AsyncTask { + private Context context; + private String filePath; + private String type; + private CreateThumbnailHandler handler; + private Exception error; + public CreateThumbnailTask(String filePath, String type, Context context, CreateThumbnailHandler handler) { + this.context = context; + this.type = type; + this.filePath = filePath; + this.handler = handler; + } + protected String doInBackground(Void... params) { + String thumbnailPath = null; + FileOutputStream os = null; + Bitmap thumbnail = null; + try { + File cacheDir = context.getExternalCacheDir(); + File thumbnailsDir = new File(String.format("%s/thumbnails", cacheDir.getAbsolutePath())); + if (!thumbnailsDir.isDirectory()) { + thumbnailsDir.mkdirs(); + } + + // save the thumbnail to the path + thumbnailPath = String.format("%s/%s.png", thumbnailsDir.getAbsolutePath(), Helper.makeid(8)); + if ("video".equals(type)) { + thumbnail = ThumbnailUtils.createVideoThumbnail(filePath, MediaStore.Video.Thumbnails.MINI_KIND); + } else { + Bitmap source = BitmapFactory.decodeFile(filePath); + // MINI_KIND dimensions + thumbnail = Bitmap.createScaledBitmap(source, 512, 384, false); + } + + os = new FileOutputStream(thumbnailPath); + thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os); + } catch (Exception ex) { + error = ex; + return null; + } finally { + Helper.closeCloseable(os); + } + + return thumbnailPath; + } + protected void onPostExecute(String thumbnailPath) { + if (handler != null) { + if (!Helper.isNullOrEmpty(thumbnailPath)) { + handler.onSuccess(thumbnailPath); + } else { + handler.onError(error); + } + } + } + + public interface CreateThumbnailHandler { + void onSuccess(String thumbnailPath); + void onError(Exception error); + } } } diff --git a/app/src/main/java/io/lbry/browser/ui/publish/PublishFragment.java b/app/src/main/java/io/lbry/browser/ui/publish/PublishFragment.java index 91e386da..4a837570 100644 --- a/app/src/main/java/io/lbry/browser/ui/publish/PublishFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/publish/PublishFragment.java @@ -3,6 +3,7 @@ package io.lbry.browser.ui.publish; import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.AsyncTask; @@ -38,6 +39,7 @@ import io.lbry.browser.MainActivity; import io.lbry.browser.R; import io.lbry.browser.adapter.GalleryGridAdapter; import io.lbry.browser.listener.CameraPermissionListener; +import io.lbry.browser.listener.FilePickerListener; import io.lbry.browser.listener.StoragePermissionListener; import io.lbry.browser.model.GalleryItem; import io.lbry.browser.model.NavMenuItem; @@ -47,9 +49,9 @@ import io.lbry.browser.utils.Helper; import io.lbry.browser.utils.Lbry; import io.lbry.browser.utils.LbryAnalytics; -public class PublishFragment extends BaseFragment implements CameraPermissionListener, StoragePermissionListener { +public class PublishFragment extends BaseFragment implements + CameraPermissionListener, FilePickerListener, StoragePermissionListener { - private boolean loadGalleryItemsPending; private PreviewView cameraPreview; private RecyclerView galleryGrid; private GalleryGridAdapter adapter; @@ -60,6 +62,8 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis private View buttonTakePhoto; private View buttonUpload; + private boolean loadGalleryItemsPending; + private boolean launchFilePickerPending; private boolean recordPending; private boolean takePhotoPending; private ListenableFuture cameraProviderFuture; @@ -95,6 +99,12 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis checkCameraPermissionAndTakePhoto(); } }); + buttonUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + checkStoragePermissionAndLaunchFilePicker(); + } + }); return root; } @@ -137,6 +147,11 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis } private void checkCameraPermissionAndRecord() { + if (!Lbry.SDK_READY) { + Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show(); + return; + } + Context context = getContext(); if (!MainActivity.hasPermission(Manifest.permission.CAMERA, context)) { recordPending = true; @@ -147,11 +162,16 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis context, true); } else { - // start video record intent + record(); } } private void checkCameraPermissionAndTakePhoto() { + if (!Lbry.SDK_READY) { + Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show(); + return; + } + Context context = getContext(); if (!MainActivity.hasPermission(Manifest.permission.CAMERA, context)) { takePhotoPending = true; @@ -161,8 +181,56 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis getString(R.string.camera_permission_rationale_photo), context, true); - } else { - // start video record intent + } else { + takePhoto(); + } + } + + private void takePhoto() { + Context context = getContext(); + if (context instanceof MainActivity) { + takePhotoPending = false; + ((MainActivity) context).requestTakePhoto(); + } + } + + private void record() { + Context context = getContext(); + if (context instanceof MainActivity) { + recordPending = false; + ((MainActivity) context).requestVideoCapture(); + } + } + + private void checkStoragePermissionAndLaunchFilePicker() { + if (!Lbry.SDK_READY) { + Snackbar.make(getView(), R.string.sdk_initializing_functionality, Snackbar.LENGTH_LONG).show(); + return; + } + + Context context = getContext(); + if (MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context)) { + launchFilePickerPending = false; + launchFilePicker(); + } else { + launchFilePickerPending = true; + 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) { + MainActivity.startingFilePickerActivity = true; + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + ((MainActivity) context).startActivityForResult( + Intent.createChooser(intent, getString(R.string.upload_file)), + MainActivity.REQUEST_FILE_PICKER); } } @@ -173,6 +241,7 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis MainActivity activity = (MainActivity) context; LbryAnalytics.setCurrentScreen(activity, "Publish", "Publish"); activity.addCameraPermissionListener(this); + activity.addFilePickerListener(this); activity.addStoragePermissionListener(this); activity.hideFloatingWalletBalance(); @@ -193,6 +262,9 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis activity.removeCameraPermissionListener(this); activity.removeStoragePermissionListener(this); activity.showFloatingWalletBalance(); + if (!MainActivity.startingFilePickerActivity) { + activity.removeFilePickerListener(this); + } } CameraX.unbindAll(); super.onStop(); @@ -276,10 +348,10 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis public void onCameraPermissionGranted() { if (recordPending) { // record video - recordPending = false; + record(); } else if (takePhotoPending) { // take a photo - takePhotoPending = false; + takePhoto(); } } @@ -313,6 +385,10 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis loadGalleryItemsPending = false; loadGalleryItems(); } + if (launchFilePickerPending) { + launchFilePickerPending = false; + launchFilePicker(); + } } @Override @@ -331,4 +407,19 @@ public class PublishFragment extends BaseFragment implements CameraPermissionLis public boolean shouldSuspendGlobalPlayer() { return true; } + + @Override + public void onFilePicked(String filePath) { + Context context = getContext(); + if (context instanceof MainActivity) { + Map params = new HashMap<>(); + params.put("directFilePath", filePath); + ((MainActivity) context).openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params); + } + } + + @Override + public void onFilePickerCancelled() { + + } } diff --git a/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java b/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java index 59618f21..13f57273 100644 --- a/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java +++ b/app/src/main/java/io/lbry/browser/ui/wallet/InvitesFragment.java @@ -41,7 +41,7 @@ import io.lbry.browser.model.lbryinc.Invitee; import io.lbry.browser.tasks.claim.ClaimListResultHandler; import io.lbry.browser.tasks.claim.ClaimListTask; import io.lbry.browser.tasks.GenericTaskHandler; -import io.lbry.browser.tasks.ChannelCreateUpdateTask; +import io.lbry.browser.tasks.claim.ChannelCreateUpdateTask; import io.lbry.browser.tasks.claim.ClaimResultHandler; import io.lbry.browser.tasks.lbryinc.LogPublishTask; import io.lbry.browser.tasks.lbryinc.FetchInviteStatusTask; 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 1eb96d98..11546161 100644 --- a/app/src/main/java/io/lbry/browser/utils/Helper.java +++ b/app/src/main/java/io/lbry/browser/utils/Helper.java @@ -37,6 +37,7 @@ import java.io.Closeable; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -67,12 +68,13 @@ 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 final double MIN_DEPOSIT = 0.01; 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 FULL_LBC_CURRENCY_FORMAT = new DecimalFormat("#,###.########"); public static final DecimalFormat SIMPLE_CURRENCY_FORMAT = new DecimalFormat("#,##0.00"); + public static final SimpleDateFormat FILESTAMP_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss"); public static final String EXPLORER_TX_PREFIX = "https://explorer.lbry.com/tx"; public static boolean isNull(String value) { @@ -438,6 +440,14 @@ public final class Helper { } return false; } + public static boolean claimNameExists(String claimName) { + for (Claim claim : Lbry.ownClaims) { + if (claimName.equalsIgnoreCase(claim.getName())) { + return true; + } + } + return false; + } public static String getRealPathFromURI_API19(final Context context, final Uri uri) { return getRealPathFromURI_API19(context, uri, false); @@ -718,4 +728,14 @@ public final class Helper { rv.setAdapter(adapter); rv.scrollToPosition(prevScrollPosition > 0 ? prevScrollPosition : 0); } + + public static String makeid(int length) { + Random random = new Random(); + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + StringBuilder id = new StringBuilder(); + for (int i = 0; i < length; i++) { + id.append(chars.charAt(random.nextInt(chars.length()))); + } + return id.toString(); + } } 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 e272d943..ec2ed59d 100644 --- a/app/src/main/java/io/lbry/browser/utils/Lbry.java +++ b/app/src/main/java/io/lbry/browser/utils/Lbry.java @@ -65,6 +65,7 @@ public final class Lbry { public static final String METHOD_FILE_LIST = "file_list"; public static final String METHOD_FILE_DELETE = "file_delete"; public static final String METHOD_GET = "get"; + public static final String METHOD_PUBLISH = "publish"; public static final String METHOD_WALLET_BALANCE = "wallet_balance"; public static final String METHOD_WALLET_ENCRYPT = "wallet_encrypt"; diff --git a/app/src/main/res/layout/fragment_publish_form.xml b/app/src/main/res/layout/fragment_publish_form.xml index 48ba5905..52b57419 100644 --- a/app/src/main/res/layout/fragment_publish_form.xml +++ b/app/src/main/res/layout/fragment_publish_form.xml @@ -117,6 +117,82 @@ + + + + + + + + + + + + + + + + + + - + + + + + + Price Your content will be free. Press the toggle to set a price. Content address + Randomize Address The address where people can find your content (ex. lbry://myvideo) License @@ -115,7 +116,26 @@ Show extra fields Hide extra fields No file found to publish. - You cannot publish content right now beceause the background service is still initializing. + Video optimization + A thumbnail could not be automatically created from your content file. + Your video is being optimized for better support on a wide range of devices. You can fill out the remaining fields below while this is in progress. + Your video was successfully optimized for better playback across as many devices as possible. Please proceed to publish your content. + Your video could not be optimized. The file will be uploaded with no changes. + Completed Video Duration: %1$s + You cannot publish content right now because the background service is still initializing. + Your content was successfully published. It may take a few moments to appear on the blockchain. + Video optimization is in progress. If you wish to cancel, press Cancel at the bottom of the page. + There is no camera app available to record videos on this device. + There is no camera app available to take photos on this device. + + Please provide a title. + Please specify an address where people can find your content. + Your content address contains invalid characters. + You have already published to the specified content address. Please enter a new address. + No file selected. Please choose a video or take a photo, or select a file before publishing. + Please enter a price or turn off the toggle to make your content free. + Please select a thumbnail to upload before publishing. + Please wait for the thumbnail to finish uploading before publishing. Language English diff --git a/app/src/main/res/xml/filepaths.xml b/app/src/main/res/xml/filepaths.xml index a6fad460..72bd94c2 100644 --- a/app/src/main/res/xml/filepaths.xml +++ b/app/src/main/res/xml/filepaths.xml @@ -1,4 +1,6 @@ + + \ No newline at end of file