diff --git a/.gitignore b/.gitignore
index 02f7d626..c21e214d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,7 +60,6 @@ buck-out/
# Other Files
app/google-services.json
-app/src/main/assets/index.android.bundle
*.log
.vagrant
*.hprof
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 285c216d..33fca8b4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -3,18 +3,12 @@ stages:
- deploy
- release
-variables:
- REACT_NATIVE_BRANCH: "master"
build apk:
stage: build
image: lbry/android-base:platform-28
before_script:
- apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign
- - wget -q https://nodejs.org/dist/latest-v10.x/node-v10.19.0-linux-x64.tar.gz && tar xf node-v10.19.0-linux-x64.tar.gz -C /opt
- - ln -fs /opt/node-v10.19.0-linux-x64/bin/node /usr/bin/node
- - ln -fs /opt/node-v10.19.0-linux-x64/bin/npm /usr/bin/npm
- - ln -fs /opt/node-v10.19.0-linux-x64/bin/npx /usr/bin/npx
- chmod u+x $CI_PROJECT_DIR/gradlew
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1)
@@ -26,25 +20,13 @@ build apk:
script:
- export PATH=/usr/bin:$PATH
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import
- - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
- - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
- echo "deb https://dl.bintray.com/sobolevn/deb git-secret main" | tee -a /etc/apt/sources.list
- wget -O - https://api.bintray.com/users/sobolevn/keys/gpg/public.key | apt-key add -
- - apt-get -y update && apt-get -y install yarn git-secret
+ - apt-get -y update && apt-get -y install git-secret
- git secret reveal
- - npm install -g react-native-cli
- - cd ~/
- - git clone --single-branch --branch $REACT_NATIVE_BRANCH https://github.com/lbryio/lbry-react-native
- - cd lbry-react-native
- - chmod u+x bundle-android.sh
- yarn
- - rm -rf android # temporary, should be a submodule init?
- - cp -rf $CI_PROJECT_DIR/ android/
- - cd android
- chmod u+x ./release.sh
- ./release.sh
- - mkdir -p $CI_PROJECT_DIR/bin/ && cp bin/*.apk $CI_PROJECT_DIR/bin/
- - cd $CI_PROJECT_DIR
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 9c973bf1..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,33 +0,0 @@
-FROM thyrlian/android-sdk
-
-## Dependencies to run as root:
-ENV DEBIAN_FRONTEND=noninteractive
-RUN dpkg --add-architecture i386 && \
- apt-get -y update && \
- apt-get install -y \
- curl ca-certificates software-properties-common gpg-agent wget \
- python3.7 python3.7-dev python3-pip python2.7 python2.7-dev python3.7-venv \
- python-pip zlib1g-dev m4 zlib1g:i386 libc6-dev-i386 gawk nodejs npm unzip openjdk-8-jdk \
- autoconf autogen automake libtool libffi-dev build-essential \
- ccache git libncurses5:i386 libstdc++6:i386 \
- libgtk2.0-0:i386 libpangox-1.0-0:i386 libpangoxft-1.0-0:i386 libidn11:i386
-RUN npm install -g npm@latest
-RUN npm install -g yarn react-native-cli && \
- pip2 install --upgrade cython setuptools && \
- pip2 install git+https://github.com/lbryio/buildozer.git@master && \
- ln -s /src/scripts/build-docker.sh /usr/local/bin/build && \
- adduser lbry-android --gecos GECOS --shell /bin/bash --disabled-password --home /home/lbry-android && \
- mkdir /home/lbry-android/.npm-packages && \
- echo "prefix=/home/lbry-android/.npm-packages" > /home/lbry-android/.npmrc && \
- chown -R lbry-android:lbry-android /home/lbry-android && \
- mkdir /src && \
- chown lbry-android:lbry-android /src && \
- mkdir /dist && \
- chown lbry-android:lbry-android /dist
-
-## Further setup done by lbry-android user:
-USER lbry-android
-
-COPY scripts/docker-build.sh /home/lbry-android/bin/build
-COPY scripts/docker-setup.sh /home/lbry-android/bin/setup
-CMD ["/home/lbry-android/bin/build"]
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/BUCK b/app/BUCK
deleted file mode 100644
index 3f7eb728..00000000
--- a/app/BUCK
+++ /dev/null
@@ -1,55 +0,0 @@
-# To learn about Buck see [Docs](https://buckbuild.com/).
-# To run your application with Buck:
-# - install Buck
-# - `npm start` - to start the packager
-# - `cd android`
-# - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
-# - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
-# - `buck install -r android/app` - compile, install and run application
-#
-
-load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
-
-lib_deps = []
-
-create_aar_targets(glob(["libs/*.aar"]))
-
-create_jar_targets(glob(["libs/*.jar"]))
-
-android_library(
- name = "all-libs",
- exported_deps = lib_deps,
-)
-
-android_library(
- name = "app-code",
- srcs = glob([
- "src/main/java/**/*.java",
- ]),
- deps = [
- ":all-libs",
- ":build_config",
- ":res",
- ],
-)
-
-android_build_config(
- name = "build_config",
- package = "com.lbryandroid",
-)
-
-android_resource(
- name = "res",
- package = "com.lbryandroid",
- res = "src/main/res",
-)
-
-android_binary(
- name = "app",
- keystore = "//android/keystores:debug",
- manifest = "src/main/AndroidManifest.xml",
- package_type = "debug",
- deps = [
- ":app-code",
- ],
-)
diff --git a/app/build.gradle b/app/build.gradle
index aa3e40f4..81794584 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,137 +1,8 @@
-apply plugin: "com.android.application"
-
-import com.android.build.OutputFile
-
-/**
- * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets
- * and bundleReleaseJsAndAssets).
- * These basically call `react-native bundle` with the correct arguments during the Android build
- * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the
- * bundle directly from the development server. Below you can see all the possible configurations
- * and their defaults. If you decide to add a configuration block, make sure to add it before the
- * `apply from: "../../node_modules/react-native/react.gradle"` line.
- *
- * project.ext.react = [
- * // the name of the generated asset file containing your JS bundle
- * bundleAssetName: "index.android.bundle",
- *
- * // the entry file for bundle generation
- * entryFile: "index.android.js",
- *
- * // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
- * bundleCommand: "ram-bundle",
- *
- * // whether to bundle JS and assets in debug mode
- * bundleInDebug: false,
- *
- * // whether to bundle JS and assets in release mode
- * bundleInRelease: true,
- *
- * // whether to bundle JS and assets in another build variant (if configured).
- * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants
- * // The configuration property can be in the following formats
- * // 'bundleIn${productFlavor}${buildType}'
- * // 'bundleIn${buildType}'
- * // bundleInFreeDebug: true,
- * // bundleInPaidRelease: true,
- * // bundleInBeta: true,
- *
- * // whether to disable dev mode in custom build variants (by default only disabled in release)
- * // for example: to disable dev mode in the staging build type (if configured)
- * devDisabledInStaging: true,
- * // The configuration property can be in the following formats
- * // 'devDisabledIn${productFlavor}${buildType}'
- * // 'devDisabledIn${buildType}'
- *
- * // the root of your project, i.e. where "package.json" lives
- * root: "../../",
- *
- * // where to put the JS bundle asset in debug mode
- * jsBundleDirDebug: "$buildDir/intermediates/assets/debug",
- *
- * // where to put the JS bundle asset in release mode
- * jsBundleDirRelease: "$buildDir/intermediates/assets/release",
- *
- * // where to put drawable resources / React Native assets, e.g. the ones you use via
- * // require('./image.png')), in debug mode
- * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug",
- *
- * // where to put drawable resources / React Native assets, e.g. the ones you use via
- * // require('./image.png')), in release mode
- * resourcesDirRelease: "$buildDir/intermediates/res/merged/release",
- *
- * // by default the gradle tasks are skipped if none of the JS files or assets change; this means
- * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to
- * // date; if you have any other folders that you want to ignore for performance reasons (gradle
- * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/
- * // for example, you might want to remove it from here.
- * inputExcludes: ["android/**", "ios/**"],
- *
- * // override which node gets called and with what additional arguments
- * nodeExecutableAndArgs: ["node"],
- *
- * // supply additional arguments to the packager
- * extraPackagerArgs: []
- * ]
- */
-
-task buildReactNativeBundle(type:Exec) {
- println("Building React Native bundle")
- workingDir new File(rootProject.projectDir, '../')
- commandLine './bundle-android.sh'
-}
-preBuild.dependsOn buildReactNativeBundle
-
-task printVersionName {
- doLast {
- println android.defaultConfig.versionName
- }
-}
-
-project.ext.react = [
- entryFile: "index.js",
- enableHermes: false, // clean and rebuild if changing
-]
-
-/**
- * Set this to true to create two separate APKs instead of one:
- * - An APK that only works on ARM devices
- * - An APK that only works on x86 devices
- * The advantage is the size of the APK is reduced by about 4MB.
- * Upload all the APKs to the Play Store and people will download
- * the correct one based on the CPU architecture of their device.
- */
-def enableSeparateBuildPerCPUArchitecture = false
-
-/**
- * Run Proguard to shrink the Java bytecode in release builds.
- */
-def enableProguardInReleaseBuilds = false
-
-/**
- * The preferred build flavor of JavaScriptCore.
- *
- * For example, to use the international variant, you can use:
- * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
- *
- * The international variant includes ICU i18n library and necessary data
- * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
- * give correct results when using with locales other than en-US. Note that
- * this variant is about 6MiB larger per architecture than default.
- */
-def jscFlavor = 'org.webkit:android-jsc:+'
-
-/**
- * Whether to enable the Hermes VM.
- *
- * This should be set on project.ext.react and mirrored here. If it is not set
- * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
- * and the benefits of using Hermes will therefore be sharply reduced.
- */
-def enableHermes = project.ext.react.get("enableHermes", false);
+apply plugin: 'com.android.application'
android {
- compileSdkVersion rootProject.ext.compileSdkVersion
+ compileSdkVersion 29
+ buildToolsVersion "29.0.1"
flavorDimensions "default"
compileOptions {
@@ -141,18 +12,14 @@ android {
defaultConfig {
applicationId "io.lbry.browser"
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1403
- versionName "0.14.3"
- missingDimensionStrategy 'react-native-camera', 'general'
- multiDexEnabled true
- }
- dexOptions {
- javaMaxHeapSize "2048M"
- preDexLibraries false
- jumboMode true
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1503
+ versionName "0.15.3"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+
productFlavors {
__32bit {
versionCode android.defaultConfig.versionCode * 10 + 1
@@ -167,66 +34,70 @@ android {
}
}
}
- signingConfigs {
- debug {
- storeFile file('debug.keystore')
- storePassword 'android'
- keyAlias 'androiddebugkey'
- keyPassword 'android'
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
- buildTypes {
- debug {
- signingConfig signingConfigs.debug
- }
- release {
- }
+}
+
+task printVersionName {
+ doLast {
+ println android.defaultConfig.versionName
}
}
dependencies {
- implementation project(':@react-native-community_async-storage')
- implementation project(':react-native-camera')
- implementation project(':react-native-exception-handler')
- implementation project(':react-native-fast-image')
- implementation project(':react-native-fs')
- implementation project(':react-native-gesture-handler')
- implementation project(':react-native-reanimated')
- implementation project(':react-native-snackbar')
- implementation project(':react-native-video')
- implementation project(':react-native-webview')
- implementation project(':rn-fetch-blob')
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.media:media:1.0.0'
- implementation 'androidx.appcompat:appcompat:1.0.0'
- implementation 'com.facebook.react:react-native:0.61.5'
- implementation 'com.facebook.fresco:fresco:1.9.0'
- implementation 'com.facebook.fresco:animated-gif:1.9.0'
- implementation 'com.squareup.picasso:picasso:2.71828'
- implementation 'com.google.firebase:firebase-analytics:17.2.1'
- implementation 'com.google.android.gms:play-services-base:17.1.0'
- implementation 'androidx.exifinterface:exifinterface:1.0.0'
- implementation 'com.facebook.fresco:animated-base-support:1.3.0'
- implementation 'com.facebook.fresco:animated-gif:1.10.0'
- implementation 'com.google.firebase:firebase-messaging:20.1.0'
+ implementation 'com.google.android.material:material:1.1.0'
+ implementation "androidx.cardview:cardview:1.0.0"
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.navigation:navigation-fragment:2.2.2'
+ implementation 'androidx.navigation:navigation-ui:2.2.2'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'androidx.camera:camera-camera2:1.0.0-beta03'
+ implementation 'androidx.camera:camera-lifecycle:1.0.0-beta03'
+ implementation 'androidx.camera:camera-view:1.0.0-alpha10'
- __32bitImplementation files('libs/lbrysdk-0.73.1-release__arm.aar')
- __64bitImplementation files('libs/lbrysdk-0.73.1-release__arm64.aar')
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.4.1'
+ implementation 'com.google.firebase:firebase-analytics:17.4.0'
+ implementation 'com.google.android.gms:play-services-base:17.2.1'
+ implementation 'com.google.firebase:firebase-messaging:20.1.6'
- if (enableHermes) {
- def hermesPath = "../../node_modules/hermes-engine/android/";
- debugImplementation files(hermesPath + "hermes-debug.aar")
- releaseImplementation files(hermesPath + "hermes-release.aar")
- } else {
- implementation jscFlavor
- }
-}
+ implementation 'com.google.code.gson:gson:2.8.6'
+ implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'
+ implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4'
+ implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.4'
+ implementation 'com.google.android.exoplayer:extension-cast:2.11.4'
+ implementation 'com.google.android.exoplayer:extension-mediasession:2.11.4'
-// Run this once to be able to run the application with BUCK
-// puts all compile dependencies into folder libs for BUCK to use
-task copyDownloadableDepsToLibs(type: Copy) {
- from configurations.compile
- into 'libs'
+ implementation 'com.google.android:flexbox:2.0.1'
+
+ implementation 'com.hbb20:ccp:2.3.8'
+
+ implementation 'com.github.chrisbanes:PhotoView:2.3.0'
+ implementation 'com.atlassian.commonmark:commonmark:0.14.0'
+
+ 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'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ __32bitImplementation files('libs/lbrysdk-0.74.0-release__arm.aar')
+ __64bitImplementation files('libs/lbrysdk-0.74.0-release__arm64.aar')
}
apply plugin: 'com.google.gms.google-services'
diff --git a/app/build_defs.bzl b/app/build_defs.bzl
deleted file mode 100644
index fff270f8..00000000
--- a/app/build_defs.bzl
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Helper definitions to glob .aar and .jar targets"""
-
-def create_aar_targets(aarfiles):
- for aarfile in aarfiles:
- name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
- lib_deps.append(":" + name)
- android_prebuilt_aar(
- name = name,
- aar = aarfile,
- )
-
-def create_jar_targets(jarfiles):
- for jarfile in jarfiles:
- name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
- lib_deps.append(":" + name)
- prebuilt_jar(
- name = name,
- binary_jar = jarfile,
- )
diff --git a/app/debug.keystore b/app/debug.keystore
deleted file mode 100644
index 364e105e..00000000
Binary files a/app/debug.keystore and /dev/null differ
diff --git a/app/libs/lbrysdk-0.73.1-release__arm.aar b/app/libs/lbrysdk-0.74.0-release__arm.aar
similarity index 70%
rename from app/libs/lbrysdk-0.73.1-release__arm.aar
rename to app/libs/lbrysdk-0.74.0-release__arm.aar
index 6058d44a..f917e371 100644
Binary files a/app/libs/lbrysdk-0.73.1-release__arm.aar and b/app/libs/lbrysdk-0.74.0-release__arm.aar differ
diff --git a/app/libs/lbrysdk-0.73.1-release__arm64.aar b/app/libs/lbrysdk-0.74.0-release__arm64.aar
similarity index 71%
rename from app/libs/lbrysdk-0.73.1-release__arm64.aar
rename to app/libs/lbrysdk-0.74.0-release__arm64.aar
index 525e693d..fe87414a 100644
Binary files a/app/libs/lbrysdk-0.73.1-release__arm64.aar and b/app/libs/lbrysdk-0.74.0-release__arm64.aar differ
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 11b02572..f1b42451 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,10 +1,21 @@
# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
-# You can edit the include path and order by changing the proguardFiles
-# directive in build.gradle.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-# Add any project specific keep options here:
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/app/src/androidTest/java/io/lbry/browser/ExampleInstrumentedTest.java b/app/src/androidTest/java/io/lbry/browser/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..50f333a6
--- /dev/null
+++ b/app/src/androidTest/java/io/lbry/browser/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package io.lbry.browser;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ assertEquals("io.lbry.browser", appContext.getPackageName());
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a08ad7b2..28eca8a7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,30 +1,30 @@
+
-
+
-
-
+
+
@@ -37,16 +37,18 @@
-
-
-
+ android:supportsPictureInPicture="true"
+ android:theme="@style/AppTheme.NoActionBar"
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize">
+
+
+
+
@@ -54,14 +56,19 @@
-
-
-
-
-
-
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/assets/font_awesome_5_free_solid.otf b/app/src/main/assets/font_awesome_5_free_solid.otf
new file mode 100644
index 00000000..9d8a0e62
Binary files /dev/null and b/app/src/main/assets/font_awesome_5_free_solid.otf differ
diff --git a/app/src/main/assets/fonts/Feather.ttf b/app/src/main/assets/fonts/Feather.ttf
deleted file mode 100644
index 852c7135..00000000
Binary files a/app/src/main/assets/fonts/Feather.ttf and /dev/null differ
diff --git a/app/src/main/assets/fonts/FontAwesome.ttf b/app/src/main/assets/fonts/FontAwesome.ttf
deleted file mode 100644
index 35acda2f..00000000
Binary files a/app/src/main/assets/fonts/FontAwesome.ttf and /dev/null differ
diff --git a/app/src/main/assets/fonts/FontAwesome5_Brands.ttf b/app/src/main/assets/fonts/FontAwesome5_Brands.ttf
deleted file mode 100644
index 5f72e912..00000000
Binary files a/app/src/main/assets/fonts/FontAwesome5_Brands.ttf and /dev/null differ
diff --git a/app/src/main/assets/fonts/FontAwesome5_Regular.ttf b/app/src/main/assets/fonts/FontAwesome5_Regular.ttf
deleted file mode 100644
index a309313d..00000000
Binary files a/app/src/main/assets/fonts/FontAwesome5_Regular.ttf and /dev/null differ
diff --git a/app/src/main/assets/fonts/FontAwesome5_Solid.ttf b/app/src/main/assets/fonts/FontAwesome5_Solid.ttf
deleted file mode 100644
index 7ece3282..00000000
Binary files a/app/src/main/assets/fonts/FontAwesome5_Solid.ttf and /dev/null differ
diff --git a/app/src/main/assets/fonts/Inter-Medium.otf b/app/src/main/assets/fonts/Inter-Medium.otf
deleted file mode 100644
index 1bcb0a93..00000000
Binary files a/app/src/main/assets/fonts/Inter-Medium.otf and /dev/null differ
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 00000000..a82c512a
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/io/lbry/browser/DownloadManager.java b/app/src/main/java/io/lbry/browser/DownloadManager.java
deleted file mode 100644
index 1d8e82ab..00000000
--- a/app/src/main/java/io/lbry/browser/DownloadManager.java
+++ /dev/null
@@ -1,415 +0,0 @@
-package io.lbry.browser;
-
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Build;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
-import io.lbry.browser.receivers.NotificationDeletedReceiver;
-import io.lbry.lbrysdk.LbrynetService;
-
-import java.text.DecimalFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.List;
-import java.util.Random;
-
-public class DownloadManager {
- private Context context;
-
- private List activeDownloads = new ArrayList();
-
- private List completedDownloads = new ArrayList();
-
- private Map downloadIdOutpointsMap = new HashMap();
-
- // maintain a map of uris to writtenBytes, so that we check if it's changed and don't flood RN with update events every 500ms
- private Map writtenDownloadBytes = new HashMap();
-
- private HashMap builders = new HashMap();
-
- private HashMap downloadIdNotificationIdMap = new HashMap();
-
- private HashMap stoppedDownloadsMap = new HashMap();
-
- private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
-
- private static final int MAX_FILENAME_LENGTH = 20;
-
- private static final int MAX_PROGRESS = 100;
-
- private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
-
- private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
-
- private static boolean channelCreated = false;
-
- private static NotificationCompat.Builder groupBuilder = null;
-
- public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
-
- public static final String ACTION_DOWNLOAD_EVENT = "io.lbry.browser.ACTION_DOWNLOAD_EVENT";
-
- public static final String ACTION_START = "start";
-
- public static final String ACTION_COMPLETE = "complete";
-
- public static final String ACTION_UPDATE = "update";
-
- public static final int DOWNLOAD_NOTIFICATION_GROUP_ID = 20;
-
- public static boolean groupCreated = false;
-
- public DownloadManager(Context context) {
- this.context = context;
- }
-
- private int generateNotificationId() {
- int id = 0;
- Random random = new Random();
- do {
- id = random.nextInt();
- } while (id < 1000);
-
- return id;
- }
-
- private void createNotificationChannel() {
- // Only applies to Android 8.0 Oreo (API Level 26) or higher
- if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- NotificationChannel channel = new NotificationChannel(
- NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
- channel.setDescription("LBRY file downloads");
- channel.setSound(null, null);
- notificationManager.createNotificationChannel(channel);
- }
- }
-
- private void createNotificationGroup() {
- if (!groupCreated) {
- Intent intent = new Intent(context, NotificationDeletedReceiver.class);
- intent.putExtra(NOTIFICATION_ID_KEY, DOWNLOAD_NOTIFICATION_GROUP_ID);
-
- PendingIntent pendingIntent = PendingIntent.getBroadcast(context, DOWNLOAD_NOTIFICATION_GROUP_ID, intent, 0);
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- groupBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
- groupBuilder.setContentTitle("Active LBRY downloads")
- // contentText will be displayed if there are no notifications in the group
- .setContentText("There are no active LBRY downloads.")
- .setSmallIcon(android.R.drawable.stat_sys_download)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setGroup(GROUP_DOWNLOADS)
- .setGroupSummary(true)
- .setDeleteIntent(pendingIntent);
- notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
-
- groupCreated = true;
- }
- }
-
- public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
- Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
- launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
- return intent;
- }
-
- public void updateWrittenBytesForDownload(String id, double writtenBytes) {
- if (!writtenDownloadBytes.containsKey(id)) {
- writtenDownloadBytes.put(id, writtenBytes);
- }
- }
-
- public double getWrittenBytesForDownload(String id) {
- if (writtenDownloadBytes.containsKey(id)) {
- return writtenDownloadBytes.get(id);
- }
-
- return -1;
- }
-
- public void clearWrittenBytesForDownload(String id) {
- if (writtenDownloadBytes.containsKey(id)) {
- writtenDownloadBytes.remove(id);
- }
- }
-
- private Intent getDeleteDownloadIntent(String uri) {
- Intent intent = new Intent();
- intent.setAction(LbrynetService.ACTION_DELETE_DOWNLOAD);
- intent.putExtra("uri", uri);
- intent.putExtra("nativeDelete", true);
- return intent;
- }
-
- public void startDownload(String id, String filename, String outpoint) {
- if (filename == null || filename.trim().length() == 0) {
- return;
- }
-
- synchronized (this) {
- if (!isDownloadActive(id)) {
- activeDownloads.add(id);
- downloadIdOutpointsMap.put(id, outpoint);
- }
-
- createNotificationChannel();
- createNotificationGroup();
-
- PendingIntent stopDownloadIntent = PendingIntent.getBroadcast(context, 0, getDeleteDownloadIntent(id), PendingIntent.FLAG_CANCEL_CURRENT);
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
- // The file URI is used as the unique ID
- builder.setColor(ContextCompat.getColor(context, R.color.lbryGreen))
- .setContentIntent(getLaunchPendingIntent(id, context))
- .setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
- .setGroup(GROUP_DOWNLOADS)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setProgress(MAX_PROGRESS, 0, false)
- .setSmallIcon(android.R.drawable.stat_sys_download)
- .setOngoing(true)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopDownloadIntent);
-
- int notificationId = getNotificationId(id);
- downloadIdNotificationIdMap.put(id, notificationId);
- builders.put(notificationId, builder);
- notificationManager.notify(notificationId, builder.build());
-
- if (groupCreated && groupBuilder != null) {
- groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
- notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
- }
- }
- }
-
- public void updateDownload(String id, String filename, double writtenBytes, double totalBytes) {
- if (filename == null || filename.trim().length() == 0) {
- return;
- }
-
- synchronized (this) {
- createNotificationChannel();
- createNotificationGroup();
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- NotificationCompat.Builder builder = null;
- int notificationId = getNotificationId(id);
- if (builders.containsKey(notificationId)) {
- builder = builders.get(notificationId);
- } else {
- PendingIntent stopDownloadIntent = PendingIntent.getBroadcast(context, 0, getDeleteDownloadIntent(id), PendingIntent.FLAG_CANCEL_CURRENT);
- builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
- builder.setColor(ContextCompat.getColor(context, R.color.lbryGreen))
- .setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setOngoing(true)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopDownloadIntent);
- builders.put(notificationId, builder);
- }
-
- double progress = (writtenBytes / totalBytes) * 100;
- builder.setContentIntent(getLaunchPendingIntent(id, context))
- .setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
- .setGroup(GROUP_DOWNLOADS)
- .setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)
- .setSmallIcon(android.R.drawable.stat_sys_download);
- notificationManager.notify(notificationId, builder.build());
-
- if (progress >= MAX_PROGRESS) {
- builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
- .setContentText(String.format("%s", formatBytes(totalBytes)))
- .setGroup(GROUP_DOWNLOADS)
- .setProgress(0, 0, false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done)
- .setOngoing(false);
- builder.mActions.clear();
- notificationManager.notify(notificationId, builder.build());
-
- if (downloadIdNotificationIdMap.containsKey(id)) {
- downloadIdNotificationIdMap.remove(id);
- }
- if (builders.containsKey(notificationId)) {
- builders.remove(notificationId);
- }
-
- // If there are no more downloads and the group exists, set the icon to stop animating
- if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
- groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
- notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
- }
-
- completeDownload(id, filename, totalBytes);
- }
- }
- }
-
- public void completeDownload(String id, String filename, double totalBytes) {
- synchronized (this) {
- if (isDownloadActive(id)) {
- activeDownloads.remove(id);
- }
- if (!isDownloadCompleted(id)) {
- completedDownloads.add(id);
- }
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- NotificationCompat.Builder builder = null;
- int notificationId = getNotificationId(id);
- if (builders.containsKey(notificationId)) {
- builder = builders.get(notificationId);
- } else {
- builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
- builder.setPriority(NotificationCompat.PRIORITY_LOW);
- builders.put(notificationId, builder);
- }
-
- builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
- .setContentText(String.format("%s", formatBytes(totalBytes)))
- .setGroup(GROUP_DOWNLOADS)
- .setProgress(0, 0, false)
- .setSmallIcon(android.R.drawable.stat_sys_download_done)
- .setOngoing(false);
- builder.mActions.clear();
- notificationManager.notify(notificationId, builder.build());
-
- // If there are no more downloads and the group exists, set the icon to stop animating
- checkGroupDownloadIcon(notificationManager);
- }
- }
-
- public void abortDownload(String id) {
- synchronized (this) {
- if (downloadIdNotificationIdMap.containsKey(id)) {
- removeDownloadNotification(id);
- }
- activeDownloads.remove(id);
- }
- }
-
- public boolean isDownloadActive(String id) {
- return (activeDownloads.contains(id));
- }
-
- public boolean isDownloadCompleted(String id) {
- return (completedDownloads.contains(id));
- }
-
- public boolean hasActiveDownloads() {
- return activeDownloads.size() > 0;
- }
-
- public List getActiveDownloads() {
- return activeDownloads;
- }
-
- public List getCompletedDownloads() {
- return completedDownloads;
- }
-
- public String getOutpointForDownload(String uri) {
- if (downloadIdOutpointsMap.containsKey(uri)) {
- return downloadIdOutpointsMap.get(uri);
- }
-
- return null;
- }
-
- public void deleteDownloadUri(String uri) {
- synchronized (this) {
- activeDownloads.remove(uri);
- completedDownloads.remove(uri);
-
- if (downloadIdOutpointsMap.containsKey(uri)) {
- downloadIdOutpointsMap.remove(uri);
- }
- if (downloadIdNotificationIdMap.containsKey(uri)) {
- removeDownloadNotification(uri);
- }
- }
- }
-
- private void removeDownloadNotification(String id) {
- int notificationId = downloadIdNotificationIdMap.get(id);
- if (downloadIdNotificationIdMap.containsKey(id)) {
- downloadIdNotificationIdMap.remove(id);
- }
- if (builders.containsKey(notificationId)) {
- builders.remove(notificationId);
- }
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- NotificationCompat.Builder builder = builders.get(notificationId);
- notificationManager.cancel(notificationId);
-
- checkGroupDownloadIcon(notificationManager);
- if (builders.values().size() == 0) {
- notificationManager.cancel(DOWNLOAD_NOTIFICATION_GROUP_ID);
- groupCreated = false;
- }
- }
-
- private int getNotificationId(String id) {
- if (downloadIdNotificationIdMap.containsKey(id)) {
- return downloadIdNotificationIdMap.get(id);
- }
-
- int notificationId = generateNotificationId();
- if (MainActivity.downloadNotificationIds != null &&
- !MainActivity.downloadNotificationIds.contains(notificationId)) {
- MainActivity.downloadNotificationIds.add(notificationId);
- }
- downloadIdNotificationIdMap.put(id, notificationId);
- return notificationId;
- }
-
- private void checkGroupDownloadIcon(NotificationManagerCompat notificationManager) {
- if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
- groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
- notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
- }
- }
-
- private static String formatBytes(double bytes)
- {
- if (bytes < 1048576) { // < 1MB
- return String.format("%s KB", DECIMAL_FORMAT.format(bytes / 1024.0));
- }
-
- if (bytes < 1073741824) { // < 1GB
- return String.format("%s MB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0)));
- }
-
- return String.format("%s GB", DECIMAL_FORMAT.format(bytes / (1024.0 * 1024.0 * 1024.0)));
- }
-
- private static String truncateFilename(String filename, int alternateMaxLength) {
- int maxLength = alternateMaxLength > 0 ? alternateMaxLength : MAX_FILENAME_LENGTH;
- if (filename.length() < maxLength) {
- return filename;
- }
-
- // Get the extension
- int dotIndex = filename.lastIndexOf(".");
- if (dotIndex > -1) {
- String extension = filename.substring(dotIndex);
- return String.format("%s...%s", filename.substring(0, maxLength - extension.length() - 4), extension);
- }
-
- return String.format("%s...", filename.substring(0, maxLength - 3));
- }
-
- private static String truncateFilename(String filename) {
- return truncateFilename(filename, 0);
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/FirstRunActivity.java b/app/src/main/java/io/lbry/browser/FirstRunActivity.java
new file mode 100644
index 00000000..8197fd70
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/FirstRunActivity.java
@@ -0,0 +1,138 @@
+package io.lbry.browser;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.text.HtmlCompat;
+import androidx.preference.PreferenceManager;
+
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.Lbry;
+import io.lbry.browser.utils.LbryAnalytics;
+import io.lbry.browser.utils.Lbryio;
+
+public class FirstRunActivity extends AppCompatActivity {
+
+ private BroadcastReceiver sdkReadyReceiver;
+ private BroadcastReceiver authReceiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_first_run);
+
+ TextView welcomeTos = findViewById(R.id.welcome_text_view_tos);
+ welcomeTos.setMovementMethod(LinkMovementMethod.getInstance());
+ welcomeTos.setText(HtmlCompat.fromHtml(getString(R.string.welcome_tos), HtmlCompat.FROM_HTML_MODE_LEGACY));
+
+ findViewById(R.id.welcome_link_use_lbry).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ finishFirstRun();
+ }
+ });
+
+ registerAuthReceiver();
+ if (!Lbry.SDK_READY) {
+ findViewById(R.id.welcome_wait_container).setVisibility(View.VISIBLE);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(MainActivity.ACTION_SDK_READY);
+ sdkReadyReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // authenticate after we receive the sdk ready event
+ authenticate();
+ }
+ };
+ registerReceiver(sdkReadyReceiver, filter);
+ } else {
+ authenticate();
+ }
+ }
+
+ public void onResume() {
+ super.onResume();
+ LbryAnalytics.setCurrentScreen(this, "First Run", "FirstRun");
+ }
+
+ private void registerAuthReceiver() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS);
+ filter.addAction(MainActivity.ACTION_USER_AUTHENTICATION_FAILED);
+ authReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (MainActivity.ACTION_USER_AUTHENTICATION_SUCCESS.equals(intent.getAction())) {
+ handleAuthenticationSuccess();
+ } else {
+ handleAuthenticationFailed();
+ }
+ }
+ };
+ registerReceiver(authReceiver, filter);
+ }
+
+ private void handleAuthenticationSuccess() {
+ // first_auth completed event
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean firstAuthCompleted = sp.getBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, false);
+ if (!firstAuthCompleted) {
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_USER_AUTH);
+ sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED, true).apply();
+ }
+
+ findViewById(R.id.welcome_wait_container).setVisibility(View.GONE);
+ findViewById(R.id.welcome_display).setVisibility(View.VISIBLE);
+ findViewById(R.id.welcome_link_use_lbry).setVisibility(View.VISIBLE);
+ }
+
+ private void handleAuthenticationFailed() {
+ Toast.makeText(this, "Authentication failed.", Toast.LENGTH_LONG).show();
+ }
+
+ private void authenticate() {
+ new AuthenticateTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void finishFirstRun() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ sp.edit().putBoolean(MainActivity.PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, true).apply();
+
+ // first_run_completed event
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_FIRST_RUN_COMPLETED);
+ finish();
+ }
+
+ @Override
+ public void onBackPressed() {
+ return;
+ }
+
+ @Override
+ protected void onDestroy() {
+ Helper.unregisterReceiver(authReceiver, this);
+ Helper.unregisterReceiver(sdkReadyReceiver, this);
+ super.onDestroy();
+ }
+
+ private static class AuthenticateTask extends AsyncTask {
+ private Context context;
+ public AuthenticateTask(Context context) {
+ this.context = context;
+ }
+ protected Void doInBackground(Void... params) {
+ Lbryio.authenticate(context);
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
index f5c4c4a8..62536467 100644
--- a/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
+++ b/app/src/main/java/io/lbry/browser/LbrynetMessagingService.java
@@ -12,14 +12,16 @@ import android.os.Build;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+
import android.util.Log;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
+import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.lbrysdk.LbrynetService;
-import io.lbry.browser.reactmodules.UtilityModule;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@@ -30,22 +32,15 @@ import java.util.Map;
public class LbrynetMessagingService extends FirebaseMessagingService {
private static final String TAG = "LbrynetMessagingService";
-
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL";
-
private static final String TYPE_SUBSCRIPTION = "subscription";
-
private static final String TYPE_REWARD = "reward";
-
private static final String TYPE_INTERESTS = "interests";
-
private static final String TYPE_CREATOR = "creator";
-
private FirebaseAnalytics firebaseAnalytics;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
- Log.d(TAG, "From: " + remoteMessage.getFrom());
if (firebaseAnalytics == null) {
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
}
@@ -67,7 +62,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
if (firebaseAnalytics != null) {
Bundle bundle = new Bundle();
bundle.putString("name", name);
- firebaseAnalytics.logEvent("lbry_notification_receive", bundle);
+ firebaseAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_RECEIVE, bundle);
}
sendNotification(title, body, type, url, name, contentTitle, channelUrl, publishTime);
@@ -97,7 +92,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// TODO: Implement this method to send token to your app server.
}
- /**
+ /**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
@@ -113,29 +108,6 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// default to home page
url = "lbry://?discover";
}
- } else {
- if (!MainActivity.isServiceRunning(this, LbrynetService.class) &&
- contentTitle != null &&
- channelUrl != null &&
- !url.startsWith("lbry://?") /* not a special url */
- ) {
- // only enter lite mode when contentTitle and channelUrl are set (and the service isn't running yet)
- // cold start
- url = url + ((url.indexOf("?") > -1) ? "&liteMode=1" : "?liteMode=1");
- try {
- if (contentTitle != null) {
- url = url + "&contentTitle=" + URLEncoder.encode(contentTitle, "UTF-8");
- }
- if (channelUrl != null) {
- url = url + "&channelUrl=" + URLEncoder.encode(channelUrl, "UTF-8");
- }
- if (publishTime != null) {
- url = url + "&publishTime=" + URLEncoder.encode(publishTime, "UTF-8");
- }
- } catch (UnsupportedEncodingException ex) {
- // shouldn't happen
- }
- }
}
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@@ -160,27 +132,27 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
- NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
+ NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
- notificationManager.notify(9898, notificationBuilder.build());
+ notificationManager.notify(3, notificationBuilder.build());
}
public List getEnabledTypes() {
- SharedPreferences sp = getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
List enabledTypes = new ArrayList();
- if (sp.getBoolean(UtilityModule.RECEIVE_SUBSCRIPTION_NOTIFICATIONS, true)) {
+ if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) {
enabledTypes.add(TYPE_SUBSCRIPTION);
}
- if (sp.getBoolean(UtilityModule.RECEIVE_REWARD_NOTIFICATIONS, true)) {
+ if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_REWARDS, true)) {
enabledTypes.add(TYPE_REWARD);
}
- if (sp.getBoolean(UtilityModule.RECEIVE_INTERESTS_NOTIFICATIONS, true)) {
+ if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS, true)) {
enabledTypes.add(TYPE_INTERESTS);
}
- if (sp.getBoolean(UtilityModule.RECEIVE_CREATOR_NOTIFICATIONS, true)) {
+ if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_CREATOR, true)) {
enabledTypes.add(TYPE_CREATOR);
}
diff --git a/app/src/main/java/io/lbry/browser/MainActivity.java b/app/src/main/java/io/lbry/browser/MainActivity.java
index f5b0d6f0..ed711318 100644
--- a/app/src/main/java/io/lbry/browser/MainActivity.java
+++ b/app/src/main/java/io/lbry/browser/MainActivity.java
@@ -1,193 +1,2395 @@
package io.lbry.browser;
-import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Environment;
-import android.app.Activity;
-import android.app.ActivityManager;
+import android.app.PictureInPictureParams;
import android.content.BroadcastReceiver;
-import android.content.ContentUris;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageManager;
import android.content.SharedPreferences;
-import android.database.Cursor;
-import android.Manifest;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.sqlite.SQLiteDatabase;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
import android.os.Handler;
-import android.provider.DocumentsContract;
import android.provider.MediaStore;
-import android.provider.Settings;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Base64;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Menu;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.ext.cast.CastPlayer;
+import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
+import com.google.android.exoplayer2.ui.PlayerView;
+import com.google.android.exoplayer2.upstream.cache.Cache;
+import com.google.android.gms.cast.framework.CastContext;
+import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.Task;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.firebase.iid.FirebaseInstanceId;
+import com.google.firebase.iid.InstanceIdResult;
+
+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;
-import androidx.fragment.app.FragmentActivity;
-import android.telephony.SmsMessage;
-import android.widget.Toast;
+import androidx.core.content.FileProvider;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.core.view.GravityCompat;
+import androidx.core.view.OnApplyWindowInsetsListener;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.media.session.MediaButtonReceiver;
+import androidx.preference.PreferenceManager;
+import androidx.drawerlayout.widget.DrawerLayout;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
-import com.azendoo.reactnativesnackbar.SnackbarPackage;
-import com.brentvatne.react.ReactVideoPackage;
-import com.dylanvann.fastimage.FastImageViewPackage;
-import com.facebook.react.common.LifecycleState;
-import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
-import com.facebook.react.ReactRootView;
-import com.facebook.react.ReactInstanceManager;
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.ReactContext;
-import com.facebook.react.bridge.WritableArray;
-import com.facebook.react.bridge.WritableMap;
-import com.facebook.react.modules.core.DeviceEventManagerModule;
-import com.facebook.react.modules.core.PermissionAwareActivity;
-import com.facebook.react.modules.core.PermissionListener;
-import com.facebook.react.shell.MainReactPackage;
-import com.facebook.soloader.SoLoader;
-import com.google.firebase.analytics.FirebaseAnalytics;
-import com.reactnativecommunity.asyncstorage.AsyncStoragePackage;
-import com.reactnativecommunity.webview.RNCWebViewPackage;
-import com.rnfs.RNFSPackage;
-import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
-import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
-import com.swmansion.reanimated.ReanimatedPackage;
-import com.RNFetchBlob.RNFetchBlobPackage;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
-import io.lbry.browser.reactmodules.UtilityModule;
-import io.lbry.browser.reactpackages.LbryReactPackage;
-import io.lbry.browser.reactmodules.BackgroundMediaModule;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.net.ConnectException;
+import java.text.DecimalFormat;
+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;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import io.lbry.browser.adapter.NavigationMenuAdapter;
+import io.lbry.browser.adapter.UrlSuggestionListAdapter;
+import io.lbry.browser.data.DatabaseHelper;
+import io.lbry.browser.dialog.ContentScopeDialogFragment;
+import io.lbry.browser.exceptions.LbryUriException;
+import io.lbry.browser.listener.CameraPermissionListener;
+import io.lbry.browser.listener.DownloadActionListener;
+import io.lbry.browser.listener.FetchChannelsListener;
+import io.lbry.browser.listener.FetchClaimsListener;
+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.ClaimCacheKey;
+import io.lbry.browser.model.NavMenuItem;
+import io.lbry.browser.model.Tag;
+import io.lbry.browser.model.UrlSuggestion;
+import io.lbry.browser.model.WalletBalance;
+import io.lbry.browser.model.WalletSync;
+import io.lbry.browser.model.lbryinc.Reward;
+import io.lbry.browser.model.lbryinc.Subscription;
+import io.lbry.browser.tasks.claim.ClaimListResultHandler;
+import io.lbry.browser.tasks.claim.ClaimListTask;
+import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
+import io.lbry.browser.tasks.lbryinc.FetchRewardsTask;
+import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
+import io.lbry.browser.tasks.MergeSubscriptionsTask;
+import io.lbry.browser.tasks.claim.ResolveTask;
+import io.lbry.browser.tasks.localdata.FetchRecentUrlHistoryTask;
+import io.lbry.browser.tasks.wallet.DefaultSyncTaskHandler;
+import io.lbry.browser.tasks.wallet.LoadSharedUserStateTask;
+import io.lbry.browser.tasks.wallet.SaveSharedUserStateTask;
+import io.lbry.browser.tasks.wallet.SyncApplyTask;
+import io.lbry.browser.tasks.wallet.SyncGetTask;
+import io.lbry.browser.tasks.wallet.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.findcontent.EditorsChoiceFragment;
+import io.lbry.browser.ui.findcontent.FileViewFragment;
+import io.lbry.browser.ui.findcontent.FollowingFragment;
+import io.lbry.browser.ui.library.LibraryFragment;
+import io.lbry.browser.ui.other.AboutFragment;
+import io.lbry.browser.ui.publish.PublishFormFragment;
+import io.lbry.browser.ui.publish.PublishFragment;
+import io.lbry.browser.ui.publish.PublishesFragment;
+import io.lbry.browser.ui.findcontent.SearchFragment;
+import io.lbry.browser.ui.other.SettingsFragment;
+import io.lbry.browser.ui.findcontent.AllContentFragment;
+import io.lbry.browser.ui.wallet.InvitesFragment;
+import io.lbry.browser.ui.wallet.RewardsFragment;
+import io.lbry.browser.ui.wallet.WalletFragment;
+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.Lbryio;
+import io.lbry.lbrysdk.DownloadManager;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.ServiceHelper;
import io.lbry.lbrysdk.Utils;
+import lombok.Getter;
+import lombok.Setter;
+import okhttp3.OkHttpClient;
-import java.io.File;
-import java.net.ConnectException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
+public class MainActivity extends AppCompatActivity implements SdkStatusListener {
-import org.json.JSONObject;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.reactnative.camera.RNCameraPackage;
+ private Map specialRouteFragmentClassMap;
+ private boolean inPictureInPictureMode;
+ public static SimpleExoPlayer appPlayer;
+ public static Cache playerCache;
+ public static boolean playerReassigned;
+ public static CastContext castContext;
+ public static CastPlayer castPlayer;
+ public static Claim nowPlayingClaim;
+ public static String nowPlayingClaimUrl;
+ public static boolean startingFilePickerActivity = false;
+ 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;
-public class MainActivity extends FragmentActivity implements DefaultHardwareBackBtnHandler, PermissionAwareActivity {
+ @Setter
+ private BackPressInterceptor backPressInterceptor;
- private static Activity currentActivity = null;
- private static final int OVERLAY_PERMISSION_REQ_CODE = 101;
- private static final int STORAGE_PERMISSION_REQ_CODE = 201;
- private static final int PHONE_STATE_PERMISSION_REQ_CODE = 202;
- private static final int RECEIVE_SMS_PERMISSION_REQ_CODE = 203;
- public static final int DOCUMENT_PICKER_RESULT_CODE = 301;
- public static final String SHARED_PREFERENCES_NAME = "LBRY";
- public static final String SALT_KEY = "salt";
- public static final String DEVICE_ID_KEY = "deviceId";
- public static final String SOURCE_NOTIFICATION_ID_KEY = "sourceNotificationId";
- public static final String SETTING_KEEP_DAEMON_RUNNING = "keepDaemonRunning";
- public static List downloadNotificationIds = new ArrayList();
+ @Getter
+ private String firebaseMessagingToken;
- private BroadcastReceiver notificationsReceiver;
- private BroadcastReceiver smsReceiver;
+ private Map openNavFragments;
+ private static final Map fragmentClassNavIdMap = new HashMap<>();
+ static {
+ Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);
+
+ fragmentClassNavIdMap.put(FollowingFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
+ fragmentClassNavIdMap.put(EditorsChoiceFragment.class, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
+ fragmentClassNavIdMap.put(AllContentFragment.class, NavMenuItem.ID_ITEM_ALL_CONTENT);
+
+ fragmentClassNavIdMap.put(PublishFragment.class, NavMenuItem.ID_ITEM_NEW_PUBLISH);
+ fragmentClassNavIdMap.put(ChannelManagerFragment.class, NavMenuItem.ID_ITEM_CHANNELS);
+ fragmentClassNavIdMap.put(LibraryFragment.class, NavMenuItem.ID_ITEM_LIBRARY);
+ fragmentClassNavIdMap.put(PublishesFragment.class, NavMenuItem.ID_ITEM_PUBLISHES);
+
+ fragmentClassNavIdMap.put(WalletFragment.class, NavMenuItem.ID_ITEM_WALLET);
+ fragmentClassNavIdMap.put(RewardsFragment.class, NavMenuItem.ID_ITEM_REWARDS);
+ fragmentClassNavIdMap.put(InvitesFragment.class, NavMenuItem.ID_ITEM_INVITES);
+
+ fragmentClassNavIdMap.put(SettingsFragment.class, NavMenuItem.ID_ITEM_SETTINGS);
+ fragmentClassNavIdMap.put(AboutFragment.class, NavMenuItem.ID_ITEM_ABOUT);
+
+ // Internal (sub-)pages
+ fragmentClassNavIdMap.put(FileViewFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
+ fragmentClassNavIdMap.put(ChannelFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
+ fragmentClassNavIdMap.put(SearchFragment.class, NavMenuItem.ID_ITEM_FOLLOWING);
+ }
+
+ public static final int REQUEST_STORAGE_PERMISSION = 1001;
+ public static final int REQUEST_CAMERA_PERMISSION = 1002;
+ public static final int REQUEST_SIMPLE_SIGN_IN = 2001;
+ public static final int REQUEST_WALLET_SYNC_SIGN_IN = 2002;
+ 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";
+ public static final String ACTION_AUTH_TOKEN_GENERATED = "io.lbry.browser.Broadcast.AuthTokenGenerated";
+ public static final String ACTION_USER_AUTHENTICATION_SUCCESS = "io.lbry.browser.Broadcast.UserAuthenticationSuccess";
+ public static final String ACTION_USER_SIGN_IN_SUCCESS = "io.lbry.browser.Broadcast.UserSignInSuccess";
+ public static final String ACTION_USER_AUTHENTICATION_FAILED = "io.lbry.browser.Broadcast.UserAuthenticationFailed";
+ public static final String ACTION_NOW_PLAYING_CLAIM_UPDATED = "io.lbry.browser.Broadcast.NowPlayingClaimUpdated";
+ public static final String ACTION_NOW_PLAYING_CLAIM_CLEARED = "io.lbry.browser.Broadcast.NowPlayingClaimCleared";
+ public static final String ACTION_OPEN_ALL_CONTENT_TAG = "io.lbry.browser.Broadcast.OpenAllContentTag";
+ public static final String ACTION_WALLET_BALANCE_UPDATED = "io.lbry.browser.Broadcast.WalletBalanceUpdated";
+ public static final String ACTION_OPEN_CHANNEL_URL = "io.lbry.browser.Broadcast.OpenChannelUrl";
+ public static final String ACTION_OPEN_WALLET_PAGE = "io.lbry.browser.Broadcast.OpenWalletPage";
+ public static final String ACTION_OPEN_REWARDS_PAGE = "io.lbry.browser.Broadcast.OpenRewardsPage";
+ public static final String ACTION_SAVE_SHARED_USER_STATE = "io.lbry.browser.Broadcast.SaveSharedUserState";
+
+ // preference keys
+ public static final String PREFERENCE_KEY_DARK_MODE = "io.lbry.browser.preference.userinterface.DarkMode";
+ public static final String PREFERENCE_KEY_SHOW_MATURE_CONTENT = "io.lbry.browser.preference.userinterface.ShowMatureContent";
+ public static final String PREFERENCE_KEY_SHOW_URL_SUGGESTIONS = "io.lbry.browser.preference.userinterface.UrlSuggestions";
+ public static final String PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS = "io.lbry.browser.preference.notifications.Subscriptions";
+ public static final String PREFERENCE_KEY_NOTIFICATION_REWARDS = "io.lbry.browser.preference.notifications.Rewards";
+ public static final String PREFERENCE_KEY_NOTIFICATION_CONTENT_INTERESTS = "io.lbry.browser.preference.notifications.ContentInterests";
+ public static final String PREFERENCE_KEY_NOTIFICATION_CREATOR = "io.lbry.browser.preference.notifications.Creator";
+ public static final String PREFERENCE_KEY_KEEP_SDK_BACKGROUND = "io.lbry.browser.preference.other.KeepSdkInBackground";
+ public static final String PREFERENCE_KEY_PARTICIPATE_DATA_NETWORK = "io.lbry.browser.preference.other.ParticipateInDataNetwork";
+
+ // Internal flags / setting preferences
+ public static final String PREFERENCE_KEY_INTERNAL_SKIP_WALLET_ACCOUNT = "io.lbry.browser.preference.internal.WalletSkipAccount";
+ public static final String PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED = "io.lbry.browser.preference.internal.WalletSyncEnabled";
+ public static final String PREFERENCE_KEY_INTERNAL_WALLET_RECEIVE_ADDRESS = "io.lbry.browser.preference.internal.WalletReceiveAddress";
+ public static final String PREFERENCE_KEY_INTERNAL_REWARDS_NOT_INTERESTED = "io.lbry.browser.preference.internal.RewardsNotInterested";
+ public static final String PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED = "io.lbry.browser.preference.internal.NewAndroidRewardClaimed";
+
+ public static final String PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED = "io.lbry.browser.preference.internal.FirstRunCompleted";
+ public static final String PREFERENCE_KEY_INTERNAL_FIRST_AUTH_COMPLETED = "io.lbry.browser.preference.internal.FirstAuthCompleted";
+
+ private final int CHECK_SDK_READY_INTERVAL = 1000;
+
+ public static final String PREFERENCE_KEY_AUTH_TOKEN = "io.lbry.browser.Preference.AuthToken";
+
+ public static final String SECURE_VALUE_KEY_SAVED_PASSWORD = "io.lbry.browser.PX";
+
+ private static final String TAG = "io.lbry.browser.Main";
+
+ private NavigationMenuAdapter navMenuAdapter;
+ private UrlSuggestionListAdapter urlSuggestionListAdapter;
+ private List recentUrlHistory;
+ private boolean hasLoadedFirstBalance;
+
+ // broadcast receivers
private BroadcastReceiver serviceActionsReceiver;
- private BroadcastReceiver downloadEventReceiver;
- private FirebaseAnalytics firebaseAnalytics;
- private ReactRootView mReactRootView;
- private ReactInstanceManager mReactInstanceManager;
+ private BroadcastReceiver requestsReceiver;
- /**
- * Flag which indicates whether or not the service is running. Will be updated in the
- * onResume method.
- */
+ private static boolean appStarted;
private boolean serviceRunning;
private CheckSdkReadyTask checkSdkReadyTask;
+ private MediaSessionCompat mediaSession;
private boolean receivedStopService;
- private PermissionListener permissionListener;
- public static boolean lbrySdkReady;
+ private ActionBarDrawerToggle toggle;
+ private SyncSetTask syncSetTask = null;
+ private List pendingSyncSetQueue;
+ @Getter
+ private DatabaseHelper dbHelper;
+ private int selectedMenuItemId = -1;
+ private List cameraPermissionListeners;
+ private List downloadActionListeners;
+ private List filePickerListeners;
+ private List sdkStatusListeners;
+ private List storagePermissionListeners;
+ private List walletBalanceListeners;
+ private List fetchClaimsListeners;
+ private List fetchChannelsListeners;
+ @Getter
+ private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+ private boolean walletBalanceUpdateScheduled;
+ private boolean shouldOpenUserSelectedMenuItem;
+ private boolean walletSyncScheduled;
+ private String pendingAllContentTag;
+ private String pendingChannelUrl;
+ private boolean pendingOpenWalletPage;
+ private boolean pendingOpenRewardsPage;
+ private boolean pendingFollowingReload;
- protected String getMainComponentName() {
- return "LBRYApp";
+ // startup stages (to be able to determine how far a user made it if startup fails)
+ // and display a more useful message for troubleshooting
+ private static final int STARTUP_STAGE_INSTALL_ID_LOADED = 1;
+ private static final int STARTUP_STAGE_KNOWN_TAGS_LOADED = 2;
+ private static final int STARTUP_STAGE_EXCHANGE_RATE_LOADED = 3;
+ private static final int STARTUP_STAGE_USER_AUTHENTICATED = 4;
+ private static final int STARTUP_STAGE_NEW_INSTALL_DONE = 5;
+ private static final int STARTUP_STAGE_SUBSCRIPTIONS_LOADED = 6;
+ private static final int STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED = 7;
+
+ public boolean isDarkMode() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ return sp.getBoolean(PREFERENCE_KEY_DARK_MODE, false);
}
-
- public static LaunchTiming CurrentLaunchTiming;
-
+
@Override
protected void onCreate(Bundle savedInstanceState) {
- CurrentLaunchTiming = new LaunchTiming(new Date());
+ AppCompatDelegate.setDefaultNightMode(isDarkMode() ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
+
+ initKeyStore();
+ loadAuthToken();
+
+ if (!isDarkMode()) {
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ initSpecialRouteMap();
+
+ LbryAnalytics.init(this);
+ FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(new OnCompleteListener() {
+ @Override
+ public void onComplete(Task task) {
+ if (!task.isSuccessful()) {
+ return;
+ }
+
+ // Get new Instance ID token
+ firebaseMessagingToken = task.getResult().getToken();
+ }
+ });
+
super.onCreate(savedInstanceState);
- currentActivity = this;
+ dbHelper = new DatabaseHelper(this);
+ checkNotificationOpenIntent(getIntent());
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
- SoLoader.init(this, false);
+ // TODO: Check Google Play Services availability
+ // castContext = CastContext.getSharedInstance(this);
- // Register the stop service receiver (so that we close the activity if the user requests the service to stop)
+ ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.content_main), new OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
+ ViewCompat.onApplyWindowInsets(findViewById(R.id.url_suggestions_container),
+ insets.replaceSystemWindowInsets(0, 0, 0, insets.getSystemWindowInsetBottom()));
+ return ViewCompat.onApplyWindowInsets(v,
+ insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0,
+ 0, insets.getSystemWindowInsetBottom()));
+ }
+ });
+
+ // register receivers
+ registerRequestsReceiver();
registerServiceActionsReceiver();
- // Register SMS receiver for handling verification texts
- registerSmsReceiver();
+ View decorView = getWindow().getDecorView();
+ decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ // not fullscreen
+ }
+ }
+ });
- // Register the receiver to emit download events
- registerDownloadEventReceiver();
+ // setup uri bar
+ setupUriBar();
- // Start the sdk service if it is not started
- // Check the dht setting
- SharedPreferences sp = getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- LbrynetService.setDHTEnabled(sp.getBoolean(UtilityModule.DHT_ENABLED, false));
+ /*View decorView = getWindow().getDecorView();
+ decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
+ findViewById(R.id.app_bar_main_container).setFitsSystemWindows(false);
+ findViewById(R.id.drawer_layout).setFitsSystemWindows(false);
+ } else {
+ findViewById(R.id.app_bar_main_container).setFitsSystemWindows(true);
+ findViewById(R.id.drawer_layout).setFitsSystemWindows(true);
+ }
+ }
+ });*/
+
+ // other
+ pendingSyncSetQueue = new ArrayList<>();
+ openNavFragments = new HashMap<>();
+ cameraPermissionListeners = new ArrayList<>();
+ downloadActionListeners = new ArrayList<>();
+ filePickerListeners = new ArrayList<>();
+ sdkStatusListeners = new ArrayList<>();
+ storagePermissionListeners = new ArrayList<>();
+ walletBalanceListeners = new ArrayList<>();
+ fetchClaimsListeners = new ArrayList<>();
+ fetchChannelsListeners = new ArrayList<>();
+
+ sdkStatusListeners.add(this);
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager.addOnBackStackChangedListener(backStackChangedListener);
+
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ toggle = new ActionBarDrawerToggle(
+ this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) {
+ public void onDrawerClosed(View view) {
+ super.onDrawerClosed(view);
+ if (shouldOpenUserSelectedMenuItem) {
+ openSelectedMenuItem();
+ shouldOpenUserSelectedMenuItem = false;
+ }
+ }
+
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {
+ if (slideOffset != 0) {
+ clearWunderbarFocus(findViewById(R.id.wunderbar));
+ }
+ super.onDrawerSlide(drawerView, slideOffset);
+ }
+ };
+ drawer.addDrawerListener(toggle);
+ toggle.syncState();
+ toggle.setToolbarNavigationClickListener((view) -> {
+ if (toggle != null && !toggle.isDrawerIndicatorEnabled()) {
+ FragmentManager manager = getSupportFragmentManager();
+ if (manager != null) {
+ manager.popBackStack();
+ setSelectedNavMenuItemForFragment(getCurrentFragment());
+ }
+ }
+ });
+
+ findViewById(R.id.global_now_playing_close).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ stopExoplayer();
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ nowPlayingClaim = null;
+ findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
+ }
+ });
+
+ findViewById(R.id.global_now_playing_card).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (nowPlayingClaim != null && !Helper.isNullOrEmpty(nowPlayingClaimUrl)) {
+ openFileUrl(nowPlayingClaimUrl);
+ }
+ }
+ });
+
+ // display custom navigation menu
+ LinearLayoutManager llm = new LinearLayoutManager(this);
+ RecyclerView navItemsView = findViewById(R.id.nav_view_items);
+ navItemsView.setLayoutManager(llm);
+ navMenuAdapter = new NavigationMenuAdapter(flattenNavMenu(buildNavMenu(this)), this);
+ navMenuAdapter.setListener(new NavigationMenuAdapter.NavigationMenuItemClickListener() {
+ @Override
+ public void onNavigationMenuItemClicked(NavMenuItem menuItem) {
+ if (navMenuAdapter.getCurrentItemId() == menuItem.getId() && !Arrays.asList(
+ NavMenuItem.ID_ITEM_FOLLOWING, NavMenuItem.ID_ITEM_ALL_CONTENT, NavMenuItem.ID_ITEM_WALLET).contains(menuItem.getId())) {
+ // already open
+ navMenuAdapter.setCurrentItem(menuItem);
+ closeDrawer();
+ return;
+ }
+
+ navMenuAdapter.setCurrentItem(menuItem);
+ shouldOpenUserSelectedMenuItem = true;
+ selectedMenuItemId = menuItem.getId();
+ closeDrawer();
+ }
+ });
+ navItemsView.setAdapter(navMenuAdapter);
+
+ findViewById(R.id.sign_in_button_container).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ walletSyncSignIn();
+ }
+ });
+ }
+
+ private void initSpecialRouteMap() {
+ specialRouteFragmentClassMap = new HashMap<>();
+ specialRouteFragmentClassMap.put("about", AboutFragment.class);
+ specialRouteFragmentClassMap.put("allContent", AllContentFragment.class);
+ specialRouteFragmentClassMap.put("channels", ChannelManagerFragment.class);
+ specialRouteFragmentClassMap.put("invite", InvitesFragment.class);
+ specialRouteFragmentClassMap.put("invites", InvitesFragment.class);
+ specialRouteFragmentClassMap.put("library", LibraryFragment.class);
+ specialRouteFragmentClassMap.put("publish", PublishFragment.class);
+ specialRouteFragmentClassMap.put("publishes", PublishesFragment.class);
+ specialRouteFragmentClassMap.put("following", FollowingFragment.class);
+ specialRouteFragmentClassMap.put("rewards", RewardsFragment.class);
+ specialRouteFragmentClassMap.put("settings", SettingsFragment.class);
+ specialRouteFragmentClassMap.put("subscriptions", FollowingFragment.class);
+ specialRouteFragmentClassMap.put("wallet", WalletFragment.class);
+ specialRouteFragmentClassMap.put("discover", FollowingFragment.class);
+ }
+
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ checkUrlIntent(intent);
+ checkNotificationOpenIntent(intent);
+ }
+
+ public void addCameraPermissionListener(CameraPermissionListener listener) {
+ if (!cameraPermissionListeners.contains(listener)) {
+ cameraPermissionListeners.add(listener);
+ }
+ }
+
+ public void removeCameraPermissionListener(CameraPermissionListener listener) {
+ cameraPermissionListeners.remove(listener);
+ }
+
+ public void addDownloadActionListener(DownloadActionListener listener) {
+ if (!downloadActionListeners.contains(listener)) {
+ downloadActionListeners.add(listener);
+ }
+ }
+
+ public void removeDownloadActionListener(DownloadActionListener listener) {
+ 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);
+ }
+ }
+
+ public void removeSdkStatusListener(SdkStatusListener listener) {
+ sdkStatusListeners.remove(listener);
+ }
+
+ public void addStoragePermissionListener(StoragePermissionListener listener) {
+ if (!storagePermissionListeners.contains(listener)) {
+ storagePermissionListeners.add(listener);
+ }
+ }
+
+ public void removeStoragePermissionListener(StoragePermissionListener listener) {
+ storagePermissionListeners.remove(listener);
+ }
+
+ public void addWalletBalanceListener(WalletBalanceListener listener) {
+ if (!walletBalanceListeners.contains(listener)) {
+ walletBalanceListeners.add(listener);
+ }
+ }
+
+ public void removeWalletBalanceListener(WalletBalanceListener listener) {
+ walletBalanceListeners.remove(listener);
+ }
+
+ public void removeNavFragment(Class fragmentClass, int navItemId) {
+ String key = buildNavFragmentKey(fragmentClass, navItemId, null);
+ if (openNavFragments.containsKey(key)) {
+ openNavFragments.remove(key);
+ }
+ }
+
+ public void addFetchChannelsListener(FetchChannelsListener listener) {
+ if (!fetchChannelsListeners.contains(listener)) {
+ fetchChannelsListeners.add(listener);
+ }
+ }
+ public void removeFetchChannelsListener(FetchChannelsListener listener) {
+ fetchChannelsListeners.remove(listener);
+ }
+
+ public void addFetchClaimsListener(FetchClaimsListener listener) {
+ if (!fetchClaimsListeners.contains(listener)) {
+ fetchClaimsListeners.add(listener);
+ }
+ }
+ public void removeFetchClaimsListener(FetchClaimsListener listener) {
+ fetchClaimsListeners.remove(listener);
+ }
+
+ private void openSelectedMenuItem() {
+ switch (selectedMenuItemId) {
+ // TODO: reverse map lookup for class?
+ case NavMenuItem.ID_ITEM_FOLLOWING:
+ openFragment(FollowingFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING);
+ break;
+ case NavMenuItem.ID_ITEM_EDITORS_CHOICE:
+ openFragment(EditorsChoiceFragment.class, true, NavMenuItem.ID_ITEM_EDITORS_CHOICE);
+ break;
+ case NavMenuItem.ID_ITEM_ALL_CONTENT:
+ openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT);
+ break;
+
+ case NavMenuItem.ID_ITEM_NEW_PUBLISH:
+ openFragment(PublishFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH);
+ break;
+ case NavMenuItem.ID_ITEM_CHANNELS:
+ openFragment(ChannelManagerFragment.class, true, NavMenuItem.ID_ITEM_CHANNELS);
+ break;
+ case NavMenuItem.ID_ITEM_LIBRARY:
+ openFragment(LibraryFragment.class, true, NavMenuItem.ID_ITEM_LIBRARY);
+ break;
+ case NavMenuItem.ID_ITEM_PUBLISHES:
+ openFragment(PublishesFragment.class, true, NavMenuItem.ID_ITEM_PUBLISHES);
+ break;
+
+ case NavMenuItem.ID_ITEM_WALLET:
+ openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
+ break;
+ case NavMenuItem.ID_ITEM_REWARDS:
+ openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
+ break;
+ case NavMenuItem.ID_ITEM_INVITES:
+ openFragment(InvitesFragment.class, true, NavMenuItem.ID_ITEM_INVITES);
+ break;
+
+ case NavMenuItem.ID_ITEM_SETTINGS:
+ openFragment(SettingsFragment.class, true, NavMenuItem.ID_ITEM_SETTINGS);
+ break;
+ case NavMenuItem.ID_ITEM_ABOUT:
+ openFragment(AboutFragment.class, true, NavMenuItem.ID_ITEM_ABOUT);
+ break;
+ }
+ }
+
+ public void openChannelClaim(Claim claim) {
+ Map params = new HashMap<>();
+ params.put("url", !Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
+ params.put("claim", getCachedClaimForUrl(claim.getPermanentUrl()));
+ 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 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) {
+ params.put("claim", claim);
+ }
+ openFragment(PublishFormFragment.class, true, NavMenuItem.ID_ITEM_NEW_PUBLISH, params);
+ }
+
+ public void openChannelUrl(String url) {
+ Map params = new HashMap<>();
+ params.put("url", url);
+ params.put("claim", getCachedClaimForUrl(url));
+ openFragment(ChannelFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
+ }
+
+ private Claim getCachedClaimForUrl(String url) {
+ ClaimCacheKey key = new ClaimCacheKey();
+ key.setUrl(url);
+ return Lbry.claimCache.containsKey(key) ? Lbry.claimCache.get(key) : null;
+ }
+
+ public void setWunderbarValue(String value) {
+ EditText wunderbar = findViewById(R.id.wunderbar);
+ wunderbar.setText(value);
+ wunderbar.setSelection(0);
+ }
+
+ public void openAllContentFragmentWithTag(String tag) {
+ Map params = new HashMap<>();
+ params.put("singleTag", tag);
+ openFragment(AllContentFragment.class, true, NavMenuItem.ID_ITEM_ALL_CONTENT, params);
+ }
+
+ public void openFileUrl(String url) {
+ Map params = new HashMap<>();
+ params.put("url", url);
+ openFragment(FileViewFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
+ }
+
+ public void openFileClaim(Claim claim) {
+ Map params = new HashMap<>();
+ params.put("claimId", claim.getClaimId());
+ params.put("url", !Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
+ openFragment(FileViewFragment.class, true, NavMenuItem.ID_ITEM_FOLLOWING, params);
+ }
+
+ private FragmentManager.OnBackStackChangedListener backStackChangedListener = new FragmentManager.OnBackStackChangedListener() {
+ @Override
+ public void onBackStackChanged() {
+ FragmentManager manager = getSupportFragmentManager();
+ if (manager != null) {
+ Fragment currentFragment = getCurrentFragment();
+
+ }
+ }
+ };
+
+ public void setSelectedMenuItemForFragment(Fragment fragment) {
+ if (fragment != null) {
+ Class fragmentClass = fragment.getClass();
+ if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
+ navMenuAdapter.setCurrentItem(fragmentClassNavIdMap.get(fragmentClass));
+ }
+ }
+ }
+
+ private void renderPictureInPictureMode() {
+ findViewById(R.id.content_main).setVisibility(View.GONE);
+ findViewById(R.id.floating_balance_main_container).setVisibility(View.GONE);
+ findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
+ getSupportActionBar().hide();
+
+ PlayerView pipPlayer = findViewById(R.id.pip_player);
+ pipPlayer.setVisibility(View.VISIBLE);
+ pipPlayer.setPlayer(appPlayer);
+ pipPlayer.setUseController(false);
+ playerReassigned = true;
+ }
+ private void renderFullMode() {
+ getSupportActionBar().show();
+ findViewById(R.id.content_main).setVisibility(View.VISIBLE);
+ findViewById(R.id.floating_balance_main_container).setVisibility(View.VISIBLE);
+ Fragment fragment = getCurrentFragment();
+ if (!(fragment instanceof FileViewFragment)) {
+ findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
+ }
+ if (!Lbry.SDK_READY) {
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
+ }
+
+ PlayerView pipPlayer = findViewById(R.id.pip_player);
+ pipPlayer.setVisibility(View.INVISIBLE);
+ pipPlayer.setPlayer(null);
+ playerReassigned = true;
+ }
+
+ @Override
+ protected void onDestroy() {
+ unregisterReceivers();
+ NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
+ if (receivedStopService || !isServiceRunning(this, LbrynetService.class)) {
+ notificationManager.cancelAll();
+ }
+ if (dbHelper != null) {
+ dbHelper.close();
+ }
+ if (mediaSession != null) {
+ mediaSession.release();
+ }
+ stopExoplayer();
+ nowPlayingClaim = null;
+ nowPlayingClaimUrl = null;
+ appStarted = false;
+
+ if (!keepSdkBackground()) {
+ sendBroadcast(new Intent(LbrynetService.ACTION_STOP_SERVICE));
+ }
+
+ super.onDestroy();
+ }
+
+ public static void stopExoplayer() {
+ if (appPlayer != null) {
+ appPlayer.stop(true);
+ appPlayer.release();
+ appPlayer = null;
+ }
+ if (playerCache != null) {
+ playerCache.release();
+ playerCache = null;
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ //getMenuInflater().inflate(R.menu.main, menu);
+ return true;
+ }
+
+ public void updateWalletBalance() {
+ WalletBalanceTask task = new WalletBalanceTask(new WalletBalanceTask.WalletBalanceHandler() {
+ @Override
+ public void onSuccess(WalletBalance walletBalance) {
+ for (WalletBalanceListener listener : walletBalanceListeners) {
+ if (listener != null) {
+ listener.onWalletBalanceUpdated(walletBalance);
+ }
+ }
+ Lbry.walletBalance = walletBalance;
+ updateFloatingWalletBalance();
+ updateUsdWalletBalanceInNav();
+
+ sendBroadcast(new Intent(ACTION_WALLET_BALANCE_UPDATED));
+ }
+
+ @Override
+ public void onError(Exception error) {
+
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ applyNavbarSigninPadding();
+ checkFirstRun();
+ checkNowPlaying();
+ fetchRewards();
+
+ // check (and start) the LBRY SDK service
serviceRunning = isServiceRunning(this, LbrynetService.class);
if (!serviceRunning) {
- CurrentLaunchTiming.setColdStart(true);
+ Lbry.SDK_READY = false;
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
ServiceHelper.start(this, "", LbrynetService.class, "lbrynetservice");
}
checkSdkReady();
+ showSignedInUser();
+ checkPendingOpens();
- checkNotificationOpenIntent(getIntent());
+ if (Lbry.SDK_READY) {
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
+ }
+ }
- mReactRootView = new RNGestureHandlerEnabledRootView(this);
- mReactInstanceManager = ReactInstanceManager.builder()
- .setApplication(getApplication())
- .setCurrentActivity(this)
- .setBundleAssetName("index.android.bundle")
- .setJSMainModulePath("index")
- .addPackage(new MainReactPackage())
- .addPackage(new AsyncStoragePackage())
- .addPackage(new FastImageViewPackage())
- .addPackage(new RNCWebViewPackage())
- .addPackage(new ReactVideoPackage())
- .addPackage(new ReanimatedPackage())
- .addPackage(new RNCameraPackage())
- .addPackage(new RNFetchBlobPackage())
- .addPackage(new RNFSPackage())
- .addPackage(new RNGestureHandlerPackage())
- .addPackage(new SnackbarPackage())
- .addPackage(new LbryReactPackage())
- .setUseDeveloperSupport(BuildConfig.DEBUG)
- .setInitialLifecycleState(LifecycleState.RESUMED)
- .build();
- mReactRootView.startReactApplication(mReactInstanceManager, "LBRYApp", null);
+ private void checkPendingOpens() {
+ if (pendingFollowingReload) {
+ loadFollowingContent();
+ pendingFollowingReload = false;
+ }
+ if (!Helper.isNullOrEmpty(pendingAllContentTag)) {
+ openAllContentFragmentWithTag(pendingAllContentTag);
+ pendingAllContentTag = null;
+ } else if (!Helper.isNullOrEmpty(pendingChannelUrl)) {
+ openChannelUrl(pendingChannelUrl);
+ pendingChannelUrl = null;
+ } else if (pendingOpenWalletPage) {
+ openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
+ } else if (pendingOpenRewardsPage) {
+ openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
+ }
+ }
- registerNotificationsReceiver();
+ @Override
+ protected void onPause() {
+ if (!enteringPIPMode && appPlayer != null) {
+ appPlayer.setPlayWhenReady(false);
+ }
+ super.onPause();
+ }
- setContentView(mReactRootView);
+ public static void suspendGlobalPlayer(Context context) {
+ if (MainActivity.appPlayer != null) {
+ MainActivity.appPlayer.setPlayWhenReady(false);
+ }
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).hideGlobalNowPlaying();
+ }
+ }
+ public static void resumeGlobalPlayer(Context context) {
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).checkNowPlaying();
+ }
+ }
+
+ private void toggleUrlSuggestions(boolean visible) {
+ View container = findViewById(R.id.url_suggestions_container);
+ View closeIcon = findViewById(R.id.wunderbar_close);
+ EditText wunderbar = findViewById(R.id.wunderbar);
+ wunderbar.setPadding(0, 0, visible ? getScaledValue(36) : 0, 0);
+
+ container.setVisibility(visible ? View.VISIBLE : View.GONE);
+ closeIcon.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ public int getScaledValue(int value) {
+ float scale = getResources().getDisplayMetrics().density;
+ return Helper.getScaledValue(value, scale);
+ }
+
+
+ public boolean canShowUrlSuggestions() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ return sp.getBoolean(MainActivity.PREFERENCE_KEY_SHOW_URL_SUGGESTIONS, false);
+ }
+
+ public boolean keepSdkBackground() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ return sp.getBoolean(MainActivity.PREFERENCE_KEY_KEEP_SDK_BACKGROUND, true);
+ }
+
+ private void setupUriBar() {
+ findViewById(R.id.wunderbar_close).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ clearWunderbarFocus(view);
+ }
+ });
+
+ EditText wunderbar = findViewById(R.id.wunderbar);
+ wunderbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (hasFocus) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.showSoftInput(view, 0);
+ }
+
+ if (canShowUrlSuggestions()) {
+ toggleUrlSuggestions(hasFocus);
+ if (hasFocus && Helper.isNullOrEmpty(Helper.getValue(((EditText) view).getText()))) {
+ displayUrlSuggestionsForNoInput();
+ }
+ }
+ }
+ });
+
+ wunderbar.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) {
+ if (charSequence != null && canShowUrlSuggestions()) {
+ handleUriInputChanged(charSequence.toString().trim());
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+
+ }
+ });
+ wunderbar.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
+ if (actionId == EditorInfo.IME_ACTION_GO) {
+ String input = Helper.getValue(wunderbar.getText());
+ boolean handled = false;
+ if (input.startsWith(LbryUri.PROTO_DEFAULT) && !input.equalsIgnoreCase(LbryUri.PROTO_DEFAULT)) {
+ try {
+ LbryUri uri = LbryUri.parse(input);
+ if (uri.isChannel()) {
+ openChannelUrl(uri.toString());
+ clearWunderbarFocus(wunderbar);
+ handled = true;
+ } else {
+ openFileUrl(uri.toString());
+ clearWunderbarFocus(wunderbar);
+ handled = true;
+ }
+ } catch (LbryUriException ex) {
+ // pass
+ }
+ }
+ if (!handled) {
+ // search
+ launchSearch(input);
+ clearWunderbarFocus(wunderbar);
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ });
+
+ urlSuggestionListAdapter = new UrlSuggestionListAdapter(this);
+ urlSuggestionListAdapter.setListener(new UrlSuggestionListAdapter.UrlSuggestionClickListener() {
+ @Override
+ public void onUrlSuggestionClicked(UrlSuggestion urlSuggestion) {
+ switch (urlSuggestion.getType()) {
+ case UrlSuggestion.TYPE_CHANNEL:
+ // open channel page
+ if (urlSuggestion.getClaim() != null) {
+ openChannelClaim(urlSuggestion.getClaim());
+ } else {
+ openChannelUrl(urlSuggestion.getUri().toString());
+ }
+ break;
+ case UrlSuggestion.TYPE_FILE:
+ if (urlSuggestion.getClaim() != null) {
+ openFileClaim(urlSuggestion.getClaim());
+ } else {
+ openFileUrl(urlSuggestion.getUri().toString());
+ }
+ break;
+ case UrlSuggestion.TYPE_SEARCH:
+ launchSearch(urlSuggestion.getText());
+ break;
+ case UrlSuggestion.TYPE_TAG:
+ // open tag page
+ openAllContentFragmentWithTag(urlSuggestion.getText());
+ break;
+ }
+ clearWunderbarFocus(findViewById(R.id.wunderbar));
+ }
+ });
+
+ RecyclerView urlSuggestionList = findViewById(R.id.url_suggestions);
+ LinearLayoutManager llm = new LinearLayoutManager(this);
+ urlSuggestionList.setLayoutManager(llm);
+ urlSuggestionList.setAdapter(urlSuggestionListAdapter);
+ }
+
+ public void clearWunderbarFocus(View view) {
+ findViewById(R.id.wunderbar).clearFocus();
+ findViewById(R.id.app_bar_main_container).requestFocus();
+ InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ public View getWunderbar() {
+ return findViewById(R.id.wunderbar);
+ }
+
+ private void launchSearch(String text) {
+ Fragment currentFragment = getCurrentFragment();
+ if (currentFragment instanceof SearchFragment) {
+ ((SearchFragment) currentFragment).search(text, 0);
+ } else {
+ try {
+ SearchFragment fragment = SearchFragment.class.newInstance();
+ fragment.setCurrentQuery(text);
+ openFragment(fragment, true);
+ } catch (Exception ex) {
+ // pass
+ }
+ }
+ }
+
+ private void resolveUrlSuggestions(List urls) {
+ 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) {
+ for (int i = 0; i < claims.size(); i++) {
+ // build a simple url from the claim for matching
+ Claim claim = claims.get(i);
+ Claim actualClaim = claim;
+ boolean isRepost = false;
+ if (Claim.TYPE_REPOST.equalsIgnoreCase(claim.getValueType())) {
+ actualClaim = claim.getRepostedClaim();
+ isRepost = true;
+ }
+ if (Helper.isNullOrEmpty(claim.getName())) {
+ continue;
+ }
+
+ LbryUri simpleUrl = new LbryUri();
+ if (actualClaim.getName().startsWith("@") && !isRepost) {
+ // channel
+ simpleUrl.setChannelName(actualClaim.getName());
+ } else {
+ simpleUrl.setStreamName(claim.getName());
+ }
+
+ urlSuggestionListAdapter.setClaimForUrl(simpleUrl, actualClaim);
+ }
+ urlSuggestionListAdapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+
+ }
+ });
+ task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ private void displayUrlSuggestionsForNoInput() {
+ urlSuggestionListAdapter.clear();
+ loadDefaultSuggestionsForBlankUrl();
+ }
+
+ private void handleUriInputChanged(String text) {
+ // build the default suggestions
+ urlSuggestionListAdapter.clear();
+ if (Helper.isNullOrEmpty(text) || text.trim().equals("@")) {
+ displayUrlSuggestionsForNoInput();
+ return;
+ }
+
+ List defaultSuggestions = buildDefaultSuggestions(text);
+ urlSuggestionListAdapter.addUrlSuggestions(defaultSuggestions);
+ if (LbryUri.PROTO_DEFAULT.equalsIgnoreCase(text)) {
+ return;
+ }
+
+ LighthouseAutoCompleteTask task = new LighthouseAutoCompleteTask(text, null, new LighthouseAutoCompleteTask.AutoCompleteResultHandler() {
+ @Override
+ public void onSuccess(List suggestions) {
+ String wunderBarText = Helper.getValue(((EditText) findViewById(R.id.wunderbar)).getText());
+ if (wunderBarText.equalsIgnoreCase(text)) {
+ urlSuggestionListAdapter.addUrlSuggestions(suggestions);
+ List urls = urlSuggestionListAdapter.getItemUrls();
+ resolveUrlSuggestions(urls);
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void loadDefaultSuggestionsForBlankUrl() {
+ if (recentUrlHistory != null && recentUrlHistory.size() > 0) {
+ urlSuggestionListAdapter.addUrlSuggestions(recentUrlHistory);
+ }
+
+ FetchRecentUrlHistoryTask task = new FetchRecentUrlHistoryTask(DatabaseHelper.getInstance(), new FetchRecentUrlHistoryTask.FetchRecentUrlHistoryHandler() {
+ @Override
+ public void onSuccess(List recentHistory) {
+ List suggestions = new ArrayList<>(recentHistory);
+ List lbrySuggestions = buildLbryUrlSuggestions();
+ if (suggestions.size() < 10) {
+ for (int i = suggestions.size(), j = 0; i < 10 && j < lbrySuggestions.size(); i++, j++) {
+ suggestions.add(lbrySuggestions.get(j));
+ }
+ } else if (suggestions.size() == 0) {
+ suggestions.addAll(lbrySuggestions);
+ }
+
+ for (UrlSuggestion suggestion : suggestions) {
+ suggestion.setUseTextAsDescription(true);
+ }
+
+ recentUrlHistory = new ArrayList<>(suggestions);
+ urlSuggestionListAdapter.clear();
+ urlSuggestionListAdapter.addUrlSuggestions(recentUrlHistory);
+ List urls = urlSuggestionListAdapter.getItemUrls();
+ resolveUrlSuggestions(urls);
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private List buildLbryUrlSuggestions() {
+ List suggestions = new ArrayList<>();
+ suggestions.add(new UrlSuggestion(
+ UrlSuggestion.TYPE_FILE, "What is LBRY?", LbryUri.tryParse("lbry://what#19b9c243bea0c45175e6a6027911abbad53e983e")));
+ suggestions.add(new UrlSuggestion(
+ UrlSuggestion.TYPE_CHANNEL, "LBRYCast", LbryUri.tryParse("lbry://@lbrycast#4c29f8b013adea4d5cca1861fb2161d5089613ea")));
+ suggestions.add(new UrlSuggestion(
+ UrlSuggestion.TYPE_CHANNEL, "The LBRY Channel", LbryUri.tryParse("lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a")));
+ for (UrlSuggestion suggestion : suggestions) {
+ suggestion.setUseTextAsDescription(true);
+ }
+ return suggestions;
+ }
+
+ private List buildDefaultSuggestions(String text) {
+ List suggestions = new ArrayList();
+
+ if (LbryUri.PROTO_DEFAULT.equalsIgnoreCase(text)) {
+ loadDefaultSuggestionsForBlankUrl();
+ return recentUrlHistory != null ? recentUrlHistory : new ArrayList<>();
+ }
+
+ // First item is always search
+ if (!text.startsWith(LbryUri.PROTO_DEFAULT)) {
+ UrlSuggestion searchSuggestion = new UrlSuggestion(UrlSuggestion.TYPE_SEARCH, text);
+ suggestions.add(searchSuggestion);
+ }
+
+ if (!text.matches(LbryUri.REGEX_INVALID_URI)) {
+ boolean isUrlWithScheme = text.startsWith(LbryUri.PROTO_DEFAULT);
+ boolean isChannel = text.startsWith("@");
+ LbryUri uri = null;
+ if (isUrlWithScheme && text.length() > 7) {
+ try {
+ uri = LbryUri.parse(text);
+ isChannel = uri.isChannel();
+ } catch (LbryUriException ex) {
+ // pass
+ }
+ }
+
+ if (!isChannel) {
+ if (uri == null) {
+ uri = new LbryUri();
+ uri.setStreamName(text);
+ }
+ UrlSuggestion fileSuggestion = new UrlSuggestion(UrlSuggestion.TYPE_FILE, text);
+ fileSuggestion.setUri(uri);
+ suggestions.add(fileSuggestion);
+ }
+
+ if (text.indexOf(' ') == -1) {
+ // channels should not contain spaces
+ if (isChannel) {
+ if (uri == null) {
+ uri = new LbryUri();
+ uri.setChannelName(text);
+ }
+ UrlSuggestion suggestion = new UrlSuggestion(UrlSuggestion.TYPE_CHANNEL, text);
+ suggestion.setUri(uri);
+ suggestions.add(suggestion);
+ }
+ }
+
+ if (!isUrlWithScheme && !isChannel) {
+ UrlSuggestion suggestion = new UrlSuggestion(UrlSuggestion.TYPE_TAG, text);
+ suggestions.add(suggestion);
+ }
+ }
+
+ return suggestions;
+ }
+
+ public void checkNowPlaying() {
+ Fragment fragment = getCurrentFragment();
+ if (fragment instanceof FileViewFragment) {
+ return;
+ }
+
+ if (nowPlayingClaim != null) {
+ findViewById(R.id.global_now_playing_card).setVisibility(View.VISIBLE);
+ ((TextView) findViewById(R.id.global_now_playing_title)).setText(nowPlayingClaim.getTitle());
+ ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(nowPlayingClaim.getPublisherTitle());
+ }
+ if (appPlayer != null) {
+ PlayerView playerView = findViewById(R.id.global_now_playing_player_view);
+ playerView.setPlayer(null);
+ playerView.setPlayer(appPlayer);
+ playerView.setUseController(false);
+ playerReassigned = true;
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ public void hideGlobalNowPlaying() {
+ findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
+ }
+
+ public void unsetFitsSystemWindows(View view) {
+ view.setFitsSystemWindows(false);
+ }
+
+ public void enterFullScreenMode() {
+ hideFloatingWalletBalance();
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
+ findViewById(R.id.app_bar_main_container).setFitsSystemWindows(false);
+
+ View decorView = getWindow().getDecorView();
+ decorView.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
+ View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+
+ public int getStatusBarHeight() {
+ int height = 0;
+ int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ height = getResources().getDimensionPixelSize(resourceId);
+ }
+ return height;
+ }
+
+ public void exitFullScreenMode() {
+ View appBarMainContainer = findViewById(R.id.app_bar_main_container);
+ View decorView = getWindow().getDecorView();
+ int flags = isDarkMode() ? (View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE) :
+ (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE);
+ appBarMainContainer.setFitsSystemWindows(true);
+ decorView.setSystemUiVisibility(flags);
+
+ if (!Lbry.SDK_READY) {
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.VISIBLE);
+ }
+ showFloatingWalletBalance();
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+
+ private void initKeyStore() {
+ try {
+ Lbry.KEYSTORE = Utils.initKeyStore(this);
+ } catch (Exception ex) {
+ // This shouldn't happen, but in case it does.
+ Toast.makeText(this, "The keystore could not be initialized. The app requires a secure keystore to run properly.", Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }
+
+ private void checkFirstRun() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean firstRunCompleted = sp.getBoolean(PREFERENCE_KEY_INTERNAL_FIRST_RUN_COMPLETED, false);
+ if (!firstRunCompleted) {
+ startActivity(new Intent(this, FirstRunActivity.class));
+ } else if (!appStarted) {
+ // first run completed, startup
+ startup();
+ return;
+ }
+
+ if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
+ openFragment(FollowingFragment.class, false, NavMenuItem.ID_ITEM_FOLLOWING);
+ }
+ }
+
+ public static boolean isServiceRunning(Context context, Class> serviceClass) {
+ ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
+ if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void loadAuthToken() {
+ // Check if an auth token is present and then set it for Lbryio
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ String encryptedAuthToken = sp.getString(PREFERENCE_KEY_AUTH_TOKEN, null);
+ if (!Helper.isNullOrEmpty(encryptedAuthToken)) {
+ try {
+ Lbryio.AUTH_TOKEN = new String(Utils.decrypt(
+ Base64.decode(encryptedAuthToken, Base64.NO_WRAP), this, Lbry.KEYSTORE), "UTF8");
+ } catch (Exception ex) {
+ // pass. A new auth token would have to be generated if the old one cannot be decrypted
+ Log.e(TAG, "Could not decrypt existing auth token.", ex);
+ }
+ }
}
private void checkSdkReady() {
- if (!lbrySdkReady) {
+ if (!Lbry.SDK_READY) {
+ new Handler().postDelayed(() -> {
+ if (checkSdkReadyTask != null && checkSdkReadyTask.getStatus() != AsyncTask.Status.FINISHED) {
+ // task already running
+ return;
+ }
+ checkSdkReadyTask = new CheckSdkReadyTask(MainActivity.this, sdkStatusListeners);
+ checkSdkReadyTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }, CHECK_SDK_READY_INTERVAL);
+ } else {
+ scheduleWalletBalanceUpdate();
+ scheduleWalletSyncTask();
+ initFloatingWalletBalance();
+ }
+ }
+
+ public void onSdkReady() {
+ if (Lbryio.isSignedIn()) {
+ checkSyncedWallet();
+ }
+
+ findViewById(R.id.global_sdk_initializing_status).setVisibility(View.GONE);
+
+ syncWalletAndLoadPreferences();
+ scheduleWalletBalanceUpdate();
+ scheduleWalletSyncTask();
+ fetchOwnChannels();
+ fetchOwnClaims();
+
+ initFloatingWalletBalance();
+
+ checkAndClaimNewAndroidReward();
+ }
+
+ public void checkAndClaimNewAndroidReward() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean rewardClaimed = sp.getBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, false);
+ if (!rewardClaimed) {
+ ClaimRewardTask task = new ClaimRewardTask(
+ Reward.TYPE_NEW_ANDROID,
+ null,
+ null,
+ this,
+ new ClaimRewardTask.ClaimRewardHandler() {
+ @Override
+ public void onSuccess(double amountClaimed, String message) {
+ if (Helper.isNullOrEmpty(message)) {
+ message = getResources().getQuantityString(
+ R.plurals.claim_reward_message,
+ amountClaimed == 1 ? 1 : 2,
+ new DecimalFormat(Helper.LBC_CURRENCY_FORMAT_PATTERN).format(amountClaimed));
+ }
+ Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).show();
+ if (sp != null) {
+ sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, true).apply();
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ // pass. fail silently
+ if (sp != null) {
+ sp.edit().putBoolean(PREFERENCE_KEY_INTERNAL_NEW_ANDROID_REWARD_CLAIMED, true).apply();
+ }
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ public void initMediaSession() {
+ ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
+ mediaSession = new MediaSessionCompat(getApplicationContext(), "LBRYMediaSession", mediaButtonReceiver, null);
+ MediaSessionConnector connector = new MediaSessionConnector(mediaSession);
+ connector.setPlayer(MainActivity.appPlayer);
+ mediaSession.setActive(true);
+ }
+
+ public void showFloatingWalletBalance() {
+ findViewById(R.id.floating_balance_main_container).setVisibility(View.VISIBLE);
+ }
+ public void hideFloatingWalletBalance() {
+ findViewById(R.id.floating_balance_main_container).setVisibility(View.GONE);
+ }
+ public void hideFloatingRewardsValue() {
+ findViewById(R.id.floating_reward_container).setVisibility(View.INVISIBLE);
+ }
+
+ private void initFloatingWalletBalance() {
+ findViewById(R.id.floating_balance_container).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ openFragment(WalletFragment.class, true, NavMenuItem.ID_ITEM_WALLET);
+ }
+ });
+ findViewById(R.id.floating_reward_container).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ openFragment(RewardsFragment.class, true, NavMenuItem.ID_ITEM_REWARDS);
+ }
+ });
+ }
+
+ private void updateUsdWalletBalanceInNav() {
+ double usdBalance = Lbry.walletBalance.getAvailable().doubleValue() * Lbryio.LBCUSDRate;
+ if (navMenuAdapter != null) {
+ navMenuAdapter.setExtraLabelForItem(
+ NavMenuItem.ID_ITEM_WALLET,
+ Lbryio.LBCUSDRate > 0 ? String.format("$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(usdBalance)) : null
+ );
+ }
+ }
+
+ private void updateFloatingWalletBalance() {
+ if (!hasLoadedFirstBalance) {
+ findViewById(R.id.floating_balance_loading).setVisibility(View.GONE);
+ findViewById(R.id.floating_balance_value).setVisibility(View.VISIBLE);
+ hasLoadedFirstBalance = true;
+ }
+
+ ((TextView) findViewById(R.id.floating_balance_value)).setText(Helper.shortCurrencyFormat(
+ Lbry.walletBalance == null ? 0 : Lbry.walletBalance.getAvailable().doubleValue()));
+ }
+
+ private void scheduleWalletBalanceUpdate() {
+ if (scheduler != null && !walletBalanceUpdateScheduled) {
+ scheduler.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ updateWalletBalance();
+ }
+ }, 0, 5, TimeUnit.SECONDS);
+ walletBalanceUpdateScheduled = true;
+ }
+ }
+
+ private void scheduleWalletSyncTask() {
+ if (scheduler != null && !walletSyncScheduled) {
+ scheduler.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
+ syncWalletAndLoadPreferences();
+ }
+ }, 0, 5, TimeUnit.MINUTES);
+ walletSyncScheduled = true;
+ }
+ }
+
+ public void saveSharedUserState() {
+ if (!userSyncEnabled()) {
+ return;
+ }
+ SaveSharedUserStateTask saveTask = new SaveSharedUserStateTask(new SaveSharedUserStateTask.SaveSharedUserStateHandler() {
+ @Override
+ public void onSuccess() {
+ // push wallet sync changes
+ pushCurrentWalletSync();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ // pass
+ }
+ });
+ saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void loadSharedUserState() {
+ // load wallet preferences
+ LoadSharedUserStateTask loadTask = new LoadSharedUserStateTask(MainActivity.this, new LoadSharedUserStateTask.LoadSharedUserStateHandler() {
+ @Override
+ public void onSuccess(List subscriptions, List followedTags) {
+ if (subscriptions != null && subscriptions.size() > 0) {
+ // reload subscriptions if wallet fragment is FollowingFragment
+ //openNavFragments.get
+ MergeSubscriptionsTask mergeTask = new MergeSubscriptionsTask(
+ subscriptions, MainActivity.this, new MergeSubscriptionsTask.MergeSubscriptionsHandler() {
+ @Override
+ public void onSuccess(List subscriptions, List diff) {
+ Lbryio.subscriptions = new ArrayList<>(subscriptions);
+ if (diff != null && diff.size() > 0) {
+ saveSharedUserState();
+ }
+ for (Fragment fragment : openNavFragments.values()) {
+ if (fragment instanceof FollowingFragment) {
+ // reload local subscriptions
+ ((FollowingFragment) fragment).fetchLoadedSubscriptions(true);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ Log.e(TAG, String.format("merge subscriptions failed: %s", error.getMessage()), error);
+ }
+ });
+ mergeTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ if (followedTags != null && followedTags.size() > 0) {
+ List previousTags = new ArrayList<>(Lbry.followedTags);
+ Lbry.followedTags = new ArrayList<>(followedTags);
+ for (Fragment fragment : openNavFragments.values()) {
+ if (fragment instanceof AllContentFragment) {
+ AllContentFragment acFragment = (AllContentFragment) fragment;
+ if (!acFragment.isSingleTagView() &&
+ acFragment.getCurrentContentScope() == ContentScopeDialogFragment.ITEM_TAGS &&
+ !previousTags.equals(followedTags)) {
+ acFragment.fetchClaimSearchContent(true);
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ Log.e(TAG, String.format("load shared user state failed: %s", error != null ? error.getMessage() : "no error message"), error);
+ }
+ });
+ loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void pushCurrentWalletSync() {
+ String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
+ SyncApplyTask fetchTask = new SyncApplyTask(true, password, new DefaultSyncTaskHandler() {
+ @Override
+ public void onSyncApplySuccess(String hash, String data) {
+ SyncSetTask setTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, null);
+ setTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ @Override
+ public void onSyncApplyError(Exception error) { }
+ });
+ fetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private boolean userSyncEnabled() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean walletSyncEnabled = sp.getBoolean(PREFERENCE_KEY_INTERNAL_WALLET_SYNC_ENABLED, false);
+ return walletSyncEnabled && Lbryio.isSignedIn();
+ }
+
+ public void syncSet(String hash, String data) {
+ if (syncSetTask == null || syncSetTask.getStatus() == AsyncTask.Status.FINISHED) {
+ syncSetTask = new SyncSetTask(Lbryio.lastRemoteHash, hash, data, new DefaultSyncTaskHandler() {
+ @Override
+ public void onSyncSetSuccess(String hash) {
+ Lbryio.lastRemoteHash = hash;
+ WalletSync walletSync = new WalletSync(hash, data);
+ Lbryio.lastWalletSync = walletSync;
+
+ if (pendingSyncSetQueue.size() > 0) {
+ fullSyncInProgress = true;
+ WalletSync nextSync = pendingSyncSetQueue.remove(0);
+ syncSet(nextSync.getHash(), nextSync.getData());
+ } else if (queuedSyncCount > 0) {
+ queuedSyncCount--;
+ syncApplyAndSet();
+ }
+
+ fullSyncInProgress = false;
+ }
+ @Override
+ public void onSyncSetError(Exception error) {
+ // log app exceptions
+ if (pendingSyncSetQueue.size() > 0) {
+ WalletSync nextSync = pendingSyncSetQueue.remove(0);
+ syncSet(nextSync.getHash(), nextSync.getData());
+ } else if (queuedSyncCount > 0) {
+ queuedSyncCount--;
+ syncApplyAndSet();
+ }
+
+ fullSyncInProgress = false;
+ }
+ });
+ syncSetTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ WalletSync pending = new WalletSync(hash, data);
+ pendingSyncSetQueue.add(pending);
+ }
+ }
+
+ public void syncApplyAndSet() {
+ fullSyncInProgress = true;
+ String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
+ SyncApplyTask fetchTask = new SyncApplyTask(true, password, new DefaultSyncTaskHandler() {
+ @Override
+ public void onSyncApplySuccess(String hash, String data) {
+ if (!hash.equalsIgnoreCase(Lbryio.lastRemoteHash)) {
+ syncSet(hash, data);
+ } else {
+ fullSyncInProgress = false;
+ queuedSyncCount = 0;
+ }
+ }
+ @Override
+ public void onSyncApplyError(Exception error) {
+ fullSyncInProgress = false;
+ if (queuedSyncCount > 0) {
+ queuedSyncCount--;
+ syncApplyAndSet();
+ }
+ }
+ });
+ fetchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void syncWalletAndLoadPreferences() {
+ if (!userSyncEnabled()) {
+ return;
+ }
+ if (fullSyncInProgress) {
+ queuedSyncCount++;
+ }
+
+ fullSyncInProgress = true;
+ String password = Utils.getSecureValue(SECURE_VALUE_KEY_SAVED_PASSWORD, this, Lbry.KEYSTORE);
+ SyncGetTask task = new SyncGetTask(password, true, null, new DefaultSyncTaskHandler() {
+ @Override
+ public void onSyncGetSuccess(WalletSync walletSync) {
+ Lbryio.lastWalletSync = walletSync;
+ Lbryio.lastRemoteHash = walletSync.getHash();
+ loadSharedUserState();
+ }
+
+ @Override
+ public void onSyncGetWalletNotFound() {
+ // pass. This actually shouldn't happen at this point.
+ // But if it does, send what we have
+ if (Lbryio.isSignedIn() && userSyncEnabled()) {
+ syncApplyAndSet();
+ }
+ }
+
+ @Override
+ public void onSyncGetError(Exception error) {
+ // pass
+ Log.e(TAG, String.format("sync get failed: %s", error != null ? error.getMessage() : "no error message"), error);
+
+ fullSyncInProgress = false;
+ if (queuedSyncCount > 0) {
+ queuedSyncCount--;
+ syncApplyAndSet();
+ }
+ }
+
+ @Override
+ public void onSyncApplySuccess(String hash, String data) {
+ if (!hash.equalsIgnoreCase(Lbryio.lastRemoteHash)) {
+ syncSet(hash, data);
+ } else {
+ fullSyncInProgress = false;
+ queuedSyncCount = 0;
+ }
+
+ loadSharedUserState();
+ }
+
+ @Override
+ public void onSyncApplyError(Exception error) {
+ // pass
+ Log.e(TAG, String.format("sync apply failed: %s", error != null ? error.getMessage() : "no error message"), error);
+ fullSyncInProgress = false;
+ if (queuedSyncCount > 0) {
+ queuedSyncCount--;
+ syncApplyAndSet();
+ }
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void registerRequestsReceiver() {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(ACTION_AUTH_TOKEN_GENERATED);
+ intentFilter.addAction(ACTION_USER_SIGN_IN_SUCCESS);
+ intentFilter.addAction(ACTION_OPEN_ALL_CONTENT_TAG);
+ intentFilter.addAction(ACTION_OPEN_CHANNEL_URL);
+ intentFilter.addAction(ACTION_OPEN_WALLET_PAGE);
+ intentFilter.addAction(ACTION_OPEN_REWARDS_PAGE);
+ intentFilter.addAction(ACTION_SAVE_SHARED_USER_STATE);
+ requestsReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (ACTION_AUTH_TOKEN_GENERATED.equalsIgnoreCase(action)) {
+ handleAuthTokenGenerated(intent);
+ } else if (ACTION_USER_SIGN_IN_SUCCESS.equalsIgnoreCase(action)) {
+ handleUserSignInSuccess(intent);
+ } else if (ACTION_OPEN_ALL_CONTENT_TAG.equalsIgnoreCase(action)) {
+ handleOpenContentTag(intent);
+ } else if (ACTION_OPEN_CHANNEL_URL.equalsIgnoreCase(action)) {
+ handleOpenChannelUrl(intent);
+ } else if (ACTION_OPEN_WALLET_PAGE.equalsIgnoreCase(action)) {
+ pendingOpenWalletPage = true;
+ } else if (ACTION_OPEN_REWARDS_PAGE.equalsIgnoreCase(action)) {
+ pendingOpenRewardsPage = true;
+ } else if (ACTION_SAVE_SHARED_USER_STATE.equalsIgnoreCase(action)) {
+ saveSharedUserState();
+ }
+ }
+
+ private void handleAuthTokenGenerated(Intent intent) {
+ // store the value
+ String encryptedAuthToken = intent.getStringExtra("authToken");
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
+ sp.edit().putString(PREFERENCE_KEY_AUTH_TOKEN, encryptedAuthToken).apply();
+ }
+
+ private void handleOpenContentTag(Intent intent) {
+ String tag = intent.getStringExtra("tag");
+ if (!Helper.isNullOrEmpty(tag)) {
+ pendingAllContentTag = tag;
+ }
+ }
+ private void handleUserSignInSuccess(Intent intent) {
+ pendingFollowingReload = true;
+ }
+ private void handleOpenChannelUrl(Intent intent) {
+ String url = intent.getStringExtra("url");
+ pendingChannelUrl = url;
+ }
+ };
+ registerReceiver(requestsReceiver, intentFilter);
+ }
+
+ private void loadFollowingContent() {
+ for (Fragment fragment : openNavFragments.values()) {
+ if (fragment instanceof FollowingFragment) {
+ ((FollowingFragment) fragment).loadFollowing();
+ }
+ }
+ }
+ 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();
+ }
+ public void showError(String message) {
+ Snackbar.make(findViewById(R.id.content_main), message, Snackbar.LENGTH_LONG).
+ setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
+ }
+
+ @Override
+ public void onBackPressed() {
+
+ if (findViewById(R.id.url_suggestions_container).getVisibility() == View.VISIBLE) {
+ clearWunderbarFocus(findViewById(R.id.wunderbar));
+ return;
+ }
+ if (backPressInterceptor != null && backPressInterceptor.onBackPressed()) {
+ return;
+ }
+
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ if (drawer.isDrawerOpen(GravityCompat.START)) {
+ 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()) {
+ if (fragment instanceof ChannelFormFragment) {
+ channelFormFragment = ((ChannelFormFragment) fragment);
+ break;
+ }
+ if (fragment instanceof PublishFormFragment) {
+ publishFormFragment = ((PublishFormFragment) fragment);
+ break;
+ }
+ }
+ if (channelFormFragment != null && channelFormFragment.isSaveInProgress()) {
+ handled = true;
+ return;
+ }
+ 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
+ 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;
+ }
+ }
+ }
+ }
+
+ public void simpleSignIn() {
+ Intent intent = new Intent(this, VerificationActivity.class);
+ intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_SIGN_IN);
+ startActivityForResult(intent, REQUEST_SIMPLE_SIGN_IN);
+ }
+
+ public void walletSyncSignIn() {
+ Intent intent = new Intent(this, VerificationActivity.class);
+ intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_WALLET);
+ startActivityForResult(intent, REQUEST_WALLET_SYNC_SIGN_IN);
+ }
+
+ public void rewardsSignIn() {
+ Intent intent = new Intent(this, VerificationActivity.class);
+ intent.putExtra("flow", VerificationActivity.VERIFICATION_FLOW_REWARDS);
+ startActivityForResult(intent, REQUEST_REWARDS_VERIFY_SIGN_IN);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
+ switch (requestCode) {
+ case REQUEST_STORAGE_PERMISSION:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ for (StoragePermissionListener listener : storagePermissionListeners) {
+ listener.onStoragePermissionGranted();
+ }
+ } else {
+ for (StoragePermissionListener listener : storagePermissionListeners) {
+ listener.onStoragePermissionRefused();
+ }
+ }
+ startingPermissionRequest = false;
+ break;
+
+ case REQUEST_CAMERA_PERMISSION:
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ for (CameraPermissionListener listener : cameraPermissionListeners) {
+ listener.onCameraPermissionGranted();
+ }
+ } else {
+ for (CameraPermissionListener listener : cameraPermissionListeners) {
+ listener.onCameraPermissionRefused();
+ }
+ }
+ startingPermissionRequest = false;
+ break;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_FILE_PICKER) {
+ startingFilePickerActivity = false;
+ if (resultCode == RESULT_OK) {
+ Uri fileUri = data.getData();
+ String filePath = Helper.getRealPathFromURI_API19(this, fileUri);
+ for (FilePickerListener listener : filePickerListeners) {
+ listener.onFilePicked(filePath);
+ }
+ } else {
+ for (FilePickerListener listener : filePickerListeners) {
+ listener.onFilePickerCancelled();
+ }
+ }
+ } 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();
+ }
+ }
+ } else if (requestCode == REQUEST_VIDEO_CAPTURE || requestCode == REQUEST_TAKE_PHOTO) {
+ if (resultCode == RESULT_OK) {
+ PublishFragment publishFragment = null;
+ for (Fragment fragment : openNavFragments.values()) {
+ if (fragment instanceof PublishFragment) {
+ publishFragment = (PublishFragment) fragment;
+ break;
+ }
+ }
+
+ Map params = new HashMap<>();
+ params.put("directFilePath", cameraOutputFilename);
+ if (publishFragment != null) {
+ params.put("suggestedUrl", publishFragment.getSuggestedPublishUrl());
+ }
+ 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();
+
+ View signInButton = findViewById(R.id.sign_in_button_container);
+ View signedInEmailContainer = findViewById(R.id.signed_in_email_container);
+ signInButton.setPadding(0, statusBarHeight, 0, 0);
+ signedInEmailContainer.setPadding(0, statusBarHeight, 0, 0);
+ }
+
+ private void showSignedInUser() {
+ if (Lbryio.isSignedIn()) {
+ findViewById(R.id.sign_in_button_container).setVisibility(View.GONE);
+ findViewById(R.id.signed_in_email_container).setVisibility(View.VISIBLE);
+ ((TextView) findViewById(R.id.signed_in_email)).setText(Lbryio.getSignedInEmail());
+ findViewById(R.id.sign_in_header_divider).setBackgroundColor(getResources().getColor(R.color.lightDivider));
+ }
+ }
+
+ private Fragment getCurrentFragment() {
+ int backCount = getSupportFragmentManager().getBackStackEntryCount();
+ if (backCount > 0) {
+ try {
+ Fragment fragment = getSupportFragmentManager().getFragments().get(backCount - 1);
+ return fragment;
+ } catch (IndexOutOfBoundsException ex) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public void hideActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
+ }
+ public void showActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.show();
+ }
+ }
+ private void renderStartupFailed(Map startupStages) {
+ Map startupStageIconIds = new HashMap<>();
+ startupStageIconIds.put(STARTUP_STAGE_INSTALL_ID_LOADED, R.id.startup_stage_icon_install_id);
+ startupStageIconIds.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, R.id.startup_stage_icon_known_tags);
+ startupStageIconIds.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, R.id.startup_stage_icon_exchange_rate);
+ startupStageIconIds.put(STARTUP_STAGE_USER_AUTHENTICATED, R.id.startup_stage_icon_user_authenticated);
+ startupStageIconIds.put(STARTUP_STAGE_NEW_INSTALL_DONE, R.id.startup_stage_icon_install_new);
+ startupStageIconIds.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, R.id.startup_stage_icon_subscriptions_loaded);
+ startupStageIconIds.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, R.id.startup_stage_icon_subscriptions_resolved);
+
+ for (Integer key : startupStages.keySet()) {
+ boolean stageDone = startupStages.get(key);
+ ImageView icon = findViewById(startupStageIconIds.get(key));
+ icon.setImageResource(stageDone ? R.drawable.ic_check : R.drawable.ic_close);
+ icon.setColorFilter(stageDone ? Color.WHITE : Color.RED);
+ }
+
+ findViewById(R.id.splash_view_loading_container).setVisibility(View.GONE);
+ findViewById(R.id.splash_view_error_container).setVisibility(View.VISIBLE);
+ }
+
+ private void startup() {
+ final Context context = this;
+ Lbry.startupInit();
+
+ // perform some tasks before launching
+ (new AsyncTask() {
+ private Map startupStages = new HashMap<>();
+
+ private void initStartupStages() {
+ startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, false);
+ startupStages.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, false);
+ startupStages.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, false);
+ startupStages.put(STARTUP_STAGE_USER_AUTHENTICATED, false);
+ startupStages.put(STARTUP_STAGE_NEW_INSTALL_DONE, false);
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, false);
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, false);
+ }
+ protected void onPreExecute() {
+ hideActionBar();
+ lockDrawer();
+ findViewById(R.id.splash_view).setVisibility(View.VISIBLE);
+ LbryAnalytics.setCurrentScreen(MainActivity.this, "Splash", "Splash");
+ initStartupStages();
+ }
+ protected Boolean doInBackground(Void... params) {
+ BufferedReader reader = null;
+ try {
+ // Load the installation id from the file system
+ String lbrynetDir = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet");
+ String installIdPath = String.format("%s/install_id", lbrynetDir);
+ reader = new BufferedReader(new InputStreamReader(new FileInputStream(installIdPath)));
+ String installId = reader.readLine();
+ if (Helper.isNullOrEmpty(installId)) {
+ // no install_id found (first run didn't start the sdk successfully?)
+ startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, false);
+ return false;
+ }
+
+ Lbry.INSTALLATION_ID = installId;
+ startupStages.put(STARTUP_STAGE_INSTALL_ID_LOADED, true);
+
+ SQLiteDatabase db = dbHelper.getReadableDatabase();
+ List fetchedTags = DatabaseHelper.getTags(db);
+ Lbry.knownTags = Helper.mergeKnownTags(fetchedTags);
+ Collections.sort(Lbry.knownTags, new Tag());
+ Lbry.followedTags = Helper.filterFollowedTags(Lbry.knownTags);
+ startupStages.put(STARTUP_STAGE_KNOWN_TAGS_LOADED, true);
+
+ // load the exchange rate
+ Lbryio.loadExchangeRate();
+ if (Lbryio.LBCUSDRate == 0) {
+ return false;
+ }
+ startupStages.put(STARTUP_STAGE_EXCHANGE_RATE_LOADED, true);
+
+ Lbryio.authenticate(context);
+ if (Lbryio.currentUser == null) {
+ throw new Exception("Did not retrieve authenticated user.");
+ }
+ startupStages.put(STARTUP_STAGE_USER_AUTHENTICATED, true);
+
+ Lbryio.newInstall(context);
+ startupStages.put(STARTUP_STAGE_NEW_INSTALL_DONE, true);
+
+ // (light) fetch subscriptions
+ if (Lbryio.subscriptions.size() == 0) {
+ List subscriptions = new ArrayList<>();
+ List subUrls = new ArrayList<>();
+ JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
+ if (array != null) {
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject item = array.getJSONObject(i);
+ String claimId = item.getString("claim_id");
+ String channelName = item.getString("channel_name");
+
+ LbryUri url = new LbryUri();
+ url.setChannelName(channelName);
+ url.setClaimId(claimId);
+ subscriptions.add(new Subscription(channelName, url.toString()));
+ subUrls.add(url.toString());
+ }
+ Lbryio.subscriptions = subscriptions;
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true);
+
+ // resolve subscriptions
+ if (subUrls.size() > 0 && Lbryio.cacheResolvedSubscriptions.size() != Lbryio.subscriptions.size()) {
+ List resolvedSubs = Lbry.resolve(subUrls, Lbry.LBRY_TV_CONNECTION_STRING);
+ Lbryio.cacheResolvedSubscriptions = resolvedSubs;
+ }
+ // if no exceptions occurred here, subscriptions have been loaded and resolved
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true);
+ } else {
+ // user has not subscribed to anything
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true);
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true);
+ }
+ } else {
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_LOADED, true);
+ startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true);
+ }
+ } catch (Exception ex) {
+ // nope
+ android.util.Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex);
+ return false;
+ } finally {
+ Helper.closeCloseable(reader);
+ }
+
+ return true;
+ }
+ protected void onPostExecute(Boolean startupSuccessful) {
+ if (!startupSuccessful) {
+ // show which startup stage failed
+ renderStartupFailed(startupStages);
+ appStarted = false;
+ return;
+ }
+
+ findViewById(R.id.splash_view).setVisibility(View.GONE);
+ unlockDrawer();
+ showActionBar();
+
+ if (navMenuAdapter != null) {
+ navMenuAdapter.setCurrentItem(NavMenuItem.ID_ITEM_FOLLOWING);
+ }
+
+ loadLastFragment();
+ showSignedInUser();
+ fetchRewards();
+
+ checkUrlIntent(getIntent());
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_APP_LAUNCH);
+ appStarted = true;
+ }
+ }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void fetchRewards() {
+ FetchRewardsTask task = new FetchRewardsTask(null, new FetchRewardsTask.FetchRewardsHandler() {
+ @Override
+ public void onSuccess(List rewards) {
+ Lbryio.updateRewardsLists(rewards);
+ for (Fragment fragment : openNavFragments.values()) {
+ if (fragment instanceof RewardsFragment) {
+ ((RewardsFragment) fragment).updateUnclaimedRewardsValue();
+ }
+ }
+
+ if (Lbryio.totalUnclaimedRewardAmount > 0) {
+ showFloatingUnclaimedRewards();
+ updateRewardsUsdVale();
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void updateRewardsUsdVale() {
+ if (Lbryio.totalUnclaimedRewardAmount > 0) {
+ double usdRewardAmount = Lbryio.totalUnclaimedRewardAmount * Lbryio.LBCUSDRate;
+ if (navMenuAdapter != null) {
+ navMenuAdapter.setExtraLabelForItem(
+ NavMenuItem.ID_ITEM_REWARDS,
+ Lbryio.LBCUSDRate > 0 ? String.format("$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(usdRewardAmount)) : null
+ );
+ }
+ }
+ }
+
+ public void showFloatingUnclaimedRewards() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean notInterestedInRewards = sp.getBoolean(PREFERENCE_KEY_INTERNAL_REWARDS_NOT_INTERESTED, false);
+ if (notInterestedInRewards) {
+ return;
+ }
+
+ ((TextView) findViewById(R.id.floating_reward_value)).setText(Helper.shortCurrencyFormat(Lbryio.totalUnclaimedRewardAmount));
+ findViewById(R.id.floating_reward_container).setVisibility(View.VISIBLE);
+ }
+
+ private void checkUrlIntent(Intent intent) {
+ if (intent != null) {
+ Uri data = intent.getData();
+ if (data != null) {
+ String url = data.toString();
+ // check special urls
+ if (url.startsWith("lbry://?")) {
+ String specialPath = url.substring(8);
+ if (specialRouteFragmentClassMap.containsKey(specialPath)) {
+ Class fragmentClass = specialRouteFragmentClassMap.get(specialPath);
+ if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
+ Map params = new HashMap<>();
+ String tag = intent.getStringExtra("tag");
+ params.put("singleTag", tag);
+
+ openFragment(
+ specialRouteFragmentClassMap.get(specialPath),
+ true,
+ fragmentClassNavIdMap.get(fragmentClass),
+ !Helper.isNullOrEmpty(tag) ? params : null
+ );
+ }
+ }
+
+ // unrecognised path will open the following by default
+ } else {
+ try {
+ LbryUri uri = LbryUri.parse(url);
+ if (uri.isChannel()) {
+ openChannelUrl(uri.toString());
+ } else {
+ openFileUrl(uri.toString());
+ }
+ } catch (LbryUriException ex) {
+ // pass
+ }
+ }
+ }
+ }
+ }
+
+ private void loadLastFragment() {
+ Fragment fragment = getCurrentFragment();
+
+ if (fragment != null) {
+ openFragment(fragment, true);
+ } else {
+ openFragment(FollowingFragment.class, false, NavMenuItem.ID_ITEM_FOLLOWING);
+ }
+ }
+
+ private void setSelectedNavMenuItemForFragment(Fragment fragment) {
+ if (fragment == null) {
+ // assume the first fragment is selected
+ navMenuAdapter.setCurrentItem(NavMenuItem.ID_ITEM_FOLLOWING);
+ return;
+ }
+
+ Class fragmentClass = fragment.getClass();
+ if (fragmentClassNavIdMap.containsKey(fragmentClass)) {
+ navMenuAdapter.setCurrentItem(fragmentClassNavIdMap.get(fragmentClass));
+ }
+ }
+
+ @Override
+ protected void onUserLeaveHint() {
+ if (startingShareActivity) {
+ // share activity triggered this, so reset the flag at this point
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
- if (checkSdkReadyTask != null && checkSdkReadyTask.getStatus() != AsyncTask.Status.FINISHED) {
- // task already running
- return;
- }
- checkSdkReadyTask = new CheckSdkReadyTask();
- checkSdkReadyTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ startingShareActivity = false;
}
}, 1000);
+ return;
}
+ if (startingPermissionRequest) {
+ return;
+ }
+ enterPIPMode();
+ }
+
+ protected boolean enterPIPMode() {
+ if (enteringPIPMode) {
+ return true;
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+ appPlayer != null &&
+ !startingFilePickerActivity &&
+ !startingSignInFlowActivity) {
+ enteringPIPMode = true;
+
+ getSupportActionBar().hide();
+ findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
+ findViewById(R.id.pip_player).setVisibility(View.VISIBLE);
+
+ PictureInPictureParams params = new PictureInPictureParams.Builder().build();
+ enterPictureInPictureMode(params);
+ return true;
+ }
+
+ return false;
}
private void checkNotificationOpenIntent(Intent intent) {
@@ -200,71 +2402,15 @@ public class MainActivity extends FragmentActivity implements DefaultHardwareBac
}
private void logNotificationOpen(String name) {
- if (firebaseAnalytics == null) {
- firebaseAnalytics = FirebaseAnalytics.getInstance(this);
- }
-
Bundle bundle = new Bundle();
bundle.putString("name", name);
- firebaseAnalytics.logEvent("lbry_notification_open", bundle);
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_LBRY_NOTIFICATION_OPEN, bundle);
}
- private void registerDownloadEventReceiver() {
- IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
- downloadEventReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String downloadAction = intent.getStringExtra("action");
- String uri = intent.getStringExtra("uri");
- String outpoint = intent.getStringExtra("outpoint");
- String fileInfoJson = intent.getStringExtra("file_info");
-
-
- if (uri == null || outpoint == null || (fileInfoJson == null && !"abort".equals(downloadAction))) {
- return;
- }
-
- String eventName = null;
- WritableMap params = Arguments.createMap();
- params.putString("uri", uri);
- params.putString("outpoint", outpoint);
-
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if ("abort".equals(downloadAction)) {
- eventName = "onDownloadAborted";
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
- }
- return;
- }
-
- try {
- JSONObject json = new JSONObject(fileInfoJson);
- WritableMap fileInfo = JSONObjectToMap(json);
- params.putMap("fileInfo", fileInfo);
-
- if (DownloadManager.ACTION_UPDATE.equals(downloadAction)) {
- double progress = intent.getDoubleExtra("progress", 0);
- params.putDouble("progress", progress);
- eventName = "onDownloadUpdated";
- } else {
- eventName = (DownloadManager.ACTION_START.equals(downloadAction)) ? "onDownloadStarted" : "onDownloadCompleted";
- }
-
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
- }
- } catch (JSONException ex) {
- // pass
- }
- }
- };
- registerReceiver(downloadEventReceiver, intentFilter);
- }
private void registerServiceActionsReceiver() {
IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
intentFilter.addAction(LbrynetService.LBRY_SDK_SERVICE_STARTED);
intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE);
serviceActionsReceiver = new BroadcastReceiver() {
@@ -284,12 +2430,30 @@ public class MainActivity extends FragmentActivity implements DefaultHardwareBac
notificationManager.notify(1, svcNotification);
}
}, 1000);
+ } else if (DownloadManager.ACTION_DOWNLOAD_EVENT.equalsIgnoreCase(action)) {
+ String downloadAction = intent.getStringExtra("action");
+ String uri = intent.getStringExtra("uri");
+ String outpoint = intent.getStringExtra("outpoint");
+ String fileInfoJson = intent.getStringExtra("file_info");
+ double progress = intent.getDoubleExtra("progress", 0);
+ if (uri == null || outpoint == null || (fileInfoJson == null && !"abort".equals(downloadAction))) {
+ return;
+ }
+
+ for (DownloadActionListener listener : downloadActionListeners) {
+ listener.onDownloadAction(downloadAction, uri, outpoint, fileInfoJson, progress);
+ }
}
}
};
registerReceiver(serviceActionsReceiver, intentFilter);
}
+ private void unregisterReceivers() {
+ Helper.unregisterReceiver(requestsReceiver, this);
+ Helper.unregisterReceiver(serviceActionsReceiver, this);
+ }
+
private Notification buildServiceNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, LbrynetService.NOTIFICATION_CHANNEL_ID);
Intent contextIntent = new Intent(this, MainActivity.class);
@@ -312,556 +2476,82 @@ public class MainActivity extends FragmentActivity implements DefaultHardwareBac
return notification;
}
- private void registerNotificationsReceiver() {
- // Background media receiver
- IntentFilter filter = new IntentFilter();
- filter.addAction(BackgroundMediaModule.ACTION_PLAY);
- filter.addAction(BackgroundMediaModule.ACTION_PAUSE);
- notificationsReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if (reactContext != null) {
- if (BackgroundMediaModule.ACTION_PLAY.equals(action)) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onBackgroundPlayPressed", null);
- }
- if (BackgroundMediaModule.ACTION_PAUSE.equals(action)) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onBackgroundPausePressed", null);
- }
- }
- }
- };
- registerReceiver(notificationsReceiver, filter);
+ private static List buildNavMenu(Context context) {
+ NavMenuItem findContentGroup = new NavMenuItem(NavMenuItem.ID_GROUP_FIND_CONTENT, R.string.find_content, true, context);
+ NavMenuItem yourContentGroup = new NavMenuItem(NavMenuItem.ID_GROUP_YOUR_CONTENT, R.string.your_content, true, context);
+ NavMenuItem walletGroup = new NavMenuItem(NavMenuItem.ID_GROUP_WALLET, R.string.wallet, true, context);
+ NavMenuItem otherGroup = new NavMenuItem(NavMenuItem.ID_GROUP_OTHER, 0, true, context);
+
+ findContentGroup.setItems(Arrays.asList(
+ new NavMenuItem(NavMenuItem.ID_ITEM_FOLLOWING, R.string.fa_heart, R.string.following, "Following", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_EDITORS_CHOICE, R.string.fa_star, R.string.editors_choice, "EditorsChoice", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_ALL_CONTENT, R.string.fa_globe_americas, R.string.all_content, "AllContent", context)
+ ));
+
+ yourContentGroup.setItems(Arrays.asList(
+ new NavMenuItem(NavMenuItem.ID_ITEM_NEW_PUBLISH, R.string.fa_upload, R.string.new_publish, "NewPublish", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_CHANNELS, R.string.fa_at, R.string.channels, "Channels", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_LIBRARY, R.string.fa_download, R.string.library, "Library", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_PUBLISHES, R.string.fa_cloud_upload, R.string.publishes, "Publishes", context)
+ ));
+
+ walletGroup.setItems(Arrays.asList(
+ new NavMenuItem(NavMenuItem.ID_ITEM_WALLET, R.string.fa_wallet, R.string.wallet, "Wallet", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_REWARDS, R.string.fa_award, R.string.rewards, "Rewards", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_INVITES, R.string.fa_user_friends, R.string.invites, "Invites", context)
+ ));
+
+ otherGroup.setItems(Arrays.asList(
+ new NavMenuItem(NavMenuItem.ID_ITEM_SETTINGS, R.string.fa_cog, R.string.settings, "Settings", context),
+ new NavMenuItem(NavMenuItem.ID_ITEM_ABOUT, R.string.fa_mobile_alt, R.string.about, "About", context)
+ ));
+
+ return Arrays.asList(findContentGroup, yourContentGroup, walletGroup, otherGroup);
}
- public void registerSmsReceiver() {
- if (!hasPermission(Manifest.permission.RECEIVE_SMS, this)) {
- // don't create the receiver if we don't have the read sms permission
- return;
- }
-
- IntentFilter smsFilter = new IntentFilter();
- smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
- smsReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- // Get the message
- Bundle bundle = intent.getExtras();
- if (bundle != null) {
- Object[] pdus = (Object[]) bundle.get("pdus");
- if (pdus != null && pdus.length > 0) {
- SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdus[0]);
- String text = sms.getMessageBody();
- if (text == null || text.trim().length() == 0) {
- return;
- }
-
- // Retrieve verification code from the text message if it contains
- // the strings "lbry", "verification code" and the colon (following the expected format)
- text = text.toLowerCase();
- if (text.indexOf("lbry") > -1 && text.indexOf("verification code") > -1 && text.indexOf(":") > -1) {
- String code = text.substring(text.lastIndexOf(":") + 1).trim();
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if (reactContext != null) {
- WritableMap params = Arguments.createMap();
- params.putString("code", code);
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onVerificationCodeReceived", params);
- }
- }
- }
- }
- }
- };
- registerReceiver(smsReceiver, smsFilter);
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- if (!Settings.canDrawOverlays(this)) {
- // SYSTEM_ALERT_WINDOW permission not granted...
+ // Flatten the structure into a single list for the RecyclerView
+ private static List flattenNavMenu(List navMenuItems) {
+ List flatMenu = new ArrayList<>();
+ for (NavMenuItem item : navMenuItems) {
+ flatMenu.add(item);
+ if (item.getItems() != null) {
+ for (NavMenuItem subItem : item.getItems()) {
+ flatMenu.add(subItem);
}
}
}
- if (requestCode == DOCUMENT_PICKER_RESULT_CODE) {
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if (reactContext != null) {
- if (resultCode == RESULT_OK) {
- Uri fileUri = data.getData();
- String filePath = getRealPathFromURI_API19(this, fileUri);
- WritableMap params = Arguments.createMap();
- params.putString("path", filePath);
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onDocumentPickerFilePicked", params);
- } else if (resultCode == RESULT_CANCELED) {
- // user canceled or request failed
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onDocumentPickerCanceled", null);
- }
- }
+ return flatMenu;
+ }
+
+ public void setNowPlayingClaim(Claim claim, String url) {
+ nowPlayingClaim = claim;
+ nowPlayingClaimUrl = url;
+ if (claim != null) {
+ ((TextView) findViewById(R.id.global_now_playing_title)).setText(nowPlayingClaim.getTitle());
+ ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(nowPlayingClaim.getPublisherTitle());
}
}
- public static Activity getActivity() {
- Activity activity = new Activity();
- activity = currentActivity;
- return activity;
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- switch (requestCode) {
- case STORAGE_PERMISSION_REQ_CODE:
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- if (BuildConfig.DEBUG && Build.VERSION.SDK_INT >= 23 && !Settings.canDrawOverlays(this)) {
- Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
- Uri.parse("package:" + getPackageName()));
- startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
- }
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onStoragePermissionGranted", null);
- }
- } else {
- // Permission not granted
- /*Toast.makeText(this,
- "LBRY requires access to your device storage to be able to download files and media." +
- " Please enable the storage permission and restart the app.", Toast.LENGTH_LONG).show();*/
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onStoragePermissionRefused", null);
- }
- }
- break;
-
- case PHONE_STATE_PERMISSION_REQ_CODE:
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // Permission granted. Emit an onPhoneStatePermissionGranted event
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onPhoneStatePermissionGranted", null);
- }
- } else {
- // Permission not granted. Simply show a message.
- Toast.makeText(this,
- "No permission granted to read your device state. Rewards cannot be claimed.", Toast.LENGTH_LONG).show();
- }
- break;
-
- case RECEIVE_SMS_PERMISSION_REQ_CODE:
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- // Permission granted. Emit an onPhoneStatePermissionGranted event
- if (reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
- .emit("onReceiveSmsPermissionGranted", null);
- }
-
- // register the receiver
- if (smsReceiver == null) {
- registerSmsReceiver();
- }
- } else {
- // Permission not granted. Simply show a message.
- Toast.makeText(this,
- "No permission granted to receive your SMS messages. You may have to enter the verification code manually.",
- Toast.LENGTH_LONG).show();
- }
- break;
- }
-
- if (permissionListener != null) {
- permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ public void clearNowPlayingClaim() {
+ nowPlayingClaim = null;
+ nowPlayingClaimUrl = null;
+ findViewById(R.id.global_now_playing_card).setVisibility(View.GONE);
+ ((TextView) findViewById(R.id.global_now_playing_title)).setText(null);
+ ((TextView) findViewById(R.id.global_now_playing_channel_title)).setText(null);
+ if (appPlayer != null) {
+ appPlayer.setPlayWhenReady(false);
}
}
- @Override
- public void invokeDefaultOnBackPressed() {
- super.onBackPressed();
- }
+ private static class CheckSdkReadyTask extends AsyncTask {
+ private Context context;
+ private List listeners;
- @Override
- protected void onPause() {
- super.onPause();
-
- if (mReactInstanceManager != null) {
- mReactInstanceManager.onHostPause(this);
+ public CheckSdkReadyTask(Context context, List listeners) {
+ this.context = context;
+ this.listeners = new ArrayList<>(listeners);
}
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- SharedPreferences sp = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- LbrynetService.setDHTEnabled(sp.getBoolean(UtilityModule.DHT_ENABLED, false));
-
- serviceRunning = isServiceRunning(this, LbrynetService.class);
- if (!serviceRunning) {
- ServiceHelper.start(this, "", LbrynetService.class, "lbrynetservice");
- }
- checkSdkReady();
-
- if (mReactInstanceManager != null) {
- mReactInstanceManager.onHostResume(this, this);
- }
- }
-
- @Override
- protected void onDestroy() {
- // check service running setting and end it here
- SharedPreferences sp = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- boolean shouldKeepDaemonRunning = sp.getBoolean(SETTING_KEEP_DAEMON_RUNNING, true);
- if (!shouldKeepDaemonRunning) {
- serviceRunning = isServiceRunning(this, LbrynetService.class);
- if (serviceRunning) {
- ServiceHelper.stop(this, LbrynetService.class);
- }
- }
-
- if (notificationsReceiver != null) {
- unregisterReceiver(notificationsReceiver);
- notificationsReceiver = null;
- }
-
- if (smsReceiver != null) {
- unregisterReceiver(smsReceiver);
- smsReceiver = null;
- }
-
- if (downloadEventReceiver != null) {
- unregisterReceiver(downloadEventReceiver);
- downloadEventReceiver = null;
- }
-
- if (serviceActionsReceiver != null) {
- unregisterReceiver(serviceActionsReceiver);
- serviceActionsReceiver = null;
- }
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
- notificationManager.cancel(BackgroundMediaModule.NOTIFICATION_ID);
- notificationManager.cancel(DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID);
- if (downloadNotificationIds != null) {
- for (int i = 0; i < downloadNotificationIds.size(); i++) {
- notificationManager.cancel(downloadNotificationIds.get(i));
- }
- }
- if (receivedStopService || !isServiceRunning(this, LbrynetService.class)) {
- notificationManager.cancelAll();
- }
- super.onDestroy();
-
- if (mReactInstanceManager != null) {
- mReactInstanceManager.onHostDestroy(this);
- }
- }
-
- @Override
- public void onBackPressed() {
- if (mReactInstanceManager != null) {
- mReactInstanceManager.onBackPressed();
- } else {
- super.onBackPressed();
- }
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- public void requestPermissions(String[] permissions, int requestCode, PermissionListener listener) {
- permissionListener = listener;
- ActivityCompat.requestPermissions(this, permissions, requestCode);
- }
-
- @Override
- public void onNewIntent(Intent intent) {
- if (mReactInstanceManager != null) {
- mReactInstanceManager.onNewIntent(intent);
- }
-
- if (intent != null) {
- int sourceNotificationId = intent.getIntExtra(SOURCE_NOTIFICATION_ID_KEY, -1);
- if (sourceNotificationId > -1) {
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
- notificationManager.cancel(sourceNotificationId);
- }
-
- checkNotificationOpenIntent(intent);
- }
-
- super.onNewIntent(intent);
- }
-
- private static void checkPermission(String permission, int requestCode, String rationale, Context context, boolean forceRequest) {
- if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
- // Should we show an explanation?
- if (!forceRequest && ActivityCompat.shouldShowRequestPermissionRationale((Activity) context, permission)) {
- Toast.makeText(context, rationale, Toast.LENGTH_LONG).show();
- } else {
- ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
- }
- }
- }
-
- private static void checkPermission(String permission, int requestCode, String rationale, Context context) {
- checkPermission(permission, requestCode, rationale, context, false);
- }
-
- public static boolean hasPermission(String permission, Context context) {
- return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
- }
-
- public static void checkPhoneStatePermission(Context context) {
- // Request read phone state permission
- checkPermission(Manifest.permission.READ_PHONE_STATE,
- PHONE_STATE_PERMISSION_REQ_CODE,
- "LBRY requires optional access to be able to identify your device for rewards. " +
- "You cannot claim rewards without this permission.",
- context,
- true);
- }
-
- public static void checkReceiveSmsPermission(Context context) {
- // Request read phone state permission
- checkPermission(Manifest.permission.RECEIVE_SMS,
- RECEIVE_SMS_PERMISSION_REQ_CODE,
- "LBRY requires access to be able to read a verification text message for rewards.",
- context,
- true);
- }
-
- public static void checkStoragePermission(Context context) {
- // Request read phone state permission
- checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
- STORAGE_PERMISSION_REQ_CODE,
- "LBRY requires access to your device storage to be able to download files and media.",
- context,
- true);
- }
-
- public static boolean isServiceRunning(Context context, Class> serviceClass) {
- ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
- for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
- if (serviceClass.getName().equals(serviceInfo.service.getClassName())) {
- return true;
- }
- }
-
- return false;
- }
-
- private static WritableMap JSONObjectToMap(JSONObject jsonObject) throws JSONException {
- WritableMap map = Arguments.createMap();
- Iterator keys = jsonObject.keys();
- while(keys.hasNext()) {
- String key = keys.next();
- Object value = jsonObject.get(key);
- if (value instanceof JSONArray) {
- map.putArray(key, JSONArrayToList((JSONArray) value));
- } else if (value instanceof JSONObject) {
- map.putMap(key, JSONObjectToMap((JSONObject) value));
- } else if (value instanceof Boolean) {
- map.putBoolean(key, (Boolean) value);
- } else if (value instanceof Integer) {
- map.putInt(key, (Integer) value);
- } else if (value instanceof Double) {
- map.putDouble(key, (Double) value);
- } else if (value instanceof String) {
- map.putString(key, (String) value);
- } else {
- map.putString(key, value.toString());
- }
- }
-
- return map;
- }
-
- private static WritableArray JSONArrayToList(JSONArray jsonArray) throws JSONException {
- WritableArray array = Arguments.createArray();
- for(int i = 0; i < jsonArray.length(); i++) {
- Object value = jsonArray.get(i);
- if (value instanceof JSONArray) {
- array.pushArray(JSONArrayToList((JSONArray) value));
- } else if (value instanceof JSONObject) {
- array.pushMap(JSONObjectToMap((JSONObject) value));
- } else if (value instanceof Boolean) {
- array.pushBoolean((Boolean) value);
- } else if (value instanceof Integer) {
- array.pushInt((Integer) value);
- } else if (value instanceof Double) {
- array.pushDouble((Double) value);
- } else if (value instanceof String) {
- array.pushString((String) value);
- } else {
- array.pushString(value.toString());
- }
- }
-
- return array;
- }
-
- /**
- * https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8
- */
- @SuppressLint("NewApi")
- public static String getRealPathFromURI_API19(final Context context, final Uri uri) {
- 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) {
- return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName;
- }
-
- String id = DocumentsContract.getDocumentId(uri);
- if (id.startsWith("raw:")) {
- id = id.replaceFirst("raw:", "");
- File file = new File(id);
- if (file.exists())
- return id;
- }
-
- final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
- return getDataColumn(context, contentUri, null, null);
- }
- // 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 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;
- }
-
-
- 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;
- }
-
- /**
- * @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());
- }
-
- private class CheckSdkReadyTask extends AsyncTask {
public Boolean doInBackground(Void... params) {
boolean sdkReady = false;
@@ -870,20 +2560,13 @@ public class MainActivity extends FragmentActivity implements DefaultHardwareBac
if (response != null) {
JSONObject result = new JSONObject(response);
JSONObject status = result.getJSONObject("result");
-
- // send status response for splash page updates
- WritableMap sdkStatus = JSONObjectToMap(status);
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if (reactContext != null) {
- WritableMap evtParams = Arguments.createMap();
- evtParams.putMap("status", sdkStatus);
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onSdkStatusResponse", evtParams);
+ if (!Lbry.IS_STATUS_PARSED) {
+ Lbry.parseStatus(status.toString());
}
+ // TODO: Broadcast startup status changes
JSONObject startupStatus = status.getJSONObject("startup_status");
- sdkReady = startupStatus.has("stream_manager") && startupStatus.has("wallet") &&
- startupStatus.getBoolean("stream_manager") && startupStatus.getBoolean("wallet") &&
- (status.getJSONObject("wallet").getLong("blocks_behind") <= 0);
+ sdkReady = startupStatus.getBoolean("file_manager") && startupStatus.getBoolean("wallet");
}
} catch (ConnectException ex) {
// pass
@@ -894,37 +2577,224 @@ public class MainActivity extends FragmentActivity implements DefaultHardwareBac
return sdkReady;
}
protected void onPostExecute(Boolean sdkReady) {
- lbrySdkReady = sdkReady;
- ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
- if (sdkReady && reactContext != null) {
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onSdkReady", null);
+ Lbry.SDK_READY = sdkReady;
+ if (context != null) {
+ if (sdkReady) {
+ context.sendBroadcast(new Intent(ACTION_SDK_READY));
+
+ // update listeners
+ for (SdkStatusListener listener : listeners) {
+ if (listener != null) {
+ listener.onSdkReady();
+ }
+ }
+ } else if (context instanceof MainActivity) {
+ ((MainActivity) context).checkSdkReady();
+ }
+ }
+ }
+ }
+
+ public void showNavigationBackIcon() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ if (toggle != null) {
+ TypedArray a = getTheme().obtainStyledAttributes(R.style.AppTheme, new int[] {R.attr.homeAsUpIndicator});
+ int attributeResourceId = a.getResourceId(0, 0);
+ Drawable drawable = ResourcesCompat.getDrawable(getResources(), attributeResourceId, null);
+ DrawableCompat.setTint(drawable, ContextCompat.getColor(this, R.color.actionBarForeground));
+
+ toggle.setDrawerIndicatorEnabled(false);
+ toggle.setHomeAsUpIndicator(drawable);
+ }
+ }
+
+ private void closeDrawer() {
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ drawer.closeDrawer(GravityCompat.START);
+ }
+
+ public void lockDrawer() {
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
+ }
+
+ public void unlockDrawer() {
+ DrawerLayout drawer = findViewById(R.id.drawer_layout);
+ drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
+ }
+
+ public void restoreToggle() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(false);
+ actionBar.setTitle(null);
+ }
+ if (toggle != null) {
+ toggle.setDrawerIndicatorEnabled(true);
+ }
+ unlockDrawer();
+ showSearchBar();
+ }
+
+ public void hideSearchBar() {
+ findViewById(R.id.wunderbar_container).setVisibility(View.GONE);
+ }
+
+ public void showSearchBar() {
+ findViewById(R.id.wunderbar_container).setVisibility(View.VISIBLE);
+ clearWunderbarFocus(findViewById(R.id.wunderbar));
+ }
+
+ @Override
+ public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
+ inPictureInPictureMode = isInPictureInPictureMode;
+ enteringPIPMode = false;
+ if (isInPictureInPictureMode) {
+ // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
+ renderPictureInPictureMode();
+ } else {
+ // Restore the full-screen UI.
+ renderFullMode();
+ }
+ }
+
+ protected void onStop() {
+ if (appPlayer != null && inPictureInPictureMode) {
+ appPlayer.setPlayWhenReady(false);
+ }
+ super.onStop();
+ }
+
+ public void openFragment(Fragment fragment, boolean allowNavigateBack) {
+ Fragment currentFragment = getCurrentFragment();
+ if (currentFragment != null && currentFragment.equals(fragment)) {
+ return;
+ }
+
+ try {
+ FragmentManager manager = getSupportFragmentManager();
+ FragmentTransaction transaction = manager.beginTransaction().replace(R.id.content_main, fragment);
+ if (allowNavigateBack) {
+ transaction.addToBackStack(null);
+ }
+ transaction.commit();
+ } catch (Exception ex) {
+ // pass
+ }
+ }
+
+ public void openFragment(Class fragmentClass, boolean allowNavigateBack, int navItemId) {
+ openFragment(fragmentClass, allowNavigateBack, navItemId, null);
+ }
+
+ private static String buildNavFragmentKey(Class fragmentClass, int navItemId, Map params) {
+ if (params != null && params.containsKey("url")) {
+ return String.format("%s-%d-%s", fragmentClass.getName(), navItemId, params.get("url").toString());
+ }
+
+ return String.format("%s-%d", fragmentClass.getName(), navItemId);
+ }
+
+ public void openFragment(Class fragmentClass, boolean allowNavigateBack, int navItemId, Map params) {
+ try {
+ String key = buildNavFragmentKey(fragmentClass, navItemId, params);
+ Fragment fragment = openNavFragments.containsKey(key) ? openNavFragments.get(key) : (Fragment) fragmentClass.newInstance();
+ if (fragment instanceof BaseFragment) {
+ ((BaseFragment) fragment).setParams(params);
+ }
+ Fragment currentFragment = getCurrentFragment();
+ if (currentFragment != null && currentFragment.equals(fragment)) {
+ return;
}
- if (!sdkReady) {
- checkSdkReady();
+ //fragment.setRetainInstance(true);
+ FragmentManager manager = getSupportFragmentManager();
+ FragmentTransaction transaction = manager.beginTransaction().replace(R.id.content_main, fragment);
+ if (allowNavigateBack) {
+ transaction.addToBackStack(null);
+ }
+ transaction.commit();
+
+ if (navItemId > -1) {
+ openNavFragments.put(key, fragment);
+ }
+ } catch (Exception ex) {
+ // pass
+ }
+ }
+
+ public void fetchOwnChannels() {
+ ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, null, new ClaimListResultHandler() {
+ @Override
+ public void onSuccess(List claims) {
+ Lbry.ownChannels = Helper.filterDeletedClaims(new ArrayList<>(claims));
+ for (FetchChannelsListener listener : fetchChannelsListeners) {
+ listener.onChannelsFetched(claims);
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ // pass
+ }
+ });
+ 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));
+ for (FetchClaimsListener listener : fetchClaimsListeners) {
+ listener.onClaimsFetched(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
+ SyncGetTask task = new SyncGetTask(password, false, null, new DefaultSyncTaskHandler() {
+ @Override
+ public void onSyncGetSuccess(WalletSync walletSync) {
+ Lbryio.userHasSyncedWallet = true;
+ Lbryio.setLastWalletSync(walletSync);
+ Lbryio.setLastRemoteHash(walletSync.getHash());
+ }
+
+ @Override
+ public void onSyncGetWalletNotFound() { }
+ @Override
+ public void onSyncGetError(Exception error) { }
+ });
+ 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 {
+ startingPermissionRequest = true;
+ ActivityCompat.requestPermissions((Activity) context, new String[] { permission }, requestCode);
}
}
}
-
- public static class LaunchTiming {
- private Date start;
- private boolean coldStart;
-
- public LaunchTiming(Date start) {
- this.start = start;
- }
-
- public Date getStart() {
- return start;
- }
- public void setStart(Date start) {
- this.start = start;
- }
- public boolean isColdStart() {
- return coldStart;
- }
- public void setColdStart(boolean coldStart) {
- this.coldStart = coldStart;
- }
+
+ public static boolean hasPermission(String permission, Context context) {
+ return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
+ }
+
+ public interface BackPressInterceptor {
+ boolean onBackPressed();
}
}
diff --git a/app/src/main/java/io/lbry/browser/VerificationActivity.java b/app/src/main/java/io/lbry/browser/VerificationActivity.java
new file mode 100644
index 00000000..cf819cb6
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/VerificationActivity.java
@@ -0,0 +1,272 @@
+package io.lbry.browser;
+
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager2.widget.ViewPager2;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import java.util.Arrays;
+
+import io.lbry.browser.adapter.VerificationPagerAdapter;
+import io.lbry.browser.listener.SignInListener;
+import io.lbry.browser.listener.WalletSyncListener;
+import io.lbry.browser.model.lbryinc.User;
+import io.lbry.browser.tasks.lbryinc.FetchCurrentUserTask;
+import io.lbry.browser.utils.LbryAnalytics;
+import io.lbry.browser.utils.Lbryio;
+
+public class VerificationActivity extends FragmentActivity implements SignInListener, WalletSyncListener {
+
+ public static final int VERIFICATION_FLOW_SIGN_IN = 1;
+ public static final int VERIFICATION_FLOW_REWARDS = 2;
+ public static final int VERIFICATION_FLOW_WALLET = 3;
+
+ private String email;
+ private boolean signedIn;
+ private int flow;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ signedIn = Lbryio.isSignedIn();
+ Intent intent = getIntent();
+ if (intent != null) {
+ flow = intent.getIntExtra("flow", -1);
+ if (flow == -1 || (flow == VERIFICATION_FLOW_SIGN_IN && signedIn)) {
+ // no flow specified (or user is already signed in), just exit
+ setResult(signedIn ? RESULT_OK : RESULT_CANCELED);
+ finish();
+ return;
+ }
+ }
+
+ if (!Arrays.asList(VERIFICATION_FLOW_SIGN_IN, VERIFICATION_FLOW_REWARDS, VERIFICATION_FLOW_WALLET).contains(flow)) {
+ // invalid flow specified
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.activity_verification);
+ ViewPager2 viewPager = findViewById(R.id.verification_pager);
+ viewPager.setUserInputEnabled(false);
+ viewPager.setSaveEnabled(false);
+ viewPager.setAdapter(new VerificationPagerAdapter(this));
+
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+ findViewById(R.id.verification_close_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ }
+
+ public void onResume() {
+ super.onResume();
+ LbryAnalytics.setCurrentScreen(this, "Verification", "Verification");
+ checkFlow();
+ }
+
+ public void checkFlow() {
+ ViewPager2 viewPager = findViewById(R.id.verification_pager);
+ if (Lbryio.isSignedIn()) {
+ boolean flowHandled = false;
+ if (flow == VERIFICATION_FLOW_WALLET) {
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
+ flowHandled = true;
+ } else if (flow == VERIFICATION_FLOW_REWARDS) {
+ User user = Lbryio.currentUser;
+ if (!user.isIdentityVerified()) {
+ // phone number verification required
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
+ flowHandled = true;
+ } else if (!user.isRewardApproved()) {
+ // manual verification required
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
+ flowHandled = true;
+ }
+ }
+
+ if (!flowHandled) {
+ // user has already been verified and or reward approved
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ }
+ }
+
+ public void showLoading() {
+ findViewById(R.id.verification_loading_progress).setVisibility(View.VISIBLE);
+ findViewById(R.id.verification_pager).setVisibility(View.INVISIBLE);
+ findViewById(R.id.verification_close_button).setVisibility(View.GONE);
+ }
+
+ public void hideLoading() {
+ findViewById(R.id.verification_loading_progress).setVisibility(View.GONE);
+ findViewById(R.id.verification_pager).setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // ignore back press
+ return;
+ }
+
+ public void onEmailAdded(String email) {
+ this.email = email;
+ findViewById(R.id.verification_close_button).setVisibility(View.GONE);
+
+ Bundle bundle = new Bundle();
+ bundle.putString("email", email);
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_ADDED, bundle);
+ }
+ public void onEmailEdit() {
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+ }
+ public void onEmailVerified() {
+ Snackbar.make(findViewById(R.id.verification_pager), R.string.sign_in_successful, Snackbar.LENGTH_LONG).show();
+ sendBroadcast(new Intent(MainActivity.ACTION_USER_SIGN_IN_SUCCESS));
+
+ Bundle bundle = new Bundle();
+ bundle.putString("email", email);
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_EMAIL_VERIFIED, bundle);
+
+ if (flow == VERIFICATION_FLOW_SIGN_IN) {
+ final Intent resultIntent = new Intent();
+ resultIntent.putExtra("flow", VERIFICATION_FLOW_SIGN_IN);
+ resultIntent.putExtra("email", email);
+
+ // only sign in required, don't do anything else
+ showLoading();
+ FetchCurrentUserTask task = new FetchCurrentUserTask(new FetchCurrentUserTask.FetchUserTaskHandler() {
+ @Override
+ public void onSuccess(User user) {
+ Lbryio.currentUser = user;
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ showFetchUserError(error.getMessage());
+ hideLoading();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ // change pager view depending on flow
+ showLoading();
+ FetchCurrentUserTask task = new FetchCurrentUserTask(new FetchCurrentUserTask.FetchUserTaskHandler() {
+ @Override
+ public void onSuccess(User user) {
+ hideLoading();
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+
+ Lbryio.currentUser = user;
+ ViewPager2 viewPager = findViewById(R.id.verification_pager);
+ // for rewards, (show phone verification if not done, or manual verification if required)
+ if (flow == VERIFICATION_FLOW_REWARDS) {
+ if (!user.isIdentityVerified()) {
+ // phone number verification required
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
+ } else if (!user.isRewardApproved()) {
+ // manual verification required
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
+ } else {
+ // fully verified
+ setResult(RESULT_OK);
+ finish();
+ }
+ } else if (flow == VERIFICATION_FLOW_WALLET) {
+ // for wallet sync, if password unlock is required, show password entry page
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_WALLET, false);
+ }
+ }
+ @Override
+ public void onError(Exception error) {
+ showFetchUserError(error.getMessage());
+ hideLoading();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ @Override
+ public void onPhoneAdded(String countryCode, String phoneNumber) {
+
+ }
+
+ @Override
+ public void onPhoneVerified() {
+ showLoading();
+ FetchCurrentUserTask task = new FetchCurrentUserTask(new FetchCurrentUserTask.FetchUserTaskHandler() {
+ @Override
+ public void onSuccess(User user) {
+ Lbryio.currentUser = user;
+ if (user.isIdentityVerified() && user.isRewardApproved()) {
+ // verified for rewards
+ LbryAnalytics.logEvent(LbryAnalytics.EVENT_REWARD_ELIGIBILITY_COMPLETED);
+
+ setResult(RESULT_OK);
+ finish();
+ return;
+ }
+
+ // show manual verification page if the user is still not reward approved
+ ViewPager2 viewPager = findViewById(R.id.verification_pager);
+ viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
+ hideLoading();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ showFetchUserError(error.getMessage());
+ hideLoading();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void showFetchUserError(String message) {
+ Snackbar.make(findViewById(R.id.verification_pager), message, Snackbar.LENGTH_LONG).
+ setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
+ }
+
+ @Override
+ public void onManualVerifyContinue() {
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ @Override
+ public void onWalletSyncProcessing() {
+ findViewById(R.id.verification_close_button).setVisibility(View.GONE);
+ }
+ @Override
+ public void onWalletSyncWaitingForInput() {
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onWalletSyncEnabled() {
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+ setResult(RESULT_OK);
+ finish();
+ }
+
+ @Override
+ public void onWalletSyncFailed(Exception error) {
+ findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/ChannelFilterListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/ChannelFilterListAdapter.java
new file mode 100644
index 00000000..a4b0873d
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/ChannelFilterListAdapter.java
@@ -0,0 +1,125 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+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.model.Claim;
+import io.lbry.browser.listener.ChannelItemSelectionListener;
+import io.lbry.browser.utils.Helper;
+import lombok.Getter;
+import lombok.Setter;
+
+public class ChannelFilterListAdapter extends RecyclerView.Adapter {
+ private Context context;
+ private List items;
+ @Getter
+ @Setter
+ private Claim selectedItem;
+ @Setter
+ private ChannelItemSelectionListener listener;
+
+ public ChannelFilterListAdapter(Context context) {
+ this.context = context;
+ this.items = new ArrayList<>();
+
+ // Always list the placeholder as the first item
+ Claim claim = new Claim();
+ claim.setPlaceholder(true);
+ items.add(claim);
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected View mediaContainer;
+ protected View alphaContainer;
+ protected View allView;
+ protected ImageView thumbnailView;
+ protected TextView alphaView;
+ protected TextView titleView;
+ public ViewHolder(View v) {
+ super(v);
+ mediaContainer = v.findViewById(R.id.channel_filter_media_container);
+ alphaContainer = v.findViewById(R.id.channel_filter_no_thumbnail);
+ alphaView = v.findViewById(R.id.channel_filter_alpha_view);
+ thumbnailView = v.findViewById(R.id.channel_filter_thumbnail);
+ titleView = v.findViewById(R.id.channel_filter_title);
+ allView = v.findViewById(R.id.channel_filter_all_container);
+ }
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ public boolean isClaimSelected(Claim claim) {
+ return claim.equals(selectedItem);
+ }
+
+ public void clearClaims() {
+ items = new ArrayList<>(items.subList(0, 1));
+ notifyDataSetChanged();
+ }
+
+ public void addClaims(List claims) {
+ for (Claim claim : claims) {
+ if (!items.contains(claim)) {
+ items.add(claim);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public ChannelFilterListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_channel_filter, root, false);
+ return new ChannelFilterListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(ChannelFilterListAdapter.ViewHolder vh, int position) {
+ Claim claim = items.get(position);
+ vh.alphaView.setVisibility(claim.isPlaceholder() ? View.GONE : View.VISIBLE);
+ vh.titleView.setVisibility(claim.isPlaceholder() ? View.INVISIBLE : View.VISIBLE);
+ vh.allView.setVisibility(claim.isPlaceholder() ? View.VISIBLE : View.GONE);
+
+ vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
+ String thumbnailUrl = claim.getThumbnailUrl();
+ if (!Helper.isNullOrEmpty(thumbnailUrl) && context != null) {
+ Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
+ }
+ vh.alphaContainer.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
+ vh.thumbnailView.setVisibility(claim.isPlaceholder() || Helper.isNullOrEmpty(thumbnailUrl) ? View.GONE : View.VISIBLE);
+ vh.alphaView.setText(claim.isPlaceholder() ? null : claim.getName() != null ? claim.getName().substring(1, 2) : "");
+
+ int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
+ Helper.setIconViewBackgroundColor(vh.alphaContainer, bgColor, claim.isPlaceholder(), context);
+
+ vh.itemView.setSelected(isClaimSelected(claim));
+ vh.itemView.setOnClickListener(view -> {
+ if (claim.isPlaceholder()) {
+ selectedItem = null;
+ if (listener != null) {
+ listener.onChannelSelectionCleared();
+ }
+ } else if (!claim.equals(selectedItem)) {
+ selectedItem = claim;
+ if (listener != null) {
+ listener.onChannelItemSelected(claim);
+ }
+ }
+ notifyDataSetChanged();
+ });
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
new file mode 100644
index 00000000..ce8bbce3
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/ClaimListAdapter.java
@@ -0,0 +1,496 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.RequestOptions;
+import com.google.android.material.snackbar.Snackbar;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.lbry.browser.MainActivity;
+import io.lbry.browser.R;
+import io.lbry.browser.listener.SelectionModeListener;
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.model.LbryFile;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import io.lbry.browser.utils.Lbryio;
+import lombok.Getter;
+import lombok.Setter;
+
+public class ClaimListAdapter extends RecyclerView.Adapter {
+ private static final int VIEW_TYPE_STREAM = 1;
+ private static final int VIEW_TYPE_CHANNEL = 2;
+ private static final int VIEW_TYPE_FEATURED = 3; // featured search result
+
+ private Map quickClaimIdMap;
+ private Map quickClaimUrlMap;
+ private Map notFoundClaimIdMap;
+ private Map notFoundClaimUrlMap;
+
+ @Setter
+ private boolean hideFee;
+ @Setter
+ private boolean canEnterSelectionMode;
+ private Context context;
+ private List items;
+ private List selectedItems;
+ @Setter
+ private ClaimListItemListener listener;
+ @Getter
+ @Setter
+ private boolean inSelectionMode;
+ @Setter
+ private SelectionModeListener selectionModeListener;
+ private float scale;
+
+ public ClaimListAdapter(List items, Context context) {
+ this.context = context;
+ this.items = new ArrayList<>(items);
+ this.selectedItems = new ArrayList<>();
+ quickClaimIdMap = new HashMap<>();
+ quickClaimUrlMap = new HashMap<>();
+ notFoundClaimIdMap = new HashMap<>();
+ notFoundClaimUrlMap = new HashMap<>();
+ if (context != null) {
+ scale = context.getResources().getDisplayMetrics().density;
+ }
+ }
+
+ public List getSelectedItems() {
+ return this.selectedItems;
+ }
+ public int getSelectedCount() {
+ return selectedItems != null ? selectedItems.size() : 0;
+ }
+ public void clearSelectedItems() {
+ this.selectedItems.clear();
+ }
+ public boolean isClaimSelected(Claim claim) {
+ return selectedItems.contains(claim);
+ }
+
+ public Claim getFeaturedItem() {
+ for (Claim claim : items) {
+ if (claim.isFeatured()) {
+ return claim;
+ }
+ }
+ return null;
+ }
+
+ public void removeFeaturedItem() {
+ int featuredIndex = -1;
+ for (int i = 0; i < items.size(); i++) {
+ if (items.get(i).isFeatured()) {
+ featuredIndex = i;
+ break;
+ }
+ }
+ if (featuredIndex > -1) {
+ items.remove(featuredIndex);
+ }
+ }
+
+ public List getItems() {
+ return new ArrayList<>(this.items);
+ }
+
+ public void updateSigningChannelForClaim(Claim resolvedClaim) {
+ for (Claim claim : items) {
+ if (claim.getClaimId().equalsIgnoreCase(resolvedClaim.getClaimId())) {
+ claim.setSigningChannel(resolvedClaim.getSigningChannel());
+ }
+ }
+ }
+
+ public void clearItems() {
+ clearSelectedItems();
+ this.items.clear();
+ quickClaimIdMap.clear();
+ quickClaimUrlMap.clear();
+ notFoundClaimIdMap.clear();
+ notFoundClaimUrlMap.clear();
+ notifyDataSetChanged();
+ }
+
+ public Claim getLastItem() {
+ return items.size() > 0 ? items.get(items.size() - 1) : null;
+ }
+
+ public void addFeaturedItem(Claim claim) {
+ items.add(0, claim);
+ notifyDataSetChanged();
+ }
+
+ public void addItems(List claims) {
+ for (Claim claim : claims) {
+ if (!items.contains(claim)) {
+ items.add(claim);
+ }
+ }
+
+ notFoundClaimUrlMap.clear();
+ notFoundClaimIdMap.clear();
+ notifyDataSetChanged();
+ }
+ public void setItems(List claims) {
+ items = new ArrayList<>(claims);
+ notifyDataSetChanged();
+ }
+
+ public void removeItems(List claims) {
+ items.removeAll(claims);
+ notifyDataSetChanged();
+ }
+
+ public void removeItem(Claim claim) {
+ items.remove(claim);
+ selectedItems.remove(claim);
+ notifyDataSetChanged();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected View feeContainer;
+ protected TextView feeView;
+ protected ImageView thumbnailView;
+ protected View noThumbnailView;
+ protected TextView alphaView;
+ protected TextView vanityUrlView;
+ protected TextView durationView;
+ protected TextView titleView;
+ protected TextView publisherView;
+ protected TextView publishTimeView;
+ protected TextView pendingTextView;
+ protected View repostInfoView;
+ protected TextView repostChannelView;
+ protected View selectedOverlayView;
+ protected TextView fileSizeView;
+ protected ProgressBar downloadProgressView;
+ protected TextView deviceView;
+ public ViewHolder(View v) {
+ super(v);
+ feeContainer = v.findViewById(R.id.claim_fee_container);
+ feeView = v.findViewById(R.id.claim_fee);
+ alphaView = v.findViewById(R.id.claim_thumbnail_alpha);
+ noThumbnailView = v.findViewById(R.id.claim_no_thumbnail);
+ thumbnailView = v.findViewById(R.id.claim_thumbnail);
+ vanityUrlView = v.findViewById(R.id.claim_vanity_url);
+ durationView = v.findViewById(R.id.claim_duration);
+ titleView = v.findViewById(R.id.claim_title);
+ publisherView = v.findViewById(R.id.claim_publisher);
+ publishTimeView = v.findViewById(R.id.claim_publish_time);
+ pendingTextView = v.findViewById(R.id.claim_pending_text);
+ repostInfoView = v.findViewById(R.id.claim_repost_info);
+ repostChannelView = v.findViewById(R.id.claim_repost_channel);
+ selectedOverlayView = v.findViewById(R.id.claim_selected_overlay);
+ fileSizeView = v.findViewById(R.id.claim_file_size);
+ downloadProgressView = v.findViewById(R.id.claim_download_progress);
+ deviceView = v.findViewById(R.id.claim_view_device);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (items.get(position).isFeatured()) {
+ return VIEW_TYPE_FEATURED;
+ }
+
+ Claim claim = items.get(position);
+ String valueType = items.get(position).getValueType();
+ Claim actualClaim = Claim.TYPE_REPOST.equalsIgnoreCase(valueType) ? claim.getRepostedClaim() : claim;
+
+ return Claim.TYPE_CHANNEL.equalsIgnoreCase(actualClaim.getValueType()) ? VIEW_TYPE_CHANNEL : VIEW_TYPE_STREAM;
+ }
+
+ public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url) {
+ updateFileForClaimByIdOrUrl(file, claimId, url, false);
+ }
+ public void updateFileForClaimByIdOrUrl(LbryFile file, String claimId, String url, boolean skipNotFound) {
+ if (!skipNotFound) {
+ if (notFoundClaimIdMap.containsKey(claimId) && notFoundClaimUrlMap.containsKey(url)) {
+ return;
+ }
+ }
+ if (quickClaimIdMap.containsKey(claimId)) {
+ quickClaimIdMap.get(claimId).setFile(file);
+ notifyDataSetChanged();
+ return;
+ }
+ if (quickClaimUrlMap.containsKey(claimId)) {
+ quickClaimUrlMap.get(claimId).setFile(file);
+ notifyDataSetChanged();
+ return;
+ }
+
+ boolean claimFound = false;
+ for (int i = 0; i < items.size(); i++) {
+ Claim claim = items.get(i);
+ if (claimId.equalsIgnoreCase(claim.getClaimId()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
+ quickClaimIdMap.put(claimId, claim);
+ quickClaimUrlMap.put(url, claim);
+ claim.setFile(file);
+ notifyDataSetChanged();
+ claimFound = true;
+ break;
+ }
+ }
+
+ if (!claimFound) {
+ notFoundClaimIdMap.put(claimId, true);
+ notFoundClaimUrlMap.put(url, true);
+ }
+ }
+ public void clearFileForClaimOrUrl(String outpoint, String url) {
+ clearFileForClaimOrUrl(outpoint, url, false);
+ notifyDataSetChanged();
+ }
+
+
+ public void clearFileForClaimOrUrl(String outpoint, String url, boolean remove) {
+ int claimIndex = -1;
+ for (int i = 0; i < items.size(); i++) {
+ Claim claim = items.get(i);
+ if (outpoint.equalsIgnoreCase(claim.getOutpoint()) || url.equalsIgnoreCase(claim.getPermanentUrl())) {
+ claimIndex = i;
+ claim.setFile(null);
+ break;
+ }
+ }
+ if (remove && claimIndex > -1) {
+ Claim removed = items.remove(claimIndex);
+ selectedItems.remove(removed);
+ }
+
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public ClaimListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ int viewResourceId = -1;
+ switch (viewType) {
+ case VIEW_TYPE_FEATURED: viewResourceId = R.layout.list_item_featured_search_result; break;
+ case VIEW_TYPE_CHANNEL: viewResourceId = R.layout.list_item_channel; break;
+ case VIEW_TYPE_STREAM: default: viewResourceId = R.layout.list_item_stream; break;
+ }
+
+ View v = LayoutInflater.from(context).inflate(viewResourceId, parent, false);
+ return new ClaimListAdapter.ViewHolder(v);
+ }
+
+ public int getScaledValue(int value) {
+ return (int) (value * scale + 0.5f);
+ }
+
+ @Override
+ public void onBindViewHolder(ClaimListAdapter.ViewHolder vh, int position) {
+ int type = getItemViewType(position);
+ int paddingTop = position == 0 ? 16 : 8;
+ int paddingBottom = position == getItemCount() - 1 ? 16 : 8;
+ int paddingTopScaled = getScaledValue(paddingTop);
+ int paddingBottomScaled = getScaledValue(paddingBottom);
+ vh.itemView.setPadding(vh.itemView.getPaddingLeft(), paddingTopScaled, vh.itemView.getPaddingRight(), paddingBottomScaled);
+
+ Claim original = items.get(position);
+ boolean isRepost = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType());
+ final Claim item = Claim.TYPE_REPOST.equalsIgnoreCase(original.getValueType()) ? original.getRepostedClaim() : original;
+ Claim.GenericMetadata metadata = item.getValue();
+ Claim signingChannel = item.getSigningChannel();
+ Claim.StreamMetadata streamMetadata = null;
+ if (metadata instanceof Claim.StreamMetadata) {
+ streamMetadata = (Claim.StreamMetadata) metadata;
+ }
+ String thumbnailUrl = item.getThumbnailUrl();
+ long publishTime = (streamMetadata != null && streamMetadata.getReleaseTime() > 0) ? streamMetadata.getReleaseTime() * 1000 : item.getTimestamp() * 1000;
+ int bgColor = Helper.generateRandomColorForValue(item.getClaimId());
+ if (bgColor == 0) {
+ bgColor = Helper.generateRandomColorForValue(item.getName());
+ }
+
+ boolean isPending = item.getConfirmations() == 0;
+ boolean isSelected = isClaimSelected(item);
+ vh.itemView.setSelected(isSelected);
+ vh.selectedOverlayView.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+ vh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (isPending) {
+ Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG).show();
+ return;
+ }
+
+ if (inSelectionMode) {
+ toggleSelectedClaim(original);
+ } else {
+ if (listener != null) {
+ listener.onClaimClicked(item);
+ }
+ }
+ }
+ });
+ vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ if (!canEnterSelectionMode) {
+ return false;
+ }
+
+ if (isPending) {
+ Snackbar.make(vh.itemView, R.string.item_pending_blockchain, Snackbar.LENGTH_LONG).show();
+ return false;
+ }
+
+ if (!inSelectionMode) {
+ inSelectionMode = true;
+ if (selectionModeListener != null) {
+ selectionModeListener.onEnterSelectionMode();
+ }
+ }
+ toggleSelectedClaim(original);
+ return true;
+ }
+ });
+
+ vh.publisherView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (listener != null && signingChannel != null) {
+ listener.onClaimClicked(signingChannel);
+ }
+ }
+ });
+
+ vh.publishTimeView.setVisibility(!isPending ? View.VISIBLE : View.GONE);
+ vh.pendingTextView.setVisibility(isPending ? View.VISIBLE : View.GONE);
+ vh.repostInfoView.setVisibility(isRepost ? View.VISIBLE : View.GONE);
+ vh.repostChannelView.setText(isRepost ? original.getSigningChannel().getName() : null);
+ vh.repostChannelView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (listener != null) {
+ listener.onClaimClicked(original.getSigningChannel());
+ }
+ }
+ });
+
+ vh.titleView.setText(Helper.isNullOrEmpty(item.getTitle()) ? item.getName() : item.getTitle());
+ if (type == VIEW_TYPE_FEATURED) {
+ LbryUri vanityUrl = new LbryUri();
+ vanityUrl.setClaimName(item.getName());
+ vh.vanityUrlView.setText(vanityUrl.toString());
+ }
+
+ vh.feeContainer.setVisibility(item.isUnresolved() || !Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType()) ? View.GONE : View.VISIBLE);
+ vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.VISIBLE : View.GONE);
+ Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
+
+ if (type == VIEW_TYPE_FEATURED && item.isUnresolved()) {
+ vh.durationView.setVisibility(View.GONE);
+ vh.titleView.setText("Nothing here. Publish something!");
+ vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
+ } else {
+ if (Claim.TYPE_STREAM.equalsIgnoreCase(item.getValueType())) {
+ long duration = item.getDuration();
+ if (!Helper.isNullOrEmpty(thumbnailUrl)) {
+ Glide.with(context.getApplicationContext()).
+ load(thumbnailUrl).
+ centerCrop().
+ placeholder(R.drawable.bg_thumbnail_placeholder).
+ into(vh.thumbnailView);
+ vh.thumbnailView.setVisibility(View.VISIBLE);
+ } else {
+ vh.thumbnailView.setVisibility(View.GONE);
+ }
+
+ BigDecimal cost = item.getActualCost(Lbryio.LBCUSDRate);
+ vh.feeContainer.setVisibility(cost.doubleValue() > 0 && !hideFee ? View.VISIBLE : View.GONE);
+ vh.feeView.setText(cost.doubleValue() > 0 ? Helper.shortCurrencyFormat(cost.doubleValue()) : "Paid");
+ vh.alphaView.setText(item.getName().substring(0, Math.min(5, item.getName().length() - 1)));
+ vh.publisherView.setText(signingChannel != null ? signingChannel.getName() : context.getString(R.string.anonymous));
+ vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
+ publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
+ vh.durationView.setVisibility(duration > 0 ? View.VISIBLE : View.GONE);
+ vh.durationView.setText(Helper.formatDuration(duration));
+
+ LbryFile claimFile = item.getFile();
+ boolean isDownloading = false;
+ int progress = 0;
+ String fileSizeString = claimFile == null ? null : Helper.formatBytes(claimFile.getTotalBytes(), false);
+ if (claimFile != null &&
+ !Helper.isNullOrEmpty(claimFile.getDownloadPath()) &&
+ !claimFile.isCompleted() &&
+ claimFile.getWrittenBytes() < claimFile.getTotalBytes()) {
+ isDownloading = true;
+ progress = claimFile.getTotalBytes() > 0 ?
+ Double.valueOf(((double) claimFile.getWrittenBytes() / (double) claimFile.getTotalBytes()) * 100.0).intValue() : 0;
+ fileSizeString = String.format("%s / %s",
+ Helper.formatBytes(claimFile.getWrittenBytes(), false),
+ Helper.formatBytes(claimFile.getTotalBytes(), false));
+ }
+
+ Helper.setViewText(vh.fileSizeView, claimFile != null && !Helper.isNullOrEmpty(claimFile.getDownloadPath()) ? fileSizeString : null);
+ Helper.setViewVisibility(vh.downloadProgressView, isDownloading ? View.VISIBLE : View.INVISIBLE);
+ Helper.setViewProgress(vh.downloadProgressView, progress);
+ Helper.setViewText(vh.deviceView, item.getDevice());
+ } else if (Claim.TYPE_CHANNEL.equalsIgnoreCase(item.getValueType())) {
+ if (!Helper.isNullOrEmpty(thumbnailUrl)) {
+ Glide.with(context.getApplicationContext()).
+ load(thumbnailUrl).
+ centerCrop().
+ placeholder(R.drawable.bg_thumbnail_placeholder).
+ apply(RequestOptions.circleCropTransform()).
+ into(vh.thumbnailView);
+ }
+ vh.alphaView.setText(item.getName().substring(1, 2).toUpperCase());
+ vh.publisherView.setText(item.getName());
+ vh.publishTimeView.setText(DateUtils.getRelativeTimeSpanString(
+ publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
+ }
+ }
+ }
+
+ private void toggleSelectedClaim(Claim claim) {
+ if (selectedItems.contains(claim)) {
+ selectedItems.remove(claim);
+ } else {
+ selectedItems.add(claim);
+ }
+
+ if (selectionModeListener != null) {
+ selectionModeListener.onItemSelectionToggled();
+ }
+
+ if (selectedItems.size() == 0) {
+ inSelectionMode = false;
+ if (selectionModeListener != null) {
+ selectionModeListener.onExitSelectionMode();
+ }
+ }
+
+ notifyDataSetChanged();
+ }
+
+ public interface ClaimListItemListener {
+ void onClaimClicked(Claim claim);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/EditorsChoiceItemAdapter.java b/app/src/main/java/io/lbry/browser/adapter/EditorsChoiceItemAdapter.java
new file mode 100644
index 00000000..557751aa
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/EditorsChoiceItemAdapter.java
@@ -0,0 +1,117 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.EditorsChoiceItem;
+import io.lbry.browser.utils.Helper;
+import lombok.Setter;
+
+public class EditorsChoiceItemAdapter extends RecyclerView.Adapter {
+ private static final int VIEW_TYPE_HEADER = 1;
+ private static final int VIEW_TYPE_CONTENT = 2;
+
+ private Context context;
+ private List items;
+ @Setter
+ private EditorsChoiceItemListener listener;
+
+ public EditorsChoiceItemAdapter(List items, Context context) {
+ this.context = context;
+ this.items = new ArrayList<>(items);
+ }
+
+ public void addFeaturedItem(EditorsChoiceItem item) {
+ items.add(0, item);
+ notifyDataSetChanged();
+ }
+
+ public void addItems(List items) {
+ for (EditorsChoiceItem item : items) {
+ if (!this.items.contains(item)) {
+ this.items.add(item);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected ImageView thumbnailView;
+ protected TextView descriptionView;
+ protected TextView headerView;
+ protected TextView titleView;
+ protected View cardView;
+
+ public ViewHolder(View v) {
+ super(v);
+
+ cardView = v.findViewById(R.id.editors_choice_content_card);
+ descriptionView = v.findViewById(R.id.editors_choice_content_description);
+ titleView = v.findViewById(R.id.editors_choice_content_title);
+
+ thumbnailView = v.findViewById(R.id.editors_choice_content_thumbnail);
+ headerView = v.findViewById(R.id.editors_choice_header_title);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return items.get(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_CONTENT;
+ }
+
+ @Override
+ public EditorsChoiceItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_editors_choice, parent, false);
+ return new EditorsChoiceItemAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(EditorsChoiceItemAdapter.ViewHolder vh, int position) {
+ int type = getItemViewType(position);
+ EditorsChoiceItem item = items.get(position);
+
+ vh.headerView.setVisibility(type == VIEW_TYPE_HEADER ? View.VISIBLE : View.GONE);
+ vh.cardView.setVisibility(type == VIEW_TYPE_CONTENT ? View.VISIBLE : View.GONE);
+
+ vh.headerView.setText(item.getTitle());
+ vh.titleView.setText(item.getTitle());
+ vh.descriptionView.setText(item.getDescription());
+ if (!Helper.isNullOrEmpty(item.getThumbnailUrl())) {
+ Glide.with(context.getApplicationContext()).
+ load(item.getThumbnailUrl()).
+ centerCrop().
+ placeholder(R.drawable.bg_thumbnail_placeholder).
+ into(vh.thumbnailView);
+ }
+
+ vh.cardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (listener != null) {
+ listener.onEditorsChoiceItemClicked(item);
+ }
+ }
+ });
+ }
+
+ public interface EditorsChoiceItemListener {
+ void onEditorsChoiceItemClicked(EditorsChoiceItem item);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java b/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java
new file mode 100644
index 00000000..b9edfcc0
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/GalleryGridAdapter.java
@@ -0,0 +1,119 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.bumptech.glide.Glide;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.GalleryItem;
+import io.lbry.browser.utils.Helper;
+import lombok.Setter;
+
+public class GalleryGridAdapter extends RecyclerView.Adapter {
+ private Context context;
+ private List items;
+ @Setter
+ private GalleryItemClickListener listener;
+
+ public GalleryGridAdapter(List items, Context context) {
+ this.items = new ArrayList<>(items);
+ this.context = context;
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected ImageView thumbnailView;
+ protected TextView durationView;
+ public ViewHolder(View v) {
+ super(v);
+ thumbnailView = v.findViewById(R.id.gallery_item_thumbnail);
+ durationView = v.findViewById(R.id.gallery_item_duration);
+ }
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ public void addItem(GalleryItem item) {
+ if (!items.contains(item)) {
+ items.add(item);
+ notifyDataSetChanged();
+ }
+ }
+
+ public void addItems(List items) {
+ for (GalleryItem item : items) {
+ if (!this.items.contains(item)) {
+ this.items.add(item);
+ notifyDataSetChanged();
+ }
+ }
+ }
+
+ public void clearItems() {
+ items.clear();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public GalleryGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_gallery, root, false);
+ return new GalleryGridAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(GalleryGridAdapter.ViewHolder vh, int position) {
+ GalleryItem item = items.get(position);
+ String thumbnailUrl = item.getThumbnailPath();
+ Glide.with(context.getApplicationContext()).load(thumbnailUrl).centerCrop().into(vh.thumbnailView);
+ vh.durationView.setVisibility(item.getDuration() > 0 ? View.VISIBLE : View.INVISIBLE);
+ vh.durationView.setText(item.getDuration() > 0 ? Helper.formatDuration(Double.valueOf(item.getDuration() / 1000.0).longValue()) : null);
+
+ vh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (listener != null) {
+ listener.onGalleryItemClicked(item);
+ }
+ }
+ });
+ }
+
+ public interface GalleryItemClickListener {
+ void onGalleryItemClicked(GalleryItem item);
+ }
+
+ public static class GalleryGridItemDecoration extends RecyclerView.ItemDecoration {
+
+ private int spanCount;
+ private int spacing;
+
+ public GalleryGridItemDecoration(int spanCount, int spacing) {
+ this.spanCount = spanCount;
+ this.spacing = spacing;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ int position = parent.getChildAdapterPosition(view); // item position
+ int column = position % spanCount; // item column
+
+ outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
+ outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
+ if (position >= spanCount) {
+ outRect.top = spacing; // item top
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/InlineChannelSpinnerAdapter.java b/app/src/main/java/io/lbry/browser/adapter/InlineChannelSpinnerAdapter.java
new file mode 100644
index 00000000..9924a67a
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/InlineChannelSpinnerAdapter.java
@@ -0,0 +1,80 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.SpinnerAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.Claim;
+
+public class InlineChannelSpinnerAdapter extends ArrayAdapter {
+
+ private List channels;
+ private int layoutResourceId;
+ private LayoutInflater inflater;
+
+ public InlineChannelSpinnerAdapter(Context context, int resource, List channels) {
+ super(context, resource, 0, channels);
+ inflater = LayoutInflater.from(context);
+ layoutResourceId = resource;
+ this.channels = new ArrayList<>(channels);
+ }
+ public void addPlaceholder(boolean includeAnonymous) {
+ Claim placeholder = new Claim();
+ placeholder.setPlaceholder(true);
+ insert(placeholder, 0);
+ channels.add(0, placeholder);
+
+ if (includeAnonymous) {
+ Claim anonymous = new Claim();
+ anonymous.setPlaceholderAnonymous(true);
+ insert(anonymous, 1);
+ channels.add(1, anonymous);
+ }
+ }
+
+ public int getItemPosition(Claim item) {
+ for (int i = 0; i < channels.size(); i++) {
+ Claim channel = channels.get(i);
+ if (item.getClaimId().equalsIgnoreCase(channel.getClaimId())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public View getDropDownView(int position, View view, ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+ private View createView(int position, View convertView, ViewGroup parent){
+ View view = inflater.inflate(layoutResourceId, parent, false);
+
+ Context context = getContext();
+ Claim channel = getItem(position);
+ String name = channel.getName();
+ if (channel.isPlaceholder()) {
+ name = context.getString(R.string.create_a_channel);
+ } else if (channel.isPlaceholderAnonymous()) {
+ name = context.getString(R.string.anonymous);
+ }
+
+ TextView label = view.findViewById(R.id.channel_item_name);
+ label.setText(name);
+
+ return view;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/InviteeListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/InviteeListAdapter.java
new file mode 100644
index 00000000..7ff066b0
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/InviteeListAdapter.java
@@ -0,0 +1,92 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.lbryinc.Invitee;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import lombok.Setter;
+
+public class InviteeListAdapter extends RecyclerView.Adapter {
+
+ private Context context;
+ private List items;
+
+ public InviteeListAdapter(List invitees, Context context) {
+ this.context = context;
+ this.items = new ArrayList<>(invitees);
+ }
+
+ public void clear() {
+ items.clear();
+ notifyDataSetChanged();
+ }
+
+ public List getItems() {
+ return new ArrayList<>(items);
+ }
+
+ public void addHeader() {
+ Invitee header = new Invitee();
+ header.setHeader(true);
+ items.add(0, header);
+ }
+
+ public void addInvitees(List Invitees) {
+ for (Invitee tx : Invitees) {
+ if (!items.contains(tx)) {
+ items.add(tx);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ @Override
+ public InviteeListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_invitee, root, false);
+ return new InviteeListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(InviteeListAdapter.ViewHolder vh, int position) {
+ Invitee item = items.get(position);
+ vh.emailView.setText(item.isHeader() ? context.getString(R.string.email) : item.getEmail());
+ vh.emailView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
+
+ String rewardText = context.getString(
+ item.isInviteRewardClaimed() ? R.string.claimed :
+ (item.isInviteRewardClaimable() ? R.string.claimable : R.string.unclaimable));
+ vh.rewardView.setText(item.isHeader() ? context.getString(R.string.reward) : rewardText);
+ vh.rewardView.setTypeface(null, item.isHeader() ? Typeface.BOLD : Typeface.NORMAL);
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected TextView emailView;
+ protected TextView rewardView;
+
+ public ViewHolder(View v) {
+ super(v);
+ emailView = v.findViewById(R.id.invitee_email);
+ rewardView = v.findViewById(R.id.invitee_reward);
+ }
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/LanguageSpinnerAdapter.java b/app/src/main/java/io/lbry/browser/adapter/LanguageSpinnerAdapter.java
new file mode 100644
index 00000000..1e5c9a44
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/LanguageSpinnerAdapter.java
@@ -0,0 +1,53 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.model.Language;
+import io.lbry.browser.utils.Predefined;
+
+public class LanguageSpinnerAdapter extends ArrayAdapter {
+ private int layoutResourceId;
+ private LayoutInflater inflater;
+
+ public LanguageSpinnerAdapter(Context context, int resource) {
+ super(context, resource, 0, Predefined.PUBLISH_LANGUAGES);
+ inflater = LayoutInflater.from(context);
+ layoutResourceId = resource;
+ }
+
+ public int getItemPosition(String languageCode) {
+ for (int i = 0; i < Predefined.PUBLISH_LANGUAGES.size(); i++) {
+ Language lang = Predefined.PUBLISH_LANGUAGES.get(i);
+ if (lang.getCode().equalsIgnoreCase(languageCode)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+ private View createView(int position, View convertView, ViewGroup parent) {
+ Language item = getItem(position);
+ View view = inflater.inflate(layoutResourceId, parent, false);
+ TextView label = view.findViewById(R.id.item_display_name);
+ label.setText(item != null ? item.getStringResourceId() : 0);
+
+ return view;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/lbry/browser/adapter/LicenseSpinnerAdapter.java b/app/src/main/java/io/lbry/browser/adapter/LicenseSpinnerAdapter.java
new file mode 100644
index 00000000..c6776cf9
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/LicenseSpinnerAdapter.java
@@ -0,0 +1,52 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.Language;
+import io.lbry.browser.model.License;
+import io.lbry.browser.utils.Predefined;
+
+public class LicenseSpinnerAdapter extends ArrayAdapter {
+ private int layoutResourceId;
+ private LayoutInflater inflater;
+
+ public LicenseSpinnerAdapter(Context context, int resource) {
+ super(context, resource, 0, Predefined.LICENSES);
+ inflater = LayoutInflater.from(context);
+ layoutResourceId = resource;
+ }
+ public int getItemPosition(String name) {
+ for (int i = 0; i < Predefined.LICENSES.size(); i++) {
+ License lic = Predefined.LICENSES.get(i);
+ if (lic.getName().equalsIgnoreCase(name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public View getDropDownView(int position, View view, @NonNull ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+ @Override
+ public View getView(int position, View view, @NonNull ViewGroup parent) {
+ return createView(position, view, parent);
+ }
+ private View createView(int position, View convertView, ViewGroup parent) {
+ License item = getItem(position);
+ View view = inflater.inflate(layoutResourceId, parent, false);
+ TextView label = view.findViewById(R.id.item_display_name);
+ label.setText(item != null ? item.getStringResourceId() : 0);
+
+ return view;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/lbry/browser/adapter/NavigationMenuAdapter.java b/app/src/main/java/io/lbry/browser/adapter/NavigationMenuAdapter.java
new file mode 100644
index 00000000..f5c75a55
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/NavigationMenuAdapter.java
@@ -0,0 +1,117 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.NavMenuItem;
+import io.lbry.browser.ui.controls.SolidIconView;
+import io.lbry.browser.utils.Helper;
+import lombok.Getter;
+import lombok.Setter;
+
+public class NavigationMenuAdapter extends RecyclerView.Adapter {
+ private static final int TYPE_GROUP = 1;
+ private static final int TYPE_ITEM = 2;
+
+ private Context context;
+ private List menuItems;
+ private NavMenuItem currentItem;
+ @Setter
+ private NavigationMenuItemClickListener listener;
+
+ public NavigationMenuAdapter(List menuItems, Context context) {
+ this.menuItems = new ArrayList<>(menuItems);
+ this.context = context;
+ }
+
+ public void setCurrentItem(int id) {
+ for (NavMenuItem item : menuItems) {
+ if (item.getId() == id) {
+ this.currentItem = item;
+ break;
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public void setExtraLabelForItem(int id, String extraLabel) {
+ for (NavMenuItem item : menuItems) {
+ if (item.getId() == id) {
+ item.setExtraLabel(extraLabel);
+ break;
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public void setCurrentItem(NavMenuItem currentItem) {
+ this.currentItem = currentItem;
+ notifyDataSetChanged();
+ }
+
+ public int getCurrentItemId() {
+ return currentItem != null ? currentItem.getId() : -1;
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected SolidIconView iconView;
+ protected TextView titleView;
+ public ViewHolder(View v) {
+ super(v);
+ titleView = v.findViewById(R.id.nav_menu_title);
+ iconView = v.findViewById(R.id.nav_menu_item_icon);
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return menuItems != null ? menuItems.size() : 0;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return menuItems.get(position).isGroup() ? TYPE_GROUP : TYPE_ITEM;
+ }
+
+ @Override
+ public NavigationMenuAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(context).inflate(viewType == TYPE_GROUP ?
+ R.layout.list_item_nav_menu_group : R.layout.list_item_nav_menu_item, parent, false);
+ return new NavigationMenuAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder vh, int position) {
+ int type = getItemViewType(position);
+ NavMenuItem item = menuItems.get(position);
+ String displayTitle = !Helper.isNullOrEmpty(item.getExtraLabel()) ? String.format("%s (%s)", item.getTitle(), item.getExtraLabel()) : item.getTitle();
+ vh.titleView.setText(displayTitle);
+ if (type == TYPE_ITEM && vh.iconView != null) {
+ vh.iconView.setText(item.getIcon());
+ }
+ vh.itemView.setSelected(item.equals(currentItem));
+ vh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (listener != null) {
+ listener.onNavigationMenuItemClicked(item);
+ }
+ }
+ });
+ }
+
+ public interface NavigationMenuItemClickListener {
+ void onNavigationMenuItemClicked(NavMenuItem menuItem);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
new file mode 100644
index 00000000..f3895452
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/RewardListAdapter.java
@@ -0,0 +1,216 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.net.Uri;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.snackbar.Snackbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.lbryinc.Reward;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.Lbryio;
+import lombok.Getter;
+import lombok.Setter;
+
+public class RewardListAdapter extends RecyclerView.Adapter {
+
+ public static final int DISPLAY_MODE_ALL = 1;
+ public static final int DISPLAY_MODE_UNCLAIMED = 2;
+
+ private Context context;
+ @Setter
+ private List all;
+ private List items;
+ @Setter
+ private RewardClickListener clickListener;
+ @Getter
+ private int displayMode;
+
+ public RewardListAdapter(List all, Context context) {
+ this.all = new ArrayList<>(all);
+ this.items = new ArrayList<>(all);
+ this.context = context;
+ this.displayMode = DISPLAY_MODE_ALL;
+
+ addCustomReward();
+ }
+
+ public void setRewards(List rewards) {
+ this.all = new ArrayList<>(rewards);
+ updateItemsForDisplayMode();
+ notifyDataSetChanged();
+ }
+
+ public void setDisplayMode(int displayMode) {
+ this.displayMode = displayMode;
+ updateItemsForDisplayMode();
+ notifyDataSetChanged();
+ }
+
+ private void updateItemsForDisplayMode() {
+ if (displayMode == DISPLAY_MODE_ALL) {
+ items = new ArrayList<>(all);
+ } else if (displayMode == DISPLAY_MODE_UNCLAIMED) {
+ items = new ArrayList<>();
+ for (Reward reward : all) {
+ if (!reward.isClaimed()) {
+ items.add(reward);
+ }
+ }
+ }
+ addCustomReward();
+ }
+
+ private void addCustomReward() {
+ Reward custom = new Reward();
+ custom.setCustom(true);
+ custom.setRewardTitle(context.getString(R.string.custom_reward_title));
+ custom.setRewardDescription(context.getString(R.string.custom_reward_description));
+ items.add(custom);
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected View iconClaimed;
+ protected View loading;
+ protected View upTo;
+ protected TextView textTitle;
+ protected TextView textDescription;
+ protected TextView textLbcValue;
+ protected TextView textUsdValue;
+ protected TextView textLinkTransaction;
+ protected EditText inputCustomCode;
+ protected MaterialButton buttonClaimCustom;
+ public ViewHolder(View v) {
+ super(v);
+ iconClaimed = v.findViewById(R.id.reward_item_claimed_icon);
+ upTo = v.findViewById(R.id.reward_item_up_to);
+ loading = v.findViewById(R.id.reward_item_loading);
+ textTitle = v.findViewById(R.id.reward_item_title);
+ textDescription = v.findViewById(R.id.reward_item_description);
+ textLbcValue = v.findViewById(R.id.reward_item_lbc_value);
+ textLinkTransaction = v.findViewById(R.id.reward_item_tx_link);
+ textUsdValue = v.findViewById(R.id.reward_item_usd_value);
+ inputCustomCode = v.findViewById(R.id.reward_item_custom_code_input);
+ buttonClaimCustom = v.findViewById(R.id.reward_item_custom_claim_button);
+ }
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ public void addReward(Reward reward) {
+ if (!items.contains(reward)) {
+ items.add(reward);
+ }
+ notifyDataSetChanged();
+ }
+ public List getRewards() {
+ return new ArrayList<>(items);
+ }
+
+ @Override
+ public RewardListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_reward, root, false);
+ return new RewardListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(RewardListAdapter.ViewHolder vh, int position) {
+ Reward reward = items.get(position);
+ String displayAmount = reward.getDisplayAmount();
+ double rewardAmount = 0;
+ if (!"?".equals(displayAmount)) {
+ rewardAmount = Double.valueOf(displayAmount);
+ }
+ boolean hasTransaction = !Helper.isNullOrEmpty(reward.getTransactionId()) && reward.getTransactionId().length() > 7;
+ vh.iconClaimed.setVisibility(reward.isClaimed() ? View.VISIBLE : View.INVISIBLE);
+ vh.inputCustomCode.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
+ vh.buttonClaimCustom.setVisibility(reward.isCustom() ? View.VISIBLE : View.GONE);
+ vh.textTitle.setText(reward.getRewardTitle());
+ vh.textDescription.setText(reward.getRewardDescription());
+ vh.upTo.setVisibility(reward.shouldDisplayRange() ? View.VISIBLE : View.GONE);
+ vh.textLbcValue.setText(reward.isCustom() ? "?" : Helper.LBC_CURRENCY_FORMAT.format(Helper.parseDouble(reward.getDisplayAmount(), 0)));
+ vh.textLinkTransaction.setVisibility(hasTransaction ? View.VISIBLE : View.GONE);
+ vh.textLinkTransaction.setText(hasTransaction ? reward.getTransactionId().substring(0, 7) : null);
+ vh.textLinkTransaction.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (context != null) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, reward.getTransactionId())));
+ context.startActivity(intent);
+ }
+ }
+ });
+
+ vh.textUsdValue.setText(reward.isCustom() || Lbryio.LBCUSDRate == 0 ? null :
+ String.format("≈$%s", Helper.SIMPLE_CURRENCY_FORMAT.format(rewardAmount * Lbryio.LBCUSDRate)));
+
+ vh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (reward.isClaimed()) {
+ return;
+ }
+
+ if (clickListener != null) {
+ clickListener.onRewardClicked(reward, vh.loading);
+ }
+ }
+ });
+
+ vh.inputCustomCode.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 = charSequence.toString().trim();
+ vh.buttonClaimCustom.setEnabled(value.length() > 0);
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+
+ }
+ });
+
+ vh.buttonClaimCustom.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String claimCode = Helper.getValue(vh.inputCustomCode.getText());
+ if (Helper.isNullOrEmpty(claimCode)) {
+ Snackbar.make(view, R.string.please_enter_claim_code, Snackbar.LENGTH_LONG).
+ setBackgroundTint(Color.RED).setTextColor(Color.WHITE).show();
+ return;
+ }
+
+ if (clickListener != null) {
+ clickListener.onCustomClaimButtonClicked(claimCode, vh.inputCustomCode, vh.buttonClaimCustom, vh.loading);
+ }
+ }
+ });
+ }
+
+ public interface RewardClickListener {
+ void onRewardClicked(Reward reward, View loadingView);
+ void onCustomClaimButtonClicked(String code, EditText inputCustomCode, MaterialButton buttonClaim, View loadingView);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/SuggestedChannelGridAdapter.java b/app/src/main/java/io/lbry/browser/adapter/SuggestedChannelGridAdapter.java
new file mode 100644
index 00000000..ff152d3f
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/SuggestedChannelGridAdapter.java
@@ -0,0 +1,127 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+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.model.Claim;
+import io.lbry.browser.listener.ChannelItemSelectionListener;
+import io.lbry.browser.utils.Helper;
+import lombok.Setter;
+
+public class SuggestedChannelGridAdapter extends RecyclerView.Adapter {
+ private Context context;
+ private List items;
+ private List selectedItems;
+ @Setter
+ private ChannelItemSelectionListener listener;
+
+ public SuggestedChannelGridAdapter(List items, Context context) {
+ this.items = new ArrayList<>(items);
+ this.selectedItems = new ArrayList<>();
+ this.context = context;
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected View noThumbnailView;
+ protected ImageView thumbnailView;
+ protected TextView alphaView;
+ protected TextView titleView;
+ protected TextView tagView;
+ public ViewHolder(View v) {
+ super(v);
+ noThumbnailView = v.findViewById(R.id.suggested_channel_no_thumbnail);
+ alphaView = v.findViewById(R.id.suggested_channel_alpha_view);
+ thumbnailView = v.findViewById(R.id.suggested_channel_thumbnail);
+ titleView = v.findViewById(R.id.suggested_channel_title);
+ tagView = v.findViewById(R.id.suggested_channel_tag);
+ }
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ public int getSelectedCount() { return selectedItems.size(); }
+
+ public void clearItems() {
+ items.clear();
+ notifyDataSetChanged();
+ }
+
+ public List getSelectedItems() {
+ return this.selectedItems;
+ }
+ public void clearSelectedItems() {
+ this.selectedItems.clear();
+ }
+ public boolean isClaimSelected(Claim claim) {
+ return selectedItems.contains(claim);
+ }
+
+ public void addClaims(List claims) {
+ for (Claim claim : claims) {
+ if (!items.contains(claim)) {
+ items.add(claim);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public SuggestedChannelGridAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_suggested_channel, root, false);
+ return new SuggestedChannelGridAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(SuggestedChannelGridAdapter.ViewHolder vh, int position) {
+ Claim claim = items.get(position);
+ String thumbnailUrl = claim.getThumbnailUrl();
+
+ int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
+ Helper.setIconViewBackgroundColor(vh.noThumbnailView, bgColor, false, context);
+ vh.noThumbnailView.setVisibility(Helper.isNullOrEmpty(thumbnailUrl) ? View.INVISIBLE : View.VISIBLE);
+ vh.alphaView.setText(claim.getName().substring(1, 2));
+ if (!Helper.isNullOrEmpty(thumbnailUrl)) {
+ vh.thumbnailView.setVisibility(View.VISIBLE);
+ Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
+ } else {
+ vh.thumbnailView.setVisibility(View.GONE);
+ }
+ vh.titleView.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
+
+ String firstTag = claim.getFirstTag();
+ vh.tagView.setVisibility(Helper.isNullOrEmpty(firstTag) ? View.INVISIBLE : View.VISIBLE);
+ vh.tagView.setBackgroundResource(R.drawable.bg_tag);
+ vh.tagView.setText(firstTag);
+ vh.itemView.setSelected(isClaimSelected(claim));
+
+ vh.itemView.setOnClickListener(view -> {
+ if (selectedItems.contains(claim)) {
+ selectedItems.remove(claim);
+ if (listener != null) {
+ listener.onChannelItemDeselected(claim);
+ }
+ } else {
+ selectedItems.add(claim);
+ if (listener != null) {
+ listener.onChannelItemSelected(claim);
+ }
+ }
+ notifyDataSetChanged();
+ });
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
new file mode 100644
index 00000000..3ed25b93
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/TagListAdapter.java
@@ -0,0 +1,108 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.Tag;
+import lombok.Getter;
+import lombok.Setter;
+
+public class TagListAdapter extends RecyclerView.Adapter {
+
+ public static final int CUSTOMIZE_MODE_NONE = 0;
+ public static final int CUSTOMIZE_MODE_ADD = 1;
+ public static final int CUSTOMIZE_MODE_REMOVE = 2;
+
+ private Context context;
+ private List items;
+ @Setter
+ private TagClickListener clickListener;
+ @Getter
+ @Setter
+ private int customizeMode;
+
+ public TagListAdapter(List tags, Context context) {
+ this.context = context;
+ this.items = new ArrayList<>(tags);
+ this.customizeMode = CUSTOMIZE_MODE_NONE;
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected ImageView iconView;
+ protected TextView nameView;
+ public ViewHolder(View v) {
+ super(v);
+ iconView = v.findViewById(R.id.tag_action);
+ nameView = v.findViewById(R.id.tag_name);
+ }
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ public void addTag(Tag tag) {
+ if (!items.contains(tag)) {
+ items.add(tag);
+ }
+ notifyDataSetChanged();
+ }
+ public List getTags() {
+ return new ArrayList<>(items);
+ }
+
+ public void setTags(List tags) {
+ items = new ArrayList<>(tags);
+ notifyDataSetChanged();
+ }
+
+ public void addTags(List tags) {
+ for (Tag tag : tags) {
+ if (!items.contains(tag)) {
+ items.add(tag);
+ }
+ }
+ notifyDataSetChanged();
+ }
+ public void removeTag(Tag tag) {
+ items.remove(tag);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public TagListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_tag, root, false);
+ return new TagListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(TagListAdapter.ViewHolder vh, int position) {
+ Tag tag = items.get(position);
+ vh.nameView.setText(tag.getName().toLowerCase());
+ vh.iconView.setVisibility(customizeMode == CUSTOMIZE_MODE_NONE ? View.GONE : View.VISIBLE);
+ vh.iconView.setImageResource(customizeMode == CUSTOMIZE_MODE_REMOVE ? R.drawable.ic_close : R.drawable.ic_add);
+ vh.itemView.setBackgroundResource(tag.isMature() ? R.drawable.bg_tag_mature : R.drawable.bg_tag);
+ vh.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (clickListener != null) {
+ clickListener.onTagClicked(tag, customizeMode);
+ }
+ }
+ });
+ }
+
+ public interface TagClickListener {
+ void onTagClicked(Tag tag, int customizeMode);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/TransactionListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/TransactionListAdapter.java
new file mode 100644
index 00000000..95b4a139
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/TransactionListAdapter.java
@@ -0,0 +1,132 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.model.Transaction;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import lombok.Setter;
+
+public class TransactionListAdapter extends RecyclerView.Adapter {
+
+ private static final DecimalFormat TX_LIST_AMOUNT_FORMAT = new DecimalFormat("#,##0.0000");
+ private static final SimpleDateFormat TX_LIST_DATE_FORMAT = new SimpleDateFormat("MMM d");
+
+ private Context context;
+ private List items;
+ @Setter
+ private TransactionClickListener listener;
+
+ public TransactionListAdapter(List transactions, Context context) {
+ this.context = context;
+ this.items = new ArrayList<>(transactions);
+ }
+
+ public void clear() {
+ items.clear();
+ notifyDataSetChanged();
+ }
+
+ public List getItems() {
+ return new ArrayList<>(items);
+ }
+
+ public void addTransactions(List transactions) {
+ for (Transaction tx : transactions) {
+ if (!items.contains(tx)) {
+ items.add(tx);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ @Override
+ public TransactionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_transaction, root, false);
+ return new TransactionListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(TransactionListAdapter.ViewHolder vh, int position) {
+ Transaction item = items.get(position);
+ vh.descView.setText(item.getDescriptionStringId());
+ vh.amountView.setText(TX_LIST_AMOUNT_FORMAT.format(item.getValue().doubleValue()));
+ vh.claimView.setText(item.getClaim());
+ vh.feeView.setText(context.getString(R.string.tx_list_fee, TX_LIST_AMOUNT_FORMAT.format(item.getFee().doubleValue())));
+ vh.txidLinkView.setText(item.getTxid().substring(0, 7));
+ vh.dateView.setText(TX_LIST_DATE_FORMAT.format(item.getTxDate()));
+
+ vh.infoFeeContainer.setVisibility(!Helper.isNullOrEmpty(item.getClaim()) || Math.abs(item.getFee().doubleValue()) > 0 ?
+ View.VISIBLE : View.GONE);
+
+ vh.claimView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LbryUri claimUrl = item.getClaimUrl();
+ if (claimUrl != null && listener != null) {
+ listener.onClaimUrlClicked(claimUrl);
+ }
+ }
+ });
+
+ vh.txidLinkView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (context != null) {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(String.format("%s/%s", Helper.EXPLORER_TX_PREFIX, item.getTxid())));
+ context.startActivity(intent);
+ }
+ }
+ });
+
+ vh.itemView.setOnClickListener(view -> {
+ if (listener != null) {
+ listener.onTransactionClicked(item);
+ }
+ });
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected TextView descView;
+ protected TextView amountView;
+ protected TextView claimView;
+ protected TextView feeView;
+ protected TextView txidLinkView;
+ protected TextView dateView;
+ protected View infoFeeContainer;
+
+ public ViewHolder(View v) {
+ super(v);
+ descView = v.findViewById(R.id.transaction_desc);
+ amountView = v.findViewById(R.id.transaction_amount);
+ claimView = v.findViewById(R.id.transaction_claim);
+ feeView = v.findViewById(R.id.transaction_fee);
+ txidLinkView = v.findViewById(R.id.transaction_id_link);
+ dateView = v.findViewById(R.id.transaction_date);
+ infoFeeContainer = v.findViewById(R.id.transaction_info_fee_container);
+ }
+ }
+
+ public interface TransactionClickListener {
+ void onTransactionClicked(Transaction transaction);
+ void onClaimUrlClicked(LbryUri uri);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/UrlSuggestionListAdapter.java b/app/src/main/java/io/lbry/browser/adapter/UrlSuggestionListAdapter.java
new file mode 100644
index 00000000..cc2a09ea
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/UrlSuggestionListAdapter.java
@@ -0,0 +1,147 @@
+package io.lbry.browser.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.exceptions.LbryUriException;
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.model.UrlSuggestion;
+import io.lbry.browser.ui.controls.SolidIconView;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import lombok.Setter;
+
+public class UrlSuggestionListAdapter extends RecyclerView.Adapter {
+ private Context context;
+ private List items;
+ @Setter
+ private UrlSuggestionClickListener listener;
+
+ public UrlSuggestionListAdapter(Context context) {
+ this.context = context;
+ this.items = new ArrayList<>();
+ }
+
+ public void clear() {
+ items.clear();
+ notifyDataSetChanged();
+ }
+
+ public List getItems() {
+ return new ArrayList<>(items);
+ }
+
+ public List getItemUrls() {
+ List uris = new ArrayList<>();
+ for (int i = 0; i < items.size(); i++) {
+ LbryUri uri = items.get(i).getUri();
+ if (uri != null) {
+ uris.add(uri.toString());
+ }
+ }
+ return uris;
+ }
+
+ public void setClaimForUrl(LbryUri url, Claim claim) {
+ for (int i = 0; i < items.size(); i++) {
+ LbryUri thisUrl = items.get(i).getUri();
+ try {
+ if (thisUrl != null) {
+ LbryUri vanity = LbryUri.parse(thisUrl.toVanityString());
+ if (thisUrl.equals(url) || vanity.equals(url)) {
+ items.get(i).setClaim(claim);
+ }
+ }
+ } catch (LbryUriException ex) {
+ // pass
+ }
+ }
+ }
+
+ public void addUrlSuggestions(List urlSuggestions) {
+ for (UrlSuggestion urlSuggestion : urlSuggestions) {
+ if (!items.contains(urlSuggestion)) {
+ items.add(urlSuggestion);
+ }
+ }
+ notifyDataSetChanged();
+ }
+
+ public int getItemCount() {
+ return items != null ? items.size() : 0;
+ }
+
+ @Override
+ public UrlSuggestionListAdapter.ViewHolder onCreateViewHolder(ViewGroup root, int viewType) {
+ View v = LayoutInflater.from(context).inflate(R.layout.list_item_url_suggestion, root, false);
+ return new UrlSuggestionListAdapter.ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(UrlSuggestionListAdapter.ViewHolder vh, int position) {
+ UrlSuggestion item = items.get(position);
+
+ String fullTitle, desc;
+ int iconStringId;
+ switch (item.getType()) {
+ case UrlSuggestion.TYPE_CHANNEL:
+ iconStringId = R.string.fa_at;
+ fullTitle = item.getTitle();
+ desc = item.getClaim() != null ? item.getClaim().getTitle() :
+ ((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_channel_url_desc), item.getText()));
+ break;
+ case UrlSuggestion.TYPE_TAG:
+ iconStringId = R.string.fa_hashtag;
+ fullTitle = String.format(context.getString(R.string.tag_url_title), item.getText());
+ desc = String.format(context.getString(R.string.explore_tag_url_desc), item.getText());
+ break;
+ case UrlSuggestion.TYPE_SEARCH:
+ iconStringId = R.string.fa_search;
+ fullTitle = String.format(context.getString(R.string.search_url_title), item.getText());
+ desc = String.format(context.getString(R.string.search_url_desc), item.getText());
+ break;
+ case UrlSuggestion.TYPE_FILE:
+ default:
+ iconStringId = R.string.fa_file;
+ fullTitle = item.getTitle();
+ desc = item.getClaim() != null ? item.getClaim().getTitle() :
+ ((item.isUseTextAsDescription() && !Helper.isNullOrEmpty(item.getText())) ? item.getText() : String.format(context.getString(R.string.view_file_url_desc), item.getText()));
+ break;
+ }
+
+ vh.iconView.setText(iconStringId);
+ vh.titleView.setText(fullTitle);
+ vh.descView.setText(desc);
+
+ vh.itemView.setOnClickListener(view -> {
+ if (listener != null) {
+ listener.onUrlSuggestionClicked(item);
+ }
+ });
+ }
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ protected SolidIconView iconView;
+ protected TextView titleView;
+ protected TextView descView;
+ public ViewHolder(View v) {
+ super(v);
+ iconView = v.findViewById(R.id.url_suggestion_icon);
+ titleView = v.findViewById(R.id.url_suggestion_title);
+ descView = v.findViewById(R.id.url_suggestion_description);
+ }
+ }
+
+ public interface UrlSuggestionClickListener {
+ void onUrlSuggestionClicked(UrlSuggestion urlSuggestion);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/adapter/VerificationPagerAdapter.java b/app/src/main/java/io/lbry/browser/adapter/VerificationPagerAdapter.java
new file mode 100644
index 00000000..8569038c
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/adapter/VerificationPagerAdapter.java
@@ -0,0 +1,71 @@
+package io.lbry.browser.adapter;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+import io.lbry.browser.listener.SignInListener;
+import io.lbry.browser.listener.WalletSyncListener;
+import io.lbry.browser.ui.verification.EmailVerificationFragment;
+import io.lbry.browser.ui.verification.ManualVerificationFragment;
+import io.lbry.browser.ui.verification.PhoneVerificationFragment;
+import io.lbry.browser.ui.verification.WalletVerificationFragment;
+import lombok.SneakyThrows;
+
+/**
+ * 4 fragments
+ * - Email collect / verify (sign in)
+ * - Phone number collect / verify (rewards)
+ * - Wallet password
+ * - Manual verification page
+ */
+public class VerificationPagerAdapter extends FragmentStateAdapter {
+ public static final int PAGE_VERIFICATION_EMAIL = 0;
+ public static final int PAGE_VERIFICATION_PHONE = 1;
+ public static final int PAGE_VERIFICATION_WALLET = 2;
+ public static final int PAGE_VERIFICATION_MANUAL = 3;
+
+ private FragmentActivity activity;
+
+ public VerificationPagerAdapter(FragmentActivity activity) {
+ super(activity);
+ this.activity = activity;
+ }
+
+ @SneakyThrows
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case 0:
+ default:
+ EmailVerificationFragment evFragment = EmailVerificationFragment.class.newInstance();
+ if (activity instanceof SignInListener) {
+ evFragment.setListener((SignInListener) activity);
+ }
+ return evFragment;
+ case 1:
+ PhoneVerificationFragment pvFragment = PhoneVerificationFragment.class.newInstance();
+ if (activity instanceof SignInListener) {
+ pvFragment.setListener((SignInListener) activity);
+ }
+ return pvFragment;
+ case 2:
+ WalletVerificationFragment wvFragment = WalletVerificationFragment.class.newInstance();
+ if (activity instanceof WalletSyncListener) {
+ wvFragment.setListener((WalletSyncListener) activity);
+ }
+ return wvFragment;
+ case 3:
+ ManualVerificationFragment mvFragment = ManualVerificationFragment.class.newInstance();
+ if (activity instanceof SignInListener) {
+ mvFragment.setListener((SignInListener) activity);
+ }
+ return mvFragment;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return 4;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java
new file mode 100644
index 00000000..2087a7ff
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/data/DatabaseHelper.java
@@ -0,0 +1,250 @@
+package io.lbry.browser.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.opengl.Visibility;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import io.lbry.browser.exceptions.LbryUriException;
+import io.lbry.browser.model.Tag;
+import io.lbry.browser.model.UrlSuggestion;
+import io.lbry.browser.model.ViewHistory;
+import io.lbry.browser.model.lbryinc.Subscription;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+
+public class DatabaseHelper extends SQLiteOpenHelper {
+ public static final int DATABASE_VERSION = 2;
+ public static final String DATABASE_NAME = "LbryApp.db";
+ private static DatabaseHelper instance;
+
+ private static final String[] SQL_CREATE_TABLES = {
+ // local subscription store
+ "CREATE TABLE subscriptions (url TEXT PRIMARY KEY NOT NULL, channel_name TEXT NOT NULL)",
+ // url entry / suggestion history
+ "CREATE TABLE url_history (id INTEGER PRIMARY KEY NOT NULL, value TEXT NOT NULL, url TEXT, type INTEGER NOT NULL, timestamp TEXT NOT NULL)",
+ // tags (known and followed)
+ "CREATE TABLE tags (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, is_followed INTEGER NOT NULL)",
+ // view history (stores only stream claims that have resolved)
+ "CREATE TABLE view_history (" +
+ " id INTEGER PRIMARY KEY NOT NULL" +
+ ", url TEXT NOT NULL" +
+ ", claim_id TEXT" +
+ ", claim_name TEXT" +
+ ", cost REAL " +
+ ", currency TEXT " +
+ ", title TEXT " +
+ ", publisher_claim_id TEXT" +
+ ", publisher_name TEXT" +
+ ", publisher_title TEXT" +
+ ", thumbnail_url TEXT" +
+ ", release_time INTEGER " +
+ ", device TEXT" +
+ ", timestamp TEXT NOT NULL)"
+ };
+ private static final String[] SQL_CREATE_INDEXES = {
+ "CREATE UNIQUE INDEX idx_subscription_url ON subscriptions (url)",
+ "CREATE UNIQUE INDEX idx_url_history_value ON url_history (value)",
+ "CREATE UNIQUE INDEX idx_url_history_url ON url_history (url)",
+ "CREATE UNIQUE INDEX idx_tag_name ON tags (name)",
+ "CREATE UNIQUE INDEX idx_view_history_url_device ON view_history (url, device)",
+ "CREATE INDEX idx_view_history_device ON view_history (device)"
+ };
+
+ private static final String[] SQL_V1_V2_UPGRADE = {
+ "ALTER TABLE view_history ADD COLUMN currency TEXT"
+ };
+
+ private static final String SQL_INSERT_SUBSCRIPTION = "REPLACE INTO subscriptions (channel_name, url) VALUES (?, ?)";
+ private static final String SQL_DELETE_SUBSCRIPTION = "DELETE FROM subscriptions WHERE url = ?";
+ private static final String SQL_GET_SUBSCRIPTIONS = "SELECT channel_name, url FROM subscriptions";
+
+ private static final String SQL_INSERT_URL_HISTORY = "REPLACE INTO url_history (value, url, type, timestamp) VALUES (?, ?, ?, ?)";
+ private static final String SQL_CLEAR_URL_HISTORY = "DELETE FROM url_history";
+ private static final String SQL_CLEAR_URL_HISTORY_BEFORE_TIME = "DELETE FROM url_history WHERE timestamp < ?";
+ private static final String SQL_GET_RECENT_URL_HISTORY = "SELECT value, url, type FROM url_history ORDER BY timestamp DESC LIMIT 10";
+
+ private static final String SQL_INSERT_VIEW_HISTORY =
+ "REPLACE INTO view_history (url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ private static final String SQL_GET_VIEW_HISTORY =
+ "SELECT url, claim_id, claim_name, cost, currency, title, publisher_claim_id, publisher_name, publisher_title, thumbnail_url, device, release_time, timestamp " +
+ "FROM view_history WHERE '' = ? OR timestamp < ? ORDER BY timestamp DESC LIMIT %d";
+ private static final String SQL_CLEAR_VIEW_HISTORY = "DELETE FROM view_history";
+ private static final String SQL_CLEAR_VIEW_HISTORY_BY_DEVICE = "DELETE FROM view_history WHERE device = ?";
+ private static final String SQL_CLEAR_VIEW_HISTORY_BEFORE_TIME = "DELETE FROM view_history WHERE timestamp < ?";
+ private static final String SQL_CLEAR_VIEW_HISTORY_BY_DEVICE_BEFORE_TIME = "DELETE FROM view_history WHERE device = ? AND timestamp < ?";
+
+ private static final String SQL_INSERT_TAG = "REPLACE INTO tags (name, is_followed) VALUES (?, ?)";
+ private static final String SQL_GET_KNOWN_TAGS = "SELECT name, is_followed FROM tags";
+ private static final String SQL_UNFOLLOW_TAGS = "UPDATE tags SET is_followed = 0";
+ private static final String SQL_GET_FOLLOWED_TAGS = "SELECT name FROM tags WHERE is_followed = 1";
+
+
+
+ public DatabaseHelper(Context context) {
+ super(context, String.format("%s/%s", context.getFilesDir().getAbsolutePath(), DATABASE_NAME), null, DATABASE_VERSION);
+ instance = this;
+ }
+ public static DatabaseHelper getInstance() {
+ return instance;
+ }
+ public void onCreate(SQLiteDatabase db) {
+ for (String sql : SQL_CREATE_TABLES) {
+ db.execSQL(sql);
+ }
+ for (String sql : SQL_CREATE_INDEXES) {
+ db.execSQL(sql);
+ }
+ }
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 2) {
+ for (String sql : SQL_V1_V2_UPGRADE) {
+ db.execSQL(sql);
+ }
+ }
+ }
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+ }
+
+ public static void createOrUpdateUrlHistoryItem(String text, String url, int type, SQLiteDatabase db) {
+ db.execSQL(SQL_INSERT_URL_HISTORY, new Object[] {
+ text, url, type, new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date())
+ });
+ }
+ public static void clearUrlHistory(SQLiteDatabase db) {
+ db.execSQL(SQL_CLEAR_URL_HISTORY);
+ }
+ public static void clearUrlHistoryBefore(Date date, SQLiteDatabase db) {
+ db.execSQL(SQL_CLEAR_URL_HISTORY_BEFORE_TIME, new Object[] { new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date()) });
+ }
+ // History items are essentially url suggestions
+ public static List getRecentHistory(SQLiteDatabase db) {
+ List suggestions = new ArrayList<>();
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(SQL_GET_RECENT_URL_HISTORY, null);
+ while (cursor.moveToNext()) {
+ UrlSuggestion suggestion = new UrlSuggestion();
+ suggestion.setText(cursor.getString(0));
+ suggestion.setUri(cursor.isNull(1) ? null : LbryUri.tryParse(cursor.getString(1)));
+ suggestion.setType(cursor.getInt(2));
+ suggestion.setTitleUrlOnly(true);
+ suggestions.add(suggestion);
+ }
+ } finally {
+ Helper.closeCursor(cursor);
+ }
+ return suggestions;
+ }
+
+ // View history items are stream claims
+ public static void createOrUpdateViewHistoryItem(ViewHistory viewHistory, SQLiteDatabase db) {
+ db.execSQL(SQL_INSERT_VIEW_HISTORY, new Object[] {
+ viewHistory.getUri().toString(),
+ viewHistory.getClaimId(),
+ viewHistory.getClaimName(),
+ viewHistory.getCost() != null ? viewHistory.getCost().doubleValue() : 0,
+ viewHistory.getCurrency(),
+ viewHistory.getTitle(),
+ viewHistory.getPublisherClaimId(),
+ viewHistory.getPublisherName(),
+ viewHistory.getPublisherTitle(),
+ viewHistory.getThumbnailUrl(),
+ viewHistory.getDevice(),
+ viewHistory.getReleaseTime(),
+ new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).format(new Date())
+ });
+ }
+
+ public static List getViewHistory(String lastTimestamp, int pageLimit, SQLiteDatabase db) {
+ List history = new ArrayList<>();
+ Cursor cursor = null;
+ try {
+ String arg = lastTimestamp == null ? "" : lastTimestamp;
+ cursor = db.rawQuery(String.format(SQL_GET_VIEW_HISTORY, pageLimit), new String[] { arg, arg });
+ while (cursor.moveToNext()) {
+ ViewHistory item = new ViewHistory();
+ int cursorIndex = 0;
+ item.setUri(LbryUri.tryParse(cursor.getString(cursorIndex++)));
+ item.setClaimId(cursor.getString(cursorIndex++));
+ item.setClaimName(cursor.getString(cursorIndex++));
+ item.setCost(new BigDecimal(cursor.getDouble(cursorIndex++)));
+ item.setCurrency(cursor.getString(cursorIndex++));
+ item.setTitle(cursor.getString(cursorIndex++));
+ item.setPublisherClaimId(cursor.getString(cursorIndex++));
+ item.setPublisherName(cursor.getString(cursorIndex++));
+ item.setPublisherTitle(cursor.getString(cursorIndex++));
+ item.setThumbnailUrl(cursor.getString(cursorIndex++));
+ item.setDevice(cursor.getString(cursorIndex++));
+ item.setReleaseTime(cursor.getLong(cursorIndex++));
+ try {
+ item.setTimestamp(new SimpleDateFormat(Helper.ISO_DATE_FORMAT_PATTERN).parse(cursor.getString(cursorIndex)));
+ } catch (ParseException ex) {
+ // invalid timestamp (which shouldn't happen). Skip this item
+ continue;
+ }
+
+ history.add(item);
+ }
+ } finally {
+ Helper.closeCursor(cursor);
+ }
+ return history;
+ }
+
+ public static void createOrUpdateTag(Tag tag, SQLiteDatabase db) {
+ db.execSQL(SQL_INSERT_TAG, new Object[] { tag.getLowercaseName(), tag.isFollowed() ? 1 : 0 });
+ }
+ public static void setAllTagsUnfollowed(SQLiteDatabase db) {
+ db.execSQL(SQL_UNFOLLOW_TAGS);
+ }
+ public static List getTags(SQLiteDatabase db) {
+ List tags = new ArrayList<>();
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(SQL_GET_KNOWN_TAGS, null);
+ while (cursor.moveToNext()) {
+ Tag tag = new Tag();
+ tag.setName(cursor.getString(0));
+ tag.setFollowed(cursor.getInt(1) == 1);
+ tags.add(tag);
+ }
+ } finally {
+ Helper.closeCursor(cursor);
+ }
+ return tags;
+ }
+
+ public static void createOrUpdateSubscription(Subscription subscription, SQLiteDatabase db) {
+ db.execSQL(SQL_INSERT_SUBSCRIPTION, new Object[] { subscription.getChannelName(), subscription.getUrl() });
+ }
+ public static void deleteSubscription(Subscription subscription, SQLiteDatabase db) {
+ db.execSQL(SQL_DELETE_SUBSCRIPTION, new Object[] { subscription.getUrl() });
+ }
+ public static List getSubscriptions(SQLiteDatabase db) {
+ List subscriptions = new ArrayList<>();
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery(SQL_GET_SUBSCRIPTIONS, null);
+ while (cursor.moveToNext()) {
+ Subscription subscription = new Subscription();
+ subscription.setChannelName(cursor.getString(0));
+ subscription.setUrl(cursor.getString(1));
+ subscriptions.add(subscription);
+ }
+ } finally {
+ Helper.closeCursor(cursor);
+ }
+ return subscriptions;
+ }
+
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/ContentFromDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/ContentFromDialogFragment.java
new file mode 100644
index 00000000..d81102b7
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/ContentFromDialogFragment.java
@@ -0,0 +1,112 @@
+package io.lbry.browser.dialog;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import io.lbry.browser.R;
+import lombok.Setter;
+
+public class ContentFromDialogFragment extends BottomSheetDialogFragment {
+ public static final String TAG = "ContentFromDialog";
+ public static final int ITEM_FROM_PAST_24_HOURS = 1;
+ public static final int ITEM_FROM_PAST_WEEK = 2;
+ public static final int ITEM_FROM_PAST_MONTH = 3;
+ public static final int ITEM_FROM_PAST_YEAR = 4;
+ public static final int ITEM_FROM_ALL_TIME = 5;
+
+ @Setter
+ private ContentFromListener contentFromListener;
+ private int currentFromItem;
+
+ public static ContentFromDialogFragment newInstance() {
+ return new ContentFromDialogFragment();
+ }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_content_from, container,false);
+
+ ContentFromItemClickListener clickListener = new ContentFromItemClickListener(this, contentFromListener);
+ view.findViewById(R.id.content_from_past_24_hours_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.content_from_past_week_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.content_from_past_month_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.content_from_past_year_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.content_from_all_time_item).setOnClickListener(clickListener);
+ checkSelectedFromItem(currentFromItem, view);
+
+ return view;
+ }
+
+ public static void checkSelectedFromItem(int fromItem, View parent) {
+ int checkViewId = -1;
+ switch (fromItem) {
+ case ITEM_FROM_PAST_24_HOURS: checkViewId = R.id.content_from_past_24_hours_item_selected; break;
+ case ITEM_FROM_PAST_WEEK: checkViewId = R.id.content_from_past_week_item_selected; break;
+ case ITEM_FROM_PAST_MONTH: checkViewId = R.id.content_from_past_month_item_selected; break;
+ case ITEM_FROM_PAST_YEAR: checkViewId = R.id.content_from_past_year_item_selected; break;
+ case ITEM_FROM_ALL_TIME: checkViewId = R.id.content_from_all_time_item_selected; break;
+ }
+ if (parent != null && checkViewId > -1) {
+ parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setCurrentFromItem(int fromItem) {
+ this.currentFromItem = fromItem;
+ }
+
+ private static class ContentFromItemClickListener implements View.OnClickListener {
+
+ private final int[] checkViewIds = {
+ R.id.content_from_past_24_hours_item,
+ R.id.content_from_past_week_item,
+ R.id.content_from_past_month_item,
+ R.id.content_from_past_year_item,
+ R.id.content_from_all_time_item
+ };
+ private BottomSheetDialogFragment dialog;
+ private ContentFromListener listener;
+
+ public ContentFromItemClickListener(BottomSheetDialogFragment dialog, ContentFromListener listener) {
+ this.dialog = dialog;
+ this.listener = listener;
+ }
+
+ public void onClick(View view) {
+ int currentFromItem = -1;
+
+ if (dialog != null) {
+ View dialogView = dialog.getView();
+ if (dialogView != null) {
+ for (int id : checkViewIds) {
+ dialogView.findViewById(id).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ switch (view.getId()) {
+ case R.id.content_from_past_24_hours_item: currentFromItem = ITEM_FROM_PAST_24_HOURS; break;
+ case R.id.content_from_past_week_item: currentFromItem = ITEM_FROM_PAST_WEEK; break;
+ case R.id.content_from_past_month_item: currentFromItem = ITEM_FROM_PAST_MONTH; break;
+ case R.id.content_from_past_year_item: currentFromItem = ITEM_FROM_PAST_YEAR; break;
+ case R.id.content_from_all_time_item: currentFromItem = ITEM_FROM_ALL_TIME; break;
+ }
+
+ checkSelectedFromItem(currentFromItem, view);
+ if (listener != null) {
+ listener.onContentFromItemSelected(currentFromItem);
+ }
+
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ }
+
+ public interface ContentFromListener {
+ void onContentFromItemSelected(int contentFromItem);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/ContentScopeDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/ContentScopeDialogFragment.java
new file mode 100644
index 00000000..4db24f8d
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/ContentScopeDialogFragment.java
@@ -0,0 +1,96 @@
+package io.lbry.browser.dialog;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import io.lbry.browser.R;
+import lombok.Setter;
+
+public class ContentScopeDialogFragment extends BottomSheetDialogFragment {
+ public static final String TAG = "ContentScopeDialog";
+ public static final int ITEM_EVERYONE = 1;
+ public static final int ITEM_TAGS = 2;
+
+ @Setter
+ private ContentScopeListener contentScopeListener;
+ private int currentScopeItem;
+
+ public static ContentScopeDialogFragment newInstance() {
+ return new ContentScopeDialogFragment();
+ }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_content_scope, container,false);
+
+ ContentScopeItemClickListener clickListener = new ContentScopeItemClickListener(this, contentScopeListener);
+ view.findViewById(R.id.content_scope_everyone_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.content_scope_tags_item).setOnClickListener(clickListener);
+ checkSelectedScopeItem(currentScopeItem, view);
+
+ return view;
+ }
+
+ public static void checkSelectedScopeItem(int scope, View parent) {
+ int checkViewId = -1;
+ switch (scope) {
+ case ITEM_EVERYONE: checkViewId = R.id.content_scope_everyone_item_selected; break;
+ case ITEM_TAGS: checkViewId = R.id.content_scope_tags_item_selected; break;
+ }
+ if (parent != null && checkViewId > -1) {
+ parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setCurrentScopeItem(int scopeItem) {
+ this.currentScopeItem = scopeItem;
+ }
+
+ private static class ContentScopeItemClickListener implements View.OnClickListener {
+
+ private final int[] checkViewIds = {
+ R.id.content_scope_everyone_item_selected, R.id.content_scope_tags_item_selected
+ };
+ private BottomSheetDialogFragment dialog;
+ private ContentScopeListener listener;
+
+ public ContentScopeItemClickListener(BottomSheetDialogFragment dialog, ContentScopeListener listener) {
+ this.dialog = dialog;
+ this.listener = listener;
+ }
+
+ public void onClick(View view) {
+ int scopeItem = -1;
+
+ if (dialog != null) {
+ View dialogView = dialog.getView();
+ if (dialogView != null) {
+ for (int id : checkViewIds) {
+ dialogView.findViewById(id).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ switch (view.getId()) {
+ case R.id.content_scope_everyone_item: scopeItem = ITEM_EVERYONE; break;
+ case R.id.content_scope_tags_item: scopeItem = ITEM_TAGS; break;
+ }
+
+ checkSelectedScopeItem(scopeItem, view);
+ if (listener != null) {
+ listener.onContentScopeItemSelected(scopeItem);
+ }
+
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ }
+
+ public interface ContentScopeListener {
+ void onContentScopeItemSelected(int scopeItem);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/ContentSortDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/ContentSortDialogFragment.java
new file mode 100644
index 00000000..06ac5e57
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/ContentSortDialogFragment.java
@@ -0,0 +1,100 @@
+package io.lbry.browser.dialog;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import io.lbry.browser.R;
+import lombok.Setter;
+
+public class ContentSortDialogFragment extends BottomSheetDialogFragment {
+ public static final String TAG = "ContentSortDialog";
+ public static final int ITEM_SORT_BY_TRENDING = 1;
+ public static final int ITEM_SORT_BY_NEW = 2;
+ public static final int ITEM_SORT_BY_TOP = 3;
+
+ @Setter
+ private SortByListener sortByListener;
+ private int currentSortByItem;
+
+ public static ContentSortDialogFragment newInstance() {
+ return new ContentSortDialogFragment();
+ }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_content_sort, container,false);
+
+ SortByItemClickListener clickListener = new SortByItemClickListener(this, sortByListener);
+ view.findViewById(R.id.sort_by_trending_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.sort_by_new_item).setOnClickListener(clickListener);
+ view.findViewById(R.id.sort_by_top_item).setOnClickListener(clickListener);
+ checkSelectedSortByItem(currentSortByItem, view);
+
+ return view;
+ }
+
+ public static void checkSelectedSortByItem(int sortByItem, View parent) {
+ int checkViewId = -1;
+ switch (sortByItem) {
+ case ITEM_SORT_BY_TRENDING: checkViewId = R.id.sort_by_trending_item_selected; break;
+ case ITEM_SORT_BY_NEW: checkViewId = R.id.sort_by_new_item_selected; break;
+ case ITEM_SORT_BY_TOP: checkViewId = R.id.sort_by_top_item_selected; break;
+ }
+ if (parent != null && checkViewId > -1) {
+ parent.findViewById(checkViewId).setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setCurrentSortByItem(int sortByItem) {
+ this.currentSortByItem = sortByItem;
+ }
+
+ private static class SortByItemClickListener implements View.OnClickListener {
+
+ private final int[] checkViewIds = {
+ R.id.sort_by_trending_item_selected, R.id.sort_by_new_item_selected, R.id.sort_by_top_item_selected
+ };
+ private BottomSheetDialogFragment dialog;
+ private SortByListener listener;
+
+ public SortByItemClickListener(BottomSheetDialogFragment dialog, SortByListener listener) {
+ this.dialog = dialog;
+ this.listener = listener;
+ }
+
+ public void onClick(View view) {
+ int selectedSortByItem = -1;
+
+ if (dialog != null) {
+ View dialogView = dialog.getView();
+ if (dialogView != null) {
+ for (int id : checkViewIds) {
+ dialogView.findViewById(id).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ switch (view.getId()) {
+ case R.id.sort_by_trending_item: selectedSortByItem = ITEM_SORT_BY_TRENDING; break;
+ case R.id.sort_by_new_item: selectedSortByItem = ITEM_SORT_BY_NEW; break;
+ case R.id.sort_by_top_item: selectedSortByItem = ITEM_SORT_BY_TOP; break;
+ }
+
+ checkSelectedSortByItem(selectedSortByItem, view);
+ if (listener != null) {
+ listener.onSortByItemSelected(selectedSortByItem);
+ }
+
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+ }
+
+ public interface SortByListener {
+ void onSortByItemSelected(int sortBy);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/CustomizeTagsDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/CustomizeTagsDialogFragment.java
new file mode 100644
index 00000000..1a328108
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/CustomizeTagsDialogFragment.java
@@ -0,0 +1,186 @@
+package io.lbry.browser.dialog;
+
+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 androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.flexbox.FlexboxLayoutManager;
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.R;
+import io.lbry.browser.adapter.TagListAdapter;
+import io.lbry.browser.listener.TagListener;
+import io.lbry.browser.model.Tag;
+import io.lbry.browser.tasks.UpdateSuggestedTagsTask;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.Lbry;
+import lombok.Setter;
+
+public class CustomizeTagsDialogFragment extends BottomSheetDialogFragment {
+ public static final String TAG = "CustomizeTagsDialog";
+ private static final int SUGGESTED_LIMIT = 8;
+ private String currentFilter;
+
+ private RecyclerView followedTagsList;
+ private RecyclerView suggestedTagsList;
+ private TagListAdapter followedTagsAdapter;
+ private TagListAdapter suggestedTagsAdapter;
+ private View noTagsView;
+ private View noResultsView;
+ @Setter
+ private TagListener listener;
+
+ private void checkNoTags() {
+ Helper.setViewVisibility(noTagsView, followedTagsAdapter == null || followedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ }
+ private void checkNoResults() {
+ Helper.setViewVisibility(noResultsView, suggestedTagsAdapter == null || suggestedTagsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
+ }
+ public void addTag(Tag tag) {
+ if (followedTagsAdapter.getTags().contains(tag)) {
+ Snackbar.make(getView(), getString(R.string.tag_already_added, tag.getName()), Snackbar.LENGTH_LONG).show();
+ return;
+ }
+
+ tag.setFollowed(true);
+ followedTagsAdapter.addTag(tag);
+ if (suggestedTagsAdapter != null) {
+ suggestedTagsAdapter.removeTag(tag);
+ }
+ updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
+ if (listener != null) {
+ listener.onTagAdded(tag);
+ }
+ checkNoTags();
+ checkNoResults();
+ }
+ public void removeTag(Tag tag) {
+ tag.setFollowed(false);
+ followedTagsAdapter.removeTag(tag);
+ updateKnownTags(currentFilter, SUGGESTED_LIMIT, false);
+ if (listener != null) {
+ listener.onTagRemoved(tag);
+ }
+ checkNoTags();
+ checkNoResults();
+ }
+
+ public void setFilter(String filter) {
+ currentFilter = filter;
+ updateKnownTags(currentFilter, SUGGESTED_LIMIT, true);
+ }
+
+ public static CustomizeTagsDialogFragment newInstance() {
+ return new CustomizeTagsDialogFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_customize_tags, container, false);
+
+ noResultsView = view.findViewById(R.id.customize_no_tag_results);
+ noTagsView = view.findViewById(R.id.customize_no_followed_tags);
+
+ followedTagsAdapter = new TagListAdapter(Lbry.followedTags, getContext());
+ followedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_REMOVE);
+ followedTagsAdapter.setClickListener(customizeTagClickListener);
+ suggestedTagsAdapter = new TagListAdapter(new ArrayList<>(), getContext());
+ suggestedTagsAdapter.setCustomizeMode(TagListAdapter.CUSTOMIZE_MODE_ADD);
+ suggestedTagsAdapter.setClickListener(customizeTagClickListener);
+
+ FlexboxLayoutManager flm1 = new FlexboxLayoutManager(getContext());
+ followedTagsList = view.findViewById(R.id.customize_tags_followed_list);
+ followedTagsList.setLayoutManager(flm1);
+ followedTagsList.setAdapter(followedTagsAdapter);
+
+ FlexboxLayoutManager flm2 = new FlexboxLayoutManager(getContext());
+ suggestedTagsList = view.findViewById(R.id.customize_tags_suggested_list);
+ suggestedTagsList.setLayoutManager(flm2);
+ suggestedTagsList.setAdapter(suggestedTagsAdapter);
+
+ TextInputEditText filterInput = view.findViewById(R.id.customize_tag_filter_input);
+ filterInput.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) {
+
+ }
+ });
+
+
+ MaterialButton doneButton = view.findViewById(R.id.customize_done_button);
+ doneButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ checkNoTags();
+ return view;
+ }
+
+ private TagListAdapter.TagClickListener customizeTagClickListener = new TagListAdapter.TagClickListener() {
+ @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);
+ }
+ }
+ };
+
+ public void onResume() {
+ super.onResume();
+ updateKnownTags(null, SUGGESTED_LIMIT, true);
+ }
+
+ private void updateKnownTags(String filter, int limit, boolean clearPrevious) {
+ UpdateSuggestedTagsTask task = new UpdateSuggestedTagsTask(
+ filter,
+ SUGGESTED_LIMIT,
+ followedTagsAdapter,
+ suggestedTagsAdapter,
+ clearPrevious,
+ false, 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(customizeTagClickListener);
+ if (suggestedTagsList != null) {
+ suggestedTagsList.setAdapter(suggestedTagsAdapter);
+ }
+ } else {
+ suggestedTagsAdapter.setTags(tags);
+ }
+ checkNoResults();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/DiscoverDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/DiscoverDialogFragment.java
new file mode 100644
index 00000000..927ebda1
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/DiscoverDialogFragment.java
@@ -0,0 +1,107 @@
+package io.lbry.browser.dialog;
+
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.button.MaterialButton;
+
+import io.lbry.browser.R;
+import io.lbry.browser.adapter.SuggestedChannelGridAdapter;
+import lombok.Getter;
+import lombok.Setter;
+
+public class DiscoverDialogFragment extends BottomSheetDialogFragment {
+ public static final String TAG = "DiscoverDialog";
+
+ @Getter
+ private SuggestedChannelGridAdapter adapter;
+ @Setter
+ private DiscoverDialogListener dialogActionsListener;
+
+ public static DiscoverDialogFragment newInstance() {
+ return new DiscoverDialogFragment();
+ }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_discover, container, false);
+
+ RecyclerView grid = view.findViewById(R.id.discover_channel_grid);
+ GridLayoutManager glm = new GridLayoutManager(getContext(), 3);
+ grid.setLayoutManager(glm);
+ grid.setAdapter(adapter);
+ grid.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ GridLayoutManager lm = (GridLayoutManager) recyclerView.getLayoutManager();
+ if (lm != null) {
+ int visibleItemCount = lm.getChildCount();
+ int totalItemCount = lm.getItemCount();
+ int pastVisibleItems = lm.findFirstVisibleItemPosition();
+ if (pastVisibleItems + visibleItemCount >= totalItemCount) {
+ if (dialogActionsListener != null) {
+ dialogActionsListener.onScrollEndReached();
+ }
+ }
+ }
+ }
+ });
+
+ MaterialButton doneButton = view.findViewById(R.id.discover_done_button);
+ doneButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ return view;
+ }
+ public void setAdapter(SuggestedChannelGridAdapter adapter) {
+ this.adapter = adapter;
+ if (getView() != null) {
+ ((RecyclerView) getView().findViewById(R.id.discover_channel_grid)).setAdapter(adapter);
+ }
+ }
+ public void setLoading(boolean loading) {
+ if (getView() != null) {
+ getView().findViewById(R.id.discover_loading).setVisibility(loading ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (dialogActionsListener != null) {
+ dialogActionsListener.onResume();
+ }
+ }
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (dialogActionsListener != null) {
+ dialogActionsListener.onCancel();
+ }
+ }
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (dialogActionsListener != null) {
+ dialogActionsListener.onCancel();
+ }
+ }
+
+ public interface DiscoverDialogListener {
+ void onResume();
+ void onCancel();
+ void onScrollEndReached();
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/RepostClaimDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/RepostClaimDialogFragment.java
new file mode 100644
index 00000000..f34099ce
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/RepostClaimDialogFragment.java
@@ -0,0 +1,291 @@
+package io.lbry.browser.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.appcompat.widget.AppCompatSpinner;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+import io.lbry.browser.MainActivity;
+import io.lbry.browser.R;
+import io.lbry.browser.adapter.InlineChannelSpinnerAdapter;
+import io.lbry.browser.listener.WalletBalanceListener;
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.model.WalletBalance;
+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.StreamRepostTask;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.Lbry;
+import io.lbry.browser.utils.LbryUri;
+import lombok.Setter;
+
+public class RepostClaimDialogFragment extends BottomSheetDialogFragment implements WalletBalanceListener {
+ public static final String TAG = "RepostClaimDialog";
+
+ private MaterialButton buttonRepost;
+ private View linkCancel;
+ private TextInputEditText inputDeposit;
+ private View inlineBalanceContainer;
+ private TextView inlineBalanceValue;
+ private ProgressBar repostProgress;
+ private TextView textTitle;
+
+ private AppCompatSpinner channelSpinner;
+ private InlineChannelSpinnerAdapter channelSpinnerAdapter;
+ private TextView textNamePrefix;
+ private EditText inputName;
+ private TextView linkToggleAdvanced;
+ private View advancedContainer;
+
+ @Setter
+ private RepostClaimListener listener;
+ @Setter
+ private Claim claim;
+
+ public static RepostClaimDialogFragment newInstance() {
+ return new RepostClaimDialogFragment();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_repost_claim, container, false);
+
+ buttonRepost = view.findViewById(R.id.repost_button);
+ linkCancel = view.findViewById(R.id.repost_cancel_link);
+ inputDeposit = view.findViewById(R.id.repost_input_deposit);
+ inlineBalanceContainer = view.findViewById(R.id.repost_inline_balance_container);
+ inlineBalanceValue = view.findViewById(R.id.repost_inline_balance_value);
+ repostProgress = view.findViewById(R.id.repost_progress);
+ textTitle = view.findViewById(R.id.repost_title);
+
+ channelSpinner = view.findViewById(R.id.repost_channel_spinner);
+ textNamePrefix = view.findViewById(R.id.repost_name_prefix);
+ inputName = view.findViewById(R.id.repost_name_input);
+ linkToggleAdvanced = view.findViewById(R.id.repost_toggle_advanced);
+ advancedContainer = view.findViewById(R.id.repost_advanced_container);
+
+ textTitle.setText(getString(R.string.repost_title, claim.getTitle()));
+ inputName.setText(claim.getName());
+ inputDeposit.setText(R.string.min_deposit);
+ channelSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int position, long l) {
+ Object item = adapterView.getItemAtPosition(position);
+ if (item instanceof Claim) {
+ Claim claim = (Claim) item;
+ textNamePrefix.setText(String.format("%s%s/", LbryUri.PROTO_DEFAULT, claim.getName()));
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+
+ }
+ });
+
+ inputDeposit.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ inputDeposit.setHint(hasFocus ? getString(R.string.zero) : "");
+ inlineBalanceContainer.setVisibility(hasFocus ? View.VISIBLE : View.INVISIBLE);
+ }
+ });
+
+ linkCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ linkToggleAdvanced.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (advancedContainer.getVisibility() != View.VISIBLE) {
+ advancedContainer.setVisibility(View.VISIBLE);
+ linkToggleAdvanced.setText(R.string.hide_advanced);
+ } else {
+ advancedContainer.setVisibility(View.GONE);
+ linkToggleAdvanced.setText(R.string.show_advanced);
+ }
+ }
+ });
+
+ buttonRepost.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ validateAndRepostClaim();
+ }
+ });
+
+ onWalletBalanceUpdated(Lbry.walletBalance);
+
+ return view;
+ }
+
+ public void onResume() {
+ super.onResume();
+ Context context = getContext();
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).addWalletBalanceListener(this);
+ }
+ fetchChannels();
+ }
+
+ public void onPause() {
+ Context context = getContext();
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).removeWalletBalanceListener(this);
+ }
+ inputDeposit.clearFocus();
+ super.onPause();
+ }
+
+
+ private void fetchChannels() {
+ if (Lbry.ownChannels == null || Lbry.ownChannels.size() == 0) {
+ startLoading();
+ ClaimListTask task = new ClaimListTask(Claim.TYPE_CHANNEL, repostProgress, new ClaimListResultHandler() {
+ @Override
+ public void onSuccess(List claims) {
+ Lbry.ownChannels = new ArrayList<>(claims);
+ loadChannels(claims);
+ finishLoading();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ // could not fetch channels
+ Context context = getContext();
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).showError(error.getMessage());
+ }
+ dismiss();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ loadChannels(Lbry.ownChannels);
+ }
+ }
+
+ private void loadChannels(List channels) {
+ if (channelSpinnerAdapter == null) {
+ Context context = getContext();
+ channelSpinnerAdapter = new InlineChannelSpinnerAdapter(context, R.layout.spinner_item_channel, channels);
+ channelSpinnerAdapter.notifyDataSetChanged();
+ } else {
+ channelSpinnerAdapter.clear();
+ channelSpinnerAdapter.addAll(channels);
+ channelSpinnerAdapter.notifyDataSetChanged();
+ }
+ if (channelSpinner != null) {
+ channelSpinner.setAdapter(channelSpinnerAdapter);
+ }
+ }
+
+ @Override
+ public void onWalletBalanceUpdated(WalletBalance walletBalance) {
+ if (walletBalance != null && inlineBalanceValue != null) {
+ inlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue()));
+ }
+ }
+
+ private void validateAndRepostClaim() {
+ String name = Helper.getValue(inputName.getText());
+ if (Helper.isNullOrEmpty(name) || !LbryUri.isNameValid(name)) {
+ showError(getString(R.string.repost_name_invalid_characters));
+ return;
+ }
+
+ String depositString = Helper.getValue(inputDeposit.getText());
+ if (Helper.isNullOrEmpty(depositString)) {
+ showError(getString(R.string.invalid_amount));
+ return;
+ }
+
+ BigDecimal bid = new BigDecimal(depositString);
+ if (bid.doubleValue() > Lbry.walletBalance.getAvailable().doubleValue()) {
+ showError(getString(R.string.insufficient_balance));
+ return;
+ }
+
+ Claim channel = (Claim) channelSpinner.getSelectedItem();
+ StreamRepostTask task = new StreamRepostTask(name, bid, claim.getClaimId(), channel.getClaimId(), repostProgress, new ClaimResultHandler() {
+ @Override
+ public void beforeStart() {
+ startLoading();
+ }
+
+ @Override
+ public void onSuccess(Claim claimResult) {
+ if (listener != null) {
+ listener.onClaimReposted(claimResult);
+ }
+ finishLoading();
+ dismiss();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ showError(error.getMessage());
+ finishLoading();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private void showError(String message) {
+ Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).
+ setBackgroundTint(Color.RED).
+ setTextColor(Color.WHITE).
+ show();
+ }
+
+ private void startLoading() {
+ Dialog dialog = getDialog();
+ if (dialog != null) {
+ dialog.setCanceledOnTouchOutside(false);
+ }
+ linkCancel.setEnabled(false);
+ buttonRepost.setEnabled(false);
+ inputName.setEnabled(false);
+ channelSpinner.setEnabled(false);
+ linkToggleAdvanced.setVisibility(View.INVISIBLE);
+ }
+ private void finishLoading() {
+ Dialog dialog = getDialog();
+ if (dialog != null) {
+ dialog.setCanceledOnTouchOutside(true);
+ }
+ linkCancel.setEnabled(true);
+ buttonRepost.setEnabled(true);
+ inputName.setEnabled(true);
+ channelSpinner.setEnabled(true);
+ linkToggleAdvanced.setVisibility(View.VISIBLE);
+ }
+
+ public interface RepostClaimListener {
+ void onClaimReposted(Claim claim);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/dialog/SendTipDialogFragment.java b/app/src/main/java/io/lbry/browser/dialog/SendTipDialogFragment.java
new file mode 100644
index 00000000..6be2d917
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/dialog/SendTipDialogFragment.java
@@ -0,0 +1,196 @@
+package io.lbry.browser.dialog;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.method.LinkMovementMethod;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+import androidx.core.text.HtmlCompat;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+import com.google.android.material.button.MaterialButton;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.textfield.TextInputEditText;
+
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+
+import io.lbry.browser.MainActivity;
+import io.lbry.browser.R;
+import io.lbry.browser.listener.WalletBalanceListener;
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.model.WalletBalance;
+import io.lbry.browser.tasks.GenericTaskHandler;
+import io.lbry.browser.tasks.wallet.SupportCreateTask;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.Lbry;
+import lombok.Setter;
+
+public class SendTipDialogFragment extends BottomSheetDialogFragment implements WalletBalanceListener {
+ public static final String TAG = "SendTipDialog";
+
+ private MaterialButton sendButton;
+ private View cancelLink;
+ private TextInputEditText inputAmount;
+ private View inlineBalanceContainer;
+ private TextView inlineBalanceValue;
+ private ProgressBar sendProgress;
+
+ @Setter
+ private SendTipListener listener;
+ @Setter
+ private Claim claim;
+
+ public static SendTipDialogFragment newInstance() {
+ return new SendTipDialogFragment();
+ }
+
+ private void disableControls() {
+ Dialog dialog = getDialog();
+ if (dialog != null) {
+ dialog.setCanceledOnTouchOutside(false);
+ }
+ sendButton.setEnabled(false);
+ cancelLink.setEnabled(false);
+ }
+ private void enableControls() {
+ Dialog dialog = getDialog();
+ if (dialog != null) {
+ dialog.setCanceledOnTouchOutside(true);
+ }
+ sendButton.setEnabled(true);
+ cancelLink.setEnabled(true);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialog_send_tip, container, false);
+
+ inputAmount = view.findViewById(R.id.tip_input_amount);
+ inlineBalanceContainer = view.findViewById(R.id.tip_inline_balance_container);
+ inlineBalanceValue = view.findViewById(R.id.tip_inline_balance_value);
+ sendProgress = view.findViewById(R.id.tip_send_progress);
+ cancelLink = view.findViewById(R.id.tip_cancel_link);
+ sendButton = view.findViewById(R.id.tip_send);
+
+ inputAmount.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ inputAmount.setHint(hasFocus ? getString(R.string.zero) : "");
+ inlineBalanceContainer.setVisibility(hasFocus ? View.VISIBLE : View.INVISIBLE);
+ }
+ });
+
+ TextView infoText = view.findViewById(R.id.tip_info);
+ infoText.setMovementMethod(LinkMovementMethod.getInstance());
+ infoText.setText(HtmlCompat.fromHtml(
+ Claim.TYPE_CHANNEL.equalsIgnoreCase(claim.getValueType()) ?
+ getString(R.string.send_tip_info_channel, claim.getTitleOrName()) :
+ getString(R.string.send_tip_info_content, claim.getTitleOrName()),
+ HtmlCompat.FROM_HTML_MODE_LEGACY));
+
+ sendButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String amountString = Helper.getValue(inputAmount.getText());
+ if (Helper.isNullOrEmpty(amountString)) {
+ showError(getString(R.string.invalid_amount));
+ return;
+ }
+
+ BigDecimal amount = new BigDecimal(amountString);
+ if (amount.doubleValue() > Lbry.walletBalance.getAvailable().doubleValue()) {
+ showError(getString(R.string.insufficient_balance));
+ return;
+ }
+
+ SupportCreateTask task = new SupportCreateTask(claim.getClaimId(), amount, true, sendProgress, new GenericTaskHandler() {
+ @Override
+ public void beforeStart() {
+ disableControls();
+ }
+
+ @Override
+ public void onSuccess() {
+ enableControls();
+ if (listener != null) {
+ listener.onTipSent(amount);
+ }
+
+ dismiss();
+ }
+
+ @Override
+ public void onError(Exception error) {
+ showError(error.getMessage());
+ enableControls();
+ }
+ });
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+
+ cancelLink.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dismiss();
+ }
+ });
+
+ String channel = null;
+ if (Claim.TYPE_CHANNEL.equalsIgnoreCase(claim.getValueType())) {
+ channel = claim.getTitleOrName();
+ } else if (claim.getSigningChannel() != null) {
+ channel = claim.getPublisherTitle();
+ }
+ ((TextView) view.findViewById(R.id.tip_send_title)).setText(
+ Helper.isNullOrEmpty(channel) ? getString(R.string.send_a_tip) : getString(R.string.send_a_tip_to, channel)
+ );
+
+ onWalletBalanceUpdated(Lbry.walletBalance);
+
+ return view;
+ }
+
+ public void onResume() {
+ super.onResume();
+ Context context = getContext();
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).addWalletBalanceListener(this);
+ }
+ }
+
+ public void onPause() {
+ Context context = getContext();
+ if (context instanceof MainActivity) {
+ ((MainActivity) context).removeWalletBalanceListener(this);
+ }
+ super.onPause();
+ }
+
+ @Override
+ public void onWalletBalanceUpdated(WalletBalance walletBalance) {
+ if (walletBalance != null && inlineBalanceValue != null) {
+ inlineBalanceValue.setText(Helper.shortCurrencyFormat(walletBalance.getAvailable().doubleValue()));
+ }
+ }
+
+ private void showError(String message) {
+ Snackbar.make(getView(), message, Snackbar.LENGTH_LONG).
+ setBackgroundTint(Color.RED).
+ setTextColor(Color.WHITE).
+ show();
+ }
+
+ public interface SendTipListener {
+ void onTipSent(BigDecimal amount);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/ApiCallException.java b/app/src/main/java/io/lbry/browser/exceptions/ApiCallException.java
new file mode 100644
index 00000000..652576c6
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/ApiCallException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class ApiCallException extends Exception {
+ public ApiCallException() {
+ super();
+ }
+ public ApiCallException(String message) {
+ super(message);
+ }
+ public ApiCallException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/LbryRequestException.java b/app/src/main/java/io/lbry/browser/exceptions/LbryRequestException.java
new file mode 100644
index 00000000..a71a09e6
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/LbryRequestException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class LbryRequestException extends Exception {
+ public LbryRequestException() {
+ super();
+ }
+ public LbryRequestException(String message) {
+ super(message);
+ }
+ public LbryRequestException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/LbryResponseException.java b/app/src/main/java/io/lbry/browser/exceptions/LbryResponseException.java
new file mode 100644
index 00000000..63a67e79
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/LbryResponseException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class LbryResponseException extends Exception {
+ public LbryResponseException() {
+ super();
+ }
+ public LbryResponseException(String message) {
+ super(message);
+ }
+ public LbryResponseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/LbryUriException.java b/app/src/main/java/io/lbry/browser/exceptions/LbryUriException.java
new file mode 100644
index 00000000..34fbcb22
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/LbryUriException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class LbryUriException extends Exception {
+ public LbryUriException() {
+ super();
+ }
+ public LbryUriException(String message) {
+ super(message);
+ }
+ public LbryUriException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/LbryioRequestException.java b/app/src/main/java/io/lbry/browser/exceptions/LbryioRequestException.java
new file mode 100644
index 00000000..bb88a0e7
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/LbryioRequestException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class LbryioRequestException extends Exception {
+ public LbryioRequestException() {
+ super();
+ }
+ public LbryioRequestException(String message) {
+ super(message);
+ }
+ public LbryioRequestException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/LbryioResponseException.java b/app/src/main/java/io/lbry/browser/exceptions/LbryioResponseException.java
new file mode 100644
index 00000000..a2a2b7b4
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/LbryioResponseException.java
@@ -0,0 +1,25 @@
+package io.lbry.browser.exceptions;
+
+import lombok.Getter;
+
+public class LbryioResponseException extends Exception {
+ @Getter
+ private int statusCode;
+ public LbryioResponseException() {
+ super();
+ }
+ public LbryioResponseException(String message) {
+ super(message);
+ }
+ public LbryioResponseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ public LbryioResponseException(String message, int statusCode) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+ public LbryioResponseException(String message, Throwable cause, int statusCode) {
+ super(message, cause);
+ this.statusCode = statusCode;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/exceptions/WalletException.java b/app/src/main/java/io/lbry/browser/exceptions/WalletException.java
new file mode 100644
index 00000000..b6dcbc40
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/exceptions/WalletException.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.exceptions;
+
+public class WalletException extends Exception {
+ public WalletException() {
+ super();
+ }
+ public WalletException(String message) {
+ super(message);
+ }
+ public WalletException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/CameraPermissionListener.java b/app/src/main/java/io/lbry/browser/listener/CameraPermissionListener.java
new file mode 100644
index 00000000..ca228c18
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/CameraPermissionListener.java
@@ -0,0 +1,8 @@
+package io.lbry.browser.listener;
+
+public interface CameraPermissionListener {
+ void onCameraPermissionGranted();
+ void onCameraPermissionRefused();
+ void onRecordAudioPermissionGranted();
+ void onRecordAudioPermissionRefused();
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/ChannelItemSelectionListener.java b/app/src/main/java/io/lbry/browser/listener/ChannelItemSelectionListener.java
new file mode 100644
index 00000000..39e06c14
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/ChannelItemSelectionListener.java
@@ -0,0 +1,9 @@
+package io.lbry.browser.listener;
+
+import io.lbry.browser.model.Claim;
+
+public interface ChannelItemSelectionListener {
+ void onChannelItemSelected(Claim claim);
+ void onChannelItemDeselected(Claim claim);
+ void onChannelSelectionCleared();
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java b/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java
new file mode 100644
index 00000000..c93f950e
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/DownloadActionListener.java
@@ -0,0 +1,5 @@
+package io.lbry.browser.listener;
+
+public interface DownloadActionListener {
+ void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress);
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/FetchChannelsListener.java b/app/src/main/java/io/lbry/browser/listener/FetchChannelsListener.java
new file mode 100644
index 00000000..b2f41e21
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/FetchChannelsListener.java
@@ -0,0 +1,9 @@
+package io.lbry.browser.listener;
+
+import java.util.List;
+
+import io.lbry.browser.model.Claim;
+
+public interface FetchChannelsListener {
+ void onChannelsFetched(List channels);
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/FetchClaimsListener.java b/app/src/main/java/io/lbry/browser/listener/FetchClaimsListener.java
new file mode 100644
index 00000000..8a0bbda5
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/FetchClaimsListener.java
@@ -0,0 +1,9 @@
+package io.lbry.browser.listener;
+
+import java.util.List;
+
+import io.lbry.browser.model.Claim;
+
+public interface FetchClaimsListener {
+ void onClaimsFetched(List claims);
+}
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/listener/SdkStatusListener.java b/app/src/main/java/io/lbry/browser/listener/SdkStatusListener.java
new file mode 100644
index 00000000..866ff382
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/SdkStatusListener.java
@@ -0,0 +1,5 @@
+package io.lbry.browser.listener;
+
+public interface SdkStatusListener {
+ void onSdkReady();
+}
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/SignInListener.java b/app/src/main/java/io/lbry/browser/listener/SignInListener.java
new file mode 100644
index 00000000..866e2774
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/SignInListener.java
@@ -0,0 +1,10 @@
+package io.lbry.browser.listener;
+
+public interface SignInListener {
+ void onEmailAdded(String email);
+ void onEmailEdit();
+ void onEmailVerified();
+ void onPhoneAdded(String countryCode, String phoneNumber);
+ void onPhoneVerified();
+ void onManualVerifyContinue();
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/StoragePermissionListener.java b/app/src/main/java/io/lbry/browser/listener/StoragePermissionListener.java
new file mode 100644
index 00000000..44bcd3dc
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/StoragePermissionListener.java
@@ -0,0 +1,6 @@
+package io.lbry.browser.listener;
+
+public interface StoragePermissionListener {
+ void onStoragePermissionGranted();
+ void onStoragePermissionRefused();
+}
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/listener/WalletBalanceListener.java b/app/src/main/java/io/lbry/browser/listener/WalletBalanceListener.java
new file mode 100644
index 00000000..5e0ede06
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/WalletBalanceListener.java
@@ -0,0 +1,7 @@
+package io.lbry.browser.listener;
+
+import io.lbry.browser.model.WalletBalance;
+
+public interface WalletBalanceListener {
+ void onWalletBalanceUpdated(WalletBalance walletBalance);
+}
diff --git a/app/src/main/java/io/lbry/browser/listener/WalletSyncListener.java b/app/src/main/java/io/lbry/browser/listener/WalletSyncListener.java
new file mode 100644
index 00000000..fcee590b
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/listener/WalletSyncListener.java
@@ -0,0 +1,8 @@
+package io.lbry.browser.listener;
+
+public interface WalletSyncListener {
+ void onWalletSyncProcessing();
+ void onWalletSyncWaitingForInput();
+ void onWalletSyncEnabled();
+ void onWalletSyncFailed(Exception error);
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Claim.java b/app/src/main/java/io/lbry/browser/model/Claim.java
new file mode 100644
index 00000000..31e8b5fb
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Claim.java
@@ -0,0 +1,490 @@
+package io.lbry.browser.model;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import io.lbry.browser.utils.Predefined;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class Claim {
+ public static final String CLAIM_TYPE_CLAIM = "claim";
+ public static final String CLAIM_TYPE_UPDATE = "update";
+ public static final String CLAIM_TYPE_SUPPORT = "support";
+
+ public static final String TYPE_STREAM = "stream";
+ public static final String TYPE_CHANNEL = "channel";
+ public static final String TYPE_REPOST = "repost";
+
+ public static final String STREAM_TYPE_AUDIO = "audio";
+ public static final String STREAM_TYPE_IMAGE = "image";
+ public static final String STREAM_TYPE_VIDEO = "video";
+ public static final String STREAM_TYPE_SOFTWARE = "software";
+
+ public static final String ORDER_BY_EFFECTIVE_AMOUNT = "effective_amount";
+ public static final String ORDER_BY_RELEASE_TIME = "release_time";
+ public static final String ORDER_BY_TRENDING_GROUP = "trending_group";
+ public static final String ORDER_BY_TRENDING_MIXED = "trending_mixed";
+
+ public static final List CLAIM_TYPES = Arrays.asList(TYPE_CHANNEL, TYPE_STREAM);
+ public static final List STREAM_TYPES = Arrays.asList(
+ STREAM_TYPE_AUDIO, STREAM_TYPE_IMAGE, STREAM_TYPE_SOFTWARE, STREAM_TYPE_VIDEO
+ );
+
+ public static final String RELEASE_TIME_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ @EqualsAndHashCode.Include
+ private boolean placeholder;
+ private boolean placeholderAnonymous;
+ private boolean featured;
+ private boolean unresolved; // used for featured
+ private String address;
+ private String amount;
+ private String canonicalUrl;
+ @EqualsAndHashCode.Include
+ private String claimId;
+ private int claimSequence;
+ private String claimOp;
+ private long confirmations;
+ private boolean decodedClaim;
+ private long timestamp;
+ private long height;
+ private boolean isMine;
+ private String name;
+ private String normalizedName;
+ private int nout;
+ private String permanentUrl;
+ private String shortUrl;
+ private String txid;
+ private String type; // claim | update | support
+ private String valueType; // stream | channel | repost
+ private Claim repostedClaim;
+ private Claim signingChannel;
+ private String repostChannelUrl;
+ private boolean isChannelSignatureValid;
+ private GenericMetadata value;
+ private LbryFile file; // associated file if it exists
+
+ // device it was viewed on (for view history)
+ private String device;
+
+ public static Claim claimFromOutput(JSONObject item) {
+ // we only need name, permanent_url, txid and nout
+ Claim claim = new Claim();
+ claim.setClaimId(Helper.getJSONString("claim_id", null, item));
+ claim.setName(Helper.getJSONString("name", null, item));
+ claim.setPermanentUrl(Helper.getJSONString("permanent_url", null, item));
+ claim.setTxid(Helper.getJSONString("txid", null, item));
+ claim.setNout(Helper.getJSONInt("nout", -1, item));
+ return claim;
+ }
+
+ public String getOutpoint() {
+ return String.format("%s:%d", txid, nout);
+ }
+
+ public boolean isFree() {
+ if (!(value instanceof StreamMetadata)) {
+ return true;
+ }
+
+ Fee fee = ((StreamMetadata) value).getFee();
+ return fee == null || Helper.parseDouble(fee.getAmount(), 0) == 0;
+ }
+
+ public BigDecimal getActualCost(double usdRate) {
+ if (!(value instanceof StreamMetadata)) {
+ return new BigDecimal(0);
+ }
+
+ Fee fee = ((StreamMetadata) value).getFee();
+ if (fee != null) {
+ double amount = Helper.parseDouble(fee.getAmount(), 0);
+ if ("usd".equalsIgnoreCase(fee.getCurrency())) {
+ return new BigDecimal(String.valueOf(amount / usdRate));
+ }
+
+ return new BigDecimal(String.valueOf(amount)); // deweys
+ }
+
+ return new BigDecimal(0);
+ }
+
+ public String getMediaType() {
+ if (value instanceof StreamMetadata) {
+ StreamMetadata metadata = (StreamMetadata) value;
+ String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
+ return mediaType;
+ }
+ return null;
+ }
+
+ public boolean isPlayable() {
+ if (value instanceof StreamMetadata) {
+ StreamMetadata metadata = (StreamMetadata) value;
+ String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
+ if (mediaType != null) {
+ return mediaType.startsWith("video") || mediaType.startsWith("audio");
+ }
+ }
+ return false;
+ }
+ public boolean isViewable() {
+ if (value instanceof StreamMetadata) {
+ StreamMetadata metadata = (StreamMetadata) value;
+ String mediaType = metadata.getSource() != null ? metadata.getSource().getMediaType() : null;
+ if (mediaType != null) {
+ return mediaType.startsWith("image") || mediaType.startsWith("text");
+ }
+ }
+ return false;
+ }
+ public boolean isMature() {
+ List tags = getTags();
+ if (tags != null && tags.size() > 0) {
+ for (String tag : tags) {
+ if (Predefined.MATURE_TAGS.contains(tag.toLowerCase())) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public String getThumbnailUrl() {
+ if (value != null && value.getThumbnail() != null) {
+ return value.getThumbnail().getUrl();
+ }
+ return null;
+ }
+
+ public String getCoverUrl() {
+ if (TYPE_CHANNEL.equals(valueType) && value != null && value instanceof ChannelMetadata && ((ChannelMetadata) value).getCover() != null) {
+ return ((ChannelMetadata) value).getCover().getUrl();
+ }
+ return null;
+ }
+
+ public String getFirstCharacter() {
+ if (name != null) {
+ return name.startsWith("@") ? name.substring(1) : name;
+ }
+ return "";
+ }
+
+ public String getFirstTag() {
+ if (value != null && value.tags != null && value.tags.size() > 0) {
+ return value.tags.get(0);
+ }
+ return null;
+ }
+
+ public String getDescription() {
+ 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();
+ }
+ return "Anonymous";
+ }
+
+ public String getPublisherTitle() {
+ if (signingChannel != null) {
+ return Helper.isNullOrEmpty(signingChannel.getTitle()) ? signingChannel.getName() : signingChannel.getTitle();
+ }
+ return "Anonymous";
+ }
+
+
+ public List getTags() {
+ return (value != null && value.getTags() != null) ? new ArrayList<>(value.getTags()) : new ArrayList<>();
+ }
+
+ public List getTagObjects() {
+ List tags = new ArrayList<>();
+ if (value != null && value.getTags() != null) {
+ for (String value : value.getTags()) {
+ tags.add(new Tag(value));
+ }
+ }
+ return tags;
+ }
+
+ public String getTitle() {
+ return (value != null) ? value.getTitle() : null;
+ }
+ public String getTitleOrName() {
+ return (value != null) ? value.getTitle() : getName();
+ }
+
+ public long getDuration() {
+ if (value instanceof StreamMetadata) {
+ StreamMetadata metadata = (StreamMetadata) value;
+ if (STREAM_TYPE_VIDEO.equalsIgnoreCase(metadata.getStreamType()) && metadata.getVideo() != null) {
+ return metadata.getVideo().getDuration();
+ } else if (STREAM_TYPE_AUDIO.equalsIgnoreCase(metadata.getStreamType()) && metadata.getAudio() != null) {
+ return metadata.getAudio().getDuration();
+ }
+ }
+
+ return 0;
+ }
+
+ public static Claim fromViewHistory(ViewHistory viewHistory) {
+ // only for stream claims
+ Claim claim = new Claim();
+ claim.setClaimId(viewHistory.getClaimId());
+ claim.setName(viewHistory.getClaimName());
+ claim.setValueType(TYPE_STREAM);
+ claim.setPermanentUrl(viewHistory.getUri().toString());
+ claim.setDevice(viewHistory.getDevice());
+ claim.setConfirmations(1);
+
+ StreamMetadata value = new StreamMetadata();
+ value.setTitle(viewHistory.getTitle());
+ value.setReleaseTime(viewHistory.getReleaseTime());
+ if (!Helper.isNullOrEmpty(viewHistory.getThumbnailUrl())) {
+ Resource thumbnail = new Resource();
+ thumbnail.setUrl(viewHistory.getThumbnailUrl());
+ value.setThumbnail(thumbnail);
+ }
+ if (viewHistory.getCost() != null && viewHistory.getCost().doubleValue() > 0) {
+ Fee fee = new Fee();
+ fee.setAmount(String.valueOf(viewHistory.getCost().doubleValue()));
+ fee.setCurrency(viewHistory.getCurrency());
+ value.setFee(fee);
+ }
+
+ claim.setValue(value);
+
+ if (!Helper.isNullOrEmpty(viewHistory.getPublisherClaimId())) {
+ Claim signingChannel = new Claim();
+ signingChannel.setClaimId(viewHistory.getPublisherClaimId());
+ signingChannel.setName(viewHistory.getPublisherName());
+ if (!Helper.isNullOrEmpty(viewHistory.getPublisherTitle())) {
+ GenericMetadata channelValue = new GenericMetadata();
+ channelValue.setTitle(viewHistory.getPublisherTitle());
+ signingChannel.setValue(channelValue);
+ }
+ claim.setSigningChannel(signingChannel);
+ }
+
+ return claim;
+ }
+
+ public static Claim fromJSONObject(JSONObject claimObject) {
+ Claim claim = null;
+ String claimJson = claimObject.toString();
+ Type type = new TypeToken(){}.getType();
+ Type streamMetadataType = new TypeToken(){}.getType();
+ Type channelMetadataType = new TypeToken(){}.getType();
+
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ claim = gson.fromJson(claimJson, type);
+
+ try {
+ String valueType = claim.getValueType();
+ // Specific value type parsing
+ if (TYPE_REPOST.equalsIgnoreCase(valueType)) {
+ JSONObject repostedClaimObject = claimObject.getJSONObject("reposted_claim");
+ claim.setRepostedClaim(Claim.fromJSONObject(repostedClaimObject));
+ } else {
+ JSONObject value = claimObject.getJSONObject("value");
+ if (value != null) {
+ String valueJson = value.toString();
+ if (TYPE_STREAM.equalsIgnoreCase(valueType)) {
+ claim.setValue(gson.fromJson(valueJson, streamMetadataType));
+ } else if (TYPE_CHANNEL.equalsIgnoreCase(valueType)) {
+ claim.setValue(gson.fromJson(valueJson, channelMetadataType));
+ }
+ }
+ }
+ } catch (JSONException ex) {
+ // pass
+ }
+
+ return claim;
+ }
+
+ public static Claim fromSearchJSONObject(JSONObject searchResultObject) {
+ Claim claim = new Claim();
+ LbryUri claimUri = new LbryUri();
+ try {
+ claim.setClaimId(searchResultObject.getString("claimId"));
+ claim.setName(searchResultObject.getString("name"));
+ claim.setConfirmations(1);
+
+ if (claim.getName().startsWith("@")) {
+ claimUri.setChannelClaimId(claim.getClaimId());
+ claimUri.setChannelName(claim.getName());
+ claim.setValueType(TYPE_CHANNEL);
+ } else {
+ claimUri.setStreamClaimId(claim.getClaimId());
+ claimUri.setStreamName(claim.getName());
+ claim.setValueType(TYPE_STREAM);
+ }
+
+ int duration = searchResultObject.isNull("duration") ? 0 : searchResultObject.getInt("duration");
+ long feeAmount = searchResultObject.isNull("fee") ? 0 : searchResultObject.getLong("fee");
+ String releaseTimeString = !searchResultObject.isNull("release_time") ? searchResultObject.getString("release_time") : null;
+ long releaseTime = 0;
+ try {
+ releaseTime = Double.valueOf(new SimpleDateFormat(RELEASE_TIME_DATE_FORMAT).parse(releaseTimeString).getTime() / 1000.0).longValue();
+ } catch (ParseException ex) {
+ // pass
+ }
+
+ GenericMetadata metadata = (duration > 0 || releaseTime > 0 || feeAmount > 0) ? new StreamMetadata() : new GenericMetadata();
+ metadata.setTitle(searchResultObject.getString("title"));
+ if (metadata instanceof StreamMetadata) {
+ StreamInfo streamInfo = new StreamInfo();
+ if (duration > 0) {
+ // assume stream type video
+ ((StreamMetadata) metadata).setStreamType(STREAM_TYPE_VIDEO);
+ streamInfo.setDuration(duration);
+ }
+
+ Fee fee = null;
+ if (feeAmount > 0) {
+ fee = new Fee();
+ fee.setAmount(String.valueOf(new BigDecimal(String.valueOf(feeAmount)).divide(new BigDecimal(100000000))));
+ fee.setCurrency("LBC");
+ }
+
+ ((StreamMetadata) metadata).setFee(fee);
+ ((StreamMetadata) metadata).setVideo(streamInfo);
+ ((StreamMetadata) metadata).setReleaseTime(releaseTime);
+ }
+ claim.setValue(metadata);
+
+ if (!searchResultObject.isNull("thumbnail_url")) {
+ Resource thumbnail = new Resource();
+ thumbnail.setUrl(searchResultObject.getString("thumbnail_url"));
+ claim.getValue().setThumbnail(thumbnail);
+ }
+
+ if (!searchResultObject.isNull("channel_claim_id") && !searchResultObject.isNull("channel")) {
+ Claim signingChannel = new Claim();
+ signingChannel.setClaimId(searchResultObject.getString("channel_claim_id"));
+ signingChannel.setName(searchResultObject.getString("channel"));
+ LbryUri channelUri = new LbryUri();
+ channelUri.setChannelClaimId(signingChannel.getClaimId());
+ channelUri.setChannelName(signingChannel.getName());
+ signingChannel.setPermanentUrl(channelUri.toString());
+
+ claim.setSigningChannel(signingChannel);
+ }
+ } catch (JSONException ex) {
+ // pass
+ }
+
+ claim.setPermanentUrl(claimUri.toString());
+
+ return claim;
+ }
+
+ @Data
+ public static class Meta {
+ private long activationHeight;
+ private int claimsInChannel;
+ private int creationHeight;
+ private int creationTimestamp;
+ private String effectiveAmount;
+ private long expirationHeight;
+ private boolean isControlling;
+ private String supportAmount;
+ private int reposted;
+ private double trendingGlobal;
+ private double trendingGroup;
+ private double trendingLocal;
+ private double trendingMixed;
+ }
+
+ @Data
+ public static class GenericMetadata {
+ private String title;
+ private String description;
+ private Resource thumbnail;
+ private List languages;
+ private List tags;
+ private List locations;
+ }
+
+ @Data
+ @EqualsAndHashCode(callSuper = true)
+ public static class ChannelMetadata extends GenericMetadata {
+ private String publicKey;
+ private String publicKeyId;
+ private Resource cover;
+ private String email;
+ private String websiteUrl;
+ private List featured;
+ }
+
+ @Data
+ @EqualsAndHashCode(callSuper = true)
+ public static class StreamMetadata extends GenericMetadata {
+ private String license;
+ private String licenseUrl;
+ private long releaseTime;
+ private String author;
+ private Fee fee;
+ private String streamType; // video | audio | image | software
+ private Source source;
+ private StreamInfo video;
+ private StreamInfo audio;
+ private StreamInfo image;
+ private StreamInfo software;
+
+ @Data
+ public static class Source {
+ private String sdHash;
+ private String mediaType;
+ private String hash;
+ private String name;
+ private long size;
+ }
+ }
+
+ // only support "url" for now
+ @Data
+ public static class Resource {
+ private String url;
+ }
+
+ @Data
+ public static class StreamInfo {
+ private long duration; // video / audio
+ private long height; // video / image
+ private long width; // video / image
+ private String os; // software
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/ClaimCacheKey.java b/app/src/main/java/io/lbry/browser/model/ClaimCacheKey.java
new file mode 100644
index 00000000..6f8c868b
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/ClaimCacheKey.java
@@ -0,0 +1,68 @@
+package io.lbry.browser.model;
+
+import androidx.annotation.Nullable;
+
+import io.lbry.browser.utils.Helper;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * Class to represent a key to check equality with another object
+ */
+@ToString
+public class ClaimCacheKey {
+ @Getter
+ @Setter
+ private String claimId;
+ @Getter
+ @Setter
+ private String url;
+
+ public static ClaimCacheKey fromClaimShortUrl(Claim claim) {
+ ClaimCacheKey key = new ClaimCacheKey();
+ key.setUrl(claim.getShortUrl());
+ return key;
+ }
+
+ public static ClaimCacheKey fromClaimPermanentUrl(Claim claim) {
+ ClaimCacheKey key = new ClaimCacheKey();
+ key.setUrl(claim.getPermanentUrl());
+ return key;
+ }
+
+ public static ClaimCacheKey fromClaim(Claim claim) {
+ ClaimCacheKey key = new ClaimCacheKey();
+ key.setClaimId(claim.getClaimId());
+ key.setUrl(!Helper.isNullOrEmpty(claim.getShortUrl()) ? claim.getShortUrl() : claim.getPermanentUrl());
+ return key;
+ }
+
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null || !(obj instanceof ClaimCacheKey)) {
+ return false;
+ }
+ ClaimCacheKey key = (ClaimCacheKey) obj;
+ if (!Helper.isNullOrEmpty(claimId) && !Helper.isNullOrEmpty(key.getClaimId())) {
+ return claimId.equalsIgnoreCase(key.getClaimId());
+ }
+ if (!Helper.isNullOrEmpty(url) && !Helper.isNullOrEmpty(key.getUrl())) {
+ return url.equalsIgnoreCase(key.getUrl());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (!Helper.isNullOrEmpty(url)) {
+ return url.hashCode();
+ }
+ if (!Helper.isNullOrEmpty(claimId)) {
+ return claimId.hashCode();
+ }
+
+ return super.hashCode();
+ }
+}
+
diff --git a/app/src/main/java/io/lbry/browser/model/ClaimSearchCacheValue.java b/app/src/main/java/io/lbry/browser/model/ClaimSearchCacheValue.java
new file mode 100644
index 00000000..60260add
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/ClaimSearchCacheValue.java
@@ -0,0 +1,26 @@
+package io.lbry.browser.model;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import lombok.Getter;
+import lombok.Setter;
+
+public class ClaimSearchCacheValue {
+ @Getter
+ @Setter
+ private List claims;
+ @Getter
+ @Setter
+ private long timestamp;
+
+ public ClaimSearchCacheValue(List claims, long timestamp) {
+ this.claims = new ArrayList<>(claims);
+ this.timestamp = timestamp;
+ }
+
+ public boolean isExpired(long ttl) {
+ return System.currentTimeMillis() - timestamp > ttl;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/EditorsChoiceItem.java b/app/src/main/java/io/lbry/browser/model/EditorsChoiceItem.java
new file mode 100644
index 00000000..49565d7a
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/EditorsChoiceItem.java
@@ -0,0 +1,23 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class EditorsChoiceItem {
+ private boolean header;
+ private String title;
+ private String parent;
+ private String description;
+ private String thumbnailUrl;
+ private String permanentUrl;
+
+ public static EditorsChoiceItem fromClaim(Claim claim) {
+ EditorsChoiceItem item = new EditorsChoiceItem();
+ item.setTitle(claim.getTitle());
+ item.setDescription(claim.getDescription());
+ item.setThumbnailUrl(claim.getThumbnailUrl());
+ item.setPermanentUrl(claim.getPermanentUrl());
+
+ return item;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Fee.java b/app/src/main/java/io/lbry/browser/model/Fee.java
new file mode 100644
index 00000000..12fbc61d
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Fee.java
@@ -0,0 +1,10 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class Fee {
+ private String amount;
+ private String currency;
+ private String address;
+}
diff --git a/app/src/main/java/io/lbry/browser/model/GalleryItem.java b/app/src/main/java/io/lbry/browser/model/GalleryItem.java
new file mode 100644
index 00000000..43feee58
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/GalleryItem.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class GalleryItem {
+ private String id;
+ private String name;
+ private String filePath;
+ private String type;
+ private String thumbnailPath;
+ private long duration;
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Language.java b/app/src/main/java/io/lbry/browser/model/Language.java
new file mode 100644
index 00000000..e3f4567b
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Language.java
@@ -0,0 +1,16 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class Language {
+ private String code;
+ private String name;
+ private int stringResourceId;
+
+ public Language(String code, String name, int stringResourceId) {
+ this.code = code;
+ this.name = name;
+ this.stringResourceId = stringResourceId;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/LbryFile.java b/app/src/main/java/io/lbry/browser/model/LbryFile.java
new file mode 100644
index 00000000..ed9cb1fb
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/LbryFile.java
@@ -0,0 +1,92 @@
+package io.lbry.browser.model;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+
+import io.lbry.browser.utils.LbryUri;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+public class LbryFile {
+ private Claim.StreamMetadata metadata;
+ private long addedOn;
+ private int blobsCompleted;
+ private int blobsInStream;
+ private int blobsRemaining;
+ private String channelClaimId;
+ private String channelName;
+ @EqualsAndHashCode.Include
+ private String claimId;
+ private String claimName;
+ private boolean completed;
+ private String downloadDirectory;
+ private String downloadPath;
+ private String fileName;
+ private String key;
+ private String mimeType;
+ private int nout;
+ private String outpoint;
+ private int pointsPaid;
+ private String protobuf;
+ private String sdHash;
+ private String status;
+ private boolean stopped;
+ private String streamHash;
+ private String streamName;
+ private String streamingUrl;
+ private String suggestedFileName;
+ private long timestamp;
+ private long totalBytes;
+ private long totalBytesLowerBound;
+ private String txid;
+ private long writtenBytes;
+
+ private Claim generatedClaim;
+
+ public Claim getClaim() {
+ if (generatedClaim != null) {
+ return generatedClaim;
+ }
+
+ generatedClaim = new Claim();
+ generatedClaim.setValueType(Claim.TYPE_STREAM);
+ generatedClaim.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString());
+ generatedClaim.setClaimId(claimId);
+ generatedClaim.setName(claimName);
+ generatedClaim.setValue(metadata);
+ generatedClaim.setConfirmations(1);
+ generatedClaim.setTxid(txid);
+ generatedClaim.setNout(nout);
+ generatedClaim.setFile(this);
+
+ if (channelClaimId != null) {
+ Claim signingChannel = new Claim();
+ signingChannel.setClaimId(channelClaimId);
+ signingChannel.setName(channelName);
+ signingChannel.setPermanentUrl(LbryUri.tryParse(String.format("%s#%s", claimName, claimId)).toString());
+ generatedClaim.setSigningChannel(signingChannel);
+ }
+
+ return generatedClaim;
+ }
+
+ public static LbryFile fromJSONObject(JSONObject fileObject) {
+ String fileJson = fileObject.toString();
+ Type type = new TypeToken(){}.getType();
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ LbryFile file = gson.fromJson(fileJson, type);
+
+ if (file.getMetadata() != null && file.getMetadata().getReleaseTime() == 0) {
+ file.getMetadata().setReleaseTime(file.getTimestamp());
+ }
+ return file;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/License.java b/app/src/main/java/io/lbry/browser/model/License.java
new file mode 100644
index 00000000..b278eafa
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/License.java
@@ -0,0 +1,20 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class License {
+ private String name;
+ private String url;
+ private int stringResourceId;
+
+ public License(String name, int stringResourceId) {
+ this.name = name;
+ this.stringResourceId = stringResourceId;
+ }
+ public License(String name, String url, int stringResourceId) {
+ this.name = name;
+ this.url = url;
+ this.stringResourceId = stringResourceId;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Location.java b/app/src/main/java/io/lbry/browser/model/Location.java
new file mode 100644
index 00000000..e1673424
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Location.java
@@ -0,0 +1,13 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class Location {
+ private double latitude;
+ private double longitude;
+ private String country;
+ private String state;
+ private String city;
+ private String code;
+}
diff --git a/app/src/main/java/io/lbry/browser/model/NavMenuItem.java b/app/src/main/java/io/lbry/browser/model/NavMenuItem.java
new file mode 100644
index 00000000..53468b09
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/NavMenuItem.java
@@ -0,0 +1,68 @@
+package io.lbry.browser.model;
+
+import android.content.Context;
+
+import androidx.core.content.res.ResourcesCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.Data;
+
+@Data
+public class NavMenuItem {
+ public static final int ID_GROUP_FIND_CONTENT = 100;
+ public static final int ID_GROUP_YOUR_CONTENT = 200;
+ public static final int ID_GROUP_WALLET = 300;
+ public static final int ID_GROUP_OTHER = 400;
+
+ // Find Content
+ public static final int ID_ITEM_FOLLOWING = 101;
+ public static final int ID_ITEM_EDITORS_CHOICE = 102;
+ public static final int ID_ITEM_ALL_CONTENT = 103;
+
+ // Your Content
+ public static final int ID_ITEM_CHANNELS = 201;
+ public static final int ID_ITEM_LIBRARY = 202;
+ public static final int ID_ITEM_PUBLISHES = 203;
+ public static final int ID_ITEM_NEW_PUBLISH = 204;
+
+ // Wallet
+ public static final int ID_ITEM_WALLET = 301;
+ public static final int ID_ITEM_REWARDS = 302;
+ public static final int ID_ITEM_INVITES = 303;
+
+ // Other
+ public static final int ID_ITEM_SETTINGS = 401;
+ public static final int ID_ITEM_ABOUT = 402;
+
+ private Context context;
+ private int id;
+ private boolean group;
+ private int icon;
+ private String title;
+ private String extraLabel;
+ private String name; // same as title, but only as en lang for events
+ private List items;
+
+ public NavMenuItem(int id, int titleResourceId, boolean group, Context context) {
+ this.context = context;
+ this.id = id;
+ this.group = group;
+
+ if (titleResourceId > 0) {
+ this.title = context.getString(titleResourceId);
+ }
+ if (group) {
+ this.items = new ArrayList<>();
+ }
+ }
+
+ public NavMenuItem(int id, int iconStringId, int titleResourceId, String name, Context context) {
+ this.context = context;
+ this.id = id;
+ this.icon = iconStringId;
+ this.title = context.getString(titleResourceId);
+ this.name = name;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Tag.java b/app/src/main/java/io/lbry/browser/model/Tag.java
new file mode 100644
index 00000000..6f898bea
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Tag.java
@@ -0,0 +1,44 @@
+package io.lbry.browser.model;
+
+import java.util.Comparator;
+
+import io.lbry.browser.utils.Predefined;
+import lombok.Getter;
+import lombok.Setter;
+
+public class Tag implements Comparator {
+ @Getter
+ @Setter
+ private String name;
+ @Getter
+ @Setter
+ private boolean followed;
+
+ public Tag() {
+
+ }
+ public Tag(String name) {
+ this.name = name;
+ }
+
+ public String getLowercaseName() {
+ return name.toLowerCase();
+ }
+
+ public boolean isMature() {
+ return Predefined.MATURE_TAGS.contains(name.toLowerCase());
+ }
+
+ public String toString() {
+ return getLowercaseName();
+ }
+ public boolean equals(Object o) {
+ return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name);
+ }
+ public int hashCode() {
+ return name.toLowerCase().hashCode();
+ }
+ public int compare(Tag a, Tag b) {
+ return a.getLowercaseName().compareToIgnoreCase(b.getLowercaseName());
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/Transaction.java b/app/src/main/java/io/lbry/browser/model/Transaction.java
new file mode 100644
index 00000000..e48f3913
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/Transaction.java
@@ -0,0 +1,133 @@
+package io.lbry.browser.model;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import io.lbry.browser.R;
+import io.lbry.browser.exceptions.LbryUriException;
+import io.lbry.browser.utils.Helper;
+import io.lbry.browser.utils.LbryUri;
+import lombok.Data;
+
+@Data
+public class Transaction {
+ private int confirmations;
+ private Date txDate;
+ private String date;
+ private String claim;
+ private String claimId;
+ private String txid;
+ private BigDecimal value;
+ private BigDecimal fee;
+ private long timestamp;
+ private int descriptionStringId;
+ private TransactionInfo abandonInfo;
+ private TransactionInfo claimInfo;
+ private TransactionInfo supportInfo;
+ private TransactionInfo updateInfo;
+
+ public LbryUri getClaimUrl() {
+ if (!Helper.isNullOrEmpty(claim) && !Helper.isNullOrEmpty(claimId)) {
+ try {
+ return LbryUri.parse(LbryUri.normalize(String.format("%s#%s", claim, claimId)));
+ } catch (LbryUriException ex) {
+ // pass
+ }
+ }
+ return null;
+ }
+
+ public static Transaction fromJSONObject(JSONObject jsonObject) {
+ Transaction transaction = new Transaction();
+ transaction.setConfirmations(Helper.getJSONInt("confirmations", -1, jsonObject));
+ transaction.setDate(Helper.getJSONString("date", null, jsonObject));
+ transaction.setTxid(Helper.getJSONString("txid", null, jsonObject));
+ transaction.setValue(new BigDecimal(Helper.getJSONString("value", "0", jsonObject)));
+ transaction.setFee(new BigDecimal(Helper.getJSONString("fee", "0", jsonObject)));
+ transaction.setTimestamp(Helper.getJSONLong("timestamp", 0, jsonObject) * 1000);
+ transaction.setTxDate(new Date(transaction.getTimestamp()));
+
+ int descStringId = -1;
+ TransactionInfo info = null;
+ try {
+ if (jsonObject.has("abandon_info")) {
+ JSONArray array = jsonObject.getJSONArray("abandon_info");
+ if (array.length() > 0) {
+ info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
+ descStringId = R.string.abandon;
+ transaction.setAbandonInfo(info);
+ }
+ }
+ if (info == null && jsonObject.has("claim_info")) {
+ JSONArray array = jsonObject.getJSONArray("claim_info");
+ if (array.length() > 0) {
+ info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
+ descStringId = info.getClaimName().startsWith("@") ? R.string.channel : R.string.publish;
+ transaction.setClaimInfo(info);
+ }
+ }
+ if (info == null && jsonObject.has("support_info")) {
+ JSONArray array = jsonObject.getJSONArray("support_info");
+ if (array.length() > 0) {
+ info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
+ descStringId = info.isTip() ? R.string.tip : R.string.support;
+ transaction.setSupportInfo(info);
+ }
+ }
+ if (info == null && jsonObject.has("update_info")) {
+ JSONArray array = jsonObject.getJSONArray("update_info");
+ if (array.length() > 0) {
+ info = TransactionInfo.fromJSONObject(array.getJSONObject(0));
+ descStringId = info.getClaimName().startsWith("@") ? R.string.channel_update : R.string.publish_update;
+ transaction.setUpdateInfo(info);
+ }
+ }
+ if (info != null) {
+ transaction.setClaim(info.getClaimName());
+ transaction.setClaimId(info.getClaimId());
+ }
+ } catch (JSONException ex) {
+ // pass
+ }
+
+ if (transaction.getValue().doubleValue() == 0 && info != null && info.getBalanceDelta().doubleValue() != 0) {
+ transaction.setValue(info.getBalanceDelta());
+ }
+
+ if (descStringId == -1) {
+ descStringId = transaction.getValue().signum() == -1 || transaction.getFee().signum() == -1 ? R.string.spend : R.string.receive;
+ }
+ transaction.setDescriptionStringId(descStringId);
+
+ return transaction;
+ }
+
+ @Data
+ public static class TransactionInfo {
+ private String address;
+ private BigDecimal amount;
+ private BigDecimal balanceDelta;
+ private String claimId;
+ private String claimName;
+ private boolean isTip;
+ private int nout;
+
+ public static TransactionInfo fromJSONObject(JSONObject jsonObject) {
+ TransactionInfo info = new TransactionInfo();
+
+ info.setAddress(Helper.getJSONString("address", null, jsonObject));
+ info.setAmount(new BigDecimal(Helper.getJSONString("amount", "0", jsonObject)));
+ info.setBalanceDelta(new BigDecimal(Helper.getJSONString("balance_delta", "0", jsonObject)));
+ info.setClaimId(Helper.getJSONString("claim_id", null, jsonObject));
+ info.setClaimName(Helper.getJSONString("claim_name", null, jsonObject));
+ info.setTip(Helper.getJSONBoolean("is_tip", false, jsonObject));
+ info.setNout(Helper.getJSONInt("nout", -1, jsonObject));
+
+ return info;
+ }
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/UrlSuggestion.java b/app/src/main/java/io/lbry/browser/model/UrlSuggestion.java
new file mode 100644
index 00000000..b5ed912a
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/UrlSuggestion.java
@@ -0,0 +1,55 @@
+package io.lbry.browser.model;
+
+import io.lbry.browser.utils.LbryUri;
+import lombok.Data;
+
+@Data
+public class UrlSuggestion {
+ public static final int TYPE_CHANNEL = 1;
+ public static final int TYPE_FILE = 2;
+ public static final int TYPE_SEARCH = 3;
+ public static final int TYPE_TAG = 4;
+
+ private int type;
+ private String text;
+ private LbryUri uri;
+ private Claim claim; // associated claim if resolved
+ private boolean titleTextOnly;
+ private boolean titleUrlOnly;
+ private boolean useTextAsDescription;
+
+ public UrlSuggestion() {
+
+ }
+ public UrlSuggestion(int type, String text) {
+ this.type = type;
+ this.text = text;
+ }
+ public UrlSuggestion(int type, String text, LbryUri uri) {
+ this(type, text);
+ this.uri = uri;
+ }
+ public UrlSuggestion(int type, String text, LbryUri uri, boolean titleTextOnly) {
+ this(type, text, uri);
+ this.titleTextOnly = titleTextOnly;
+ }
+
+ public String getTitle() {
+ if (titleUrlOnly && (type == TYPE_CHANNEL || type == TYPE_FILE)) {
+ return uri.toString();
+ }
+
+ if (!titleTextOnly) {
+ switch (type) {
+ case TYPE_CHANNEL:
+ return String.format("%s - %s", text.startsWith("@") ? text.substring(1) : text, uri.toVanityString());
+ case TYPE_FILE:
+ return String.format("%s - %s", text, uri.toVanityString());
+ case TYPE_TAG:
+ return String.format("%s - #%s", text, text);
+ }
+ }
+
+ return text;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/ViewHistory.java b/app/src/main/java/io/lbry/browser/model/ViewHistory.java
new file mode 100644
index 00000000..ea668615
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/ViewHistory.java
@@ -0,0 +1,64 @@
+package io.lbry.browser.model;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+import io.lbry.browser.exceptions.LbryUriException;
+import io.lbry.browser.utils.LbryUri;
+import io.lbry.browser.utils.Lbryio;
+import lombok.Data;
+
+@Data
+public class ViewHistory {
+ private LbryUri uri;
+ private String claimId;
+ private String claimName;
+ private BigDecimal cost;
+ private String currency;
+ private String title;
+ private String publisherClaimId;
+ private String publisherName;
+ private String publisherTitle;
+ private String thumbnailUrl;
+ private String device;
+ private long releaseTime;
+ private Date timestamp;
+
+ public static ViewHistory fromClaimWithUrlAndDeviceName(Claim claim, String url, String deviceName) {
+ ViewHistory history = new ViewHistory();
+ LbryUri uri = LbryUri.tryParse(url);
+ if (uri == null) {
+ uri = LbryUri.tryParse(claim.getPermanentUrl());
+ }
+ history.setUri(uri);
+ history.setClaimId(claim.getClaimId());
+ history.setClaimName(claim.getName());
+ history.setTitle(claim.getTitle());
+ history.setThumbnailUrl(claim.getThumbnailUrl());
+
+ Claim.GenericMetadata metadata = claim.getValue();
+ if (metadata instanceof Claim.StreamMetadata) {
+ Claim.StreamMetadata value = (Claim.StreamMetadata) metadata;
+ history.setReleaseTime(value.getReleaseTime());
+ if (value.getFee() != null) {
+ Fee fee = value.getFee();
+ history.setCost(new BigDecimal(fee.getAmount()));
+ history.setCurrency(fee.getCurrency());
+ }
+ }
+ if (history.getReleaseTime() == 0) {
+ history.setReleaseTime(claim.getTimestamp());
+ }
+
+ Claim signingChannel = claim.getSigningChannel();
+ if (signingChannel != null) {
+ history.setPublisherClaimId(signingChannel.getClaimId());
+ history.setPublisherName(signingChannel.getName());
+ history.setPublisherTitle(signingChannel.getTitle());
+ }
+
+ history.setDevice(deviceName);
+
+ return history;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/WalletBalance.java b/app/src/main/java/io/lbry/browser/model/WalletBalance.java
new file mode 100644
index 00000000..993a0c31
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/WalletBalance.java
@@ -0,0 +1,24 @@
+package io.lbry.browser.model;
+
+import java.math.BigDecimal;
+
+import lombok.Data;
+
+@Data
+public class WalletBalance {
+ private BigDecimal available;
+ private BigDecimal reserved;
+ private BigDecimal claims;
+ private BigDecimal supports;
+ private BigDecimal tips;
+ private BigDecimal total;
+
+ public WalletBalance() {
+ available = new BigDecimal(0);
+ reserved = new BigDecimal(0);
+ claims = new BigDecimal(0);
+ supports = new BigDecimal(0);
+ tips = new BigDecimal(0);
+ total = new BigDecimal(0);
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/WalletSync.java b/app/src/main/java/io/lbry/browser/model/WalletSync.java
new file mode 100644
index 00000000..05857ff0
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/WalletSync.java
@@ -0,0 +1,20 @@
+package io.lbry.browser.model;
+
+import lombok.Data;
+
+@Data
+public class WalletSync {
+ private String hash;
+ private String data;
+ private boolean changed;
+
+ public WalletSync(String hash, String data) {
+ this.hash = hash;
+ this.data = data;
+ }
+
+ public WalletSync(String hash, String data, boolean changed) {
+ this(hash, data);
+ this.changed = changed;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/Invitee.java b/app/src/main/java/io/lbry/browser/model/lbryinc/Invitee.java
new file mode 100644
index 00000000..2d84072d
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/lbryinc/Invitee.java
@@ -0,0 +1,11 @@
+package io.lbry.browser.model.lbryinc;
+
+import lombok.Data;
+
+@Data
+public class Invitee {
+ private boolean header;
+ private String email;
+ private boolean inviteRewardClaimed;
+ private boolean inviteRewardClaimable;
+}
diff --git a/app/src/main/java/io/lbry/browser/model/lbryinc/Reward.java b/app/src/main/java/io/lbry/browser/model/lbryinc/Reward.java
new file mode 100644
index 00000000..67f46db6
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/lbryinc/Reward.java
@@ -0,0 +1,69 @@
+package io.lbry.browser.model.lbryinc;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import org.json.JSONObject;
+
+import java.lang.reflect.Type;
+
+import io.lbry.browser.model.Claim;
+import io.lbry.browser.utils.Helper;
+import lombok.Data;
+
+@Data
+public class Reward {
+ public static final String TYPE_NEW_DEVELOPER = "new_developer";
+ public static final String TYPE_NEW_USER = "new_user";
+ public static final String TYPE_CONFIRM_EMAIL = "email_provided";
+ public static final String TYPE_FIRST_CHANNEL = "new_channel";
+ public static final String TYPE_FIRST_STREAM = "first_stream";
+ public static final String TYPE_MANY_DOWNLOADS = "many_downloads";
+ public static final String TYPE_FIRST_PUBLISH = "first_publish";
+ public static final String TYPE_REFERRAL = "referrer";
+ public static final String TYPE_REFEREE = "referee";
+ public static final String TYPE_REWARD_CODE = "reward_code";
+ public static final String TYPE_SUBSCRIPTION = "subscription";
+ public static final String YOUTUBE_CREATOR = "youtube_creator";
+ public static final String TYPE_DAILY_VIEW = "daily_view";
+ public static final String TYPE_NEW_ANDROID = "new_android";
+
+ private boolean custom;
+ private long id;
+ private String rewardType;
+ private double rewardAmount;
+ private String transactionId;
+ private String createdAt;
+ private String updatedAt;
+ private String rewardTitle;
+ private String rewardDescription;
+ private String rewardNotification;
+ private String rewardRange;
+
+ public String getDisplayAmount() {
+ if (shouldDisplayRange()) {
+ return rewardRange.split("-")[1];
+ }
+ if (rewardAmount > 0) {
+ return String.valueOf(rewardAmount);
+ }
+ return "?";
+ }
+
+ public boolean isClaimed() {
+ return !Helper.isNullOrEmpty(transactionId);
+ }
+
+ public boolean shouldDisplayRange() {
+ return (!isClaimed() && !Helper.isNullOrEmpty(rewardRange) && rewardRange.indexOf('-') > -1);
+ }
+
+ public static Reward fromJSONObject(JSONObject rewardObject) {
+ String rewardJson = rewardObject.toString();
+ Type type = new TypeToken(){}.getType();
+ Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ return gson.fromJson(rewardJson, type);
+ }
+}
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
new file mode 100644
index 00000000..30f2412a
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/lbryinc/Subscription.java
@@ -0,0 +1,36 @@
+package io.lbry.browser.model.lbryinc;
+
+import io.lbry.browser.model.Claim;
+import lombok.Getter;
+import lombok.Setter;
+
+public class Subscription {
+ @Getter
+ @Setter
+ private String channelName;
+ @Getter
+ @Setter
+ private String url;
+
+ public Subscription() {
+
+ }
+ public Subscription(String channelName, String url) {
+ this.channelName = channelName;
+ this.url = url;
+ }
+
+ public static Subscription fromClaim(Claim claim) {
+ return new Subscription(claim.getName(), claim.getPermanentUrl());
+ }
+ public String toString() {
+ return url;
+ }
+
+ public boolean equals(Object o) {
+ 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/model/lbryinc/User.java b/app/src/main/java/io/lbry/browser/model/lbryinc/User.java
new file mode 100644
index 00000000..15c5bec2
--- /dev/null
+++ b/app/src/main/java/io/lbry/browser/model/lbryinc/User.java
@@ -0,0 +1,42 @@
+package io.lbry.browser.model.lbryinc;
+
+import java.util.List;
+
+import lombok.Data;
+
+@Data
+public class User {
+ private String createdAt;
+ private String familyName;
+ private String givenName;
+ private List groups;
+ private boolean hasVerifiedEmail;
+ private long id;
+ private boolean inviteRewardClaimed;
+ private String invitedAt;
+ private long inivtedById;
+ private int invitesRemaining;
+ private boolean isEmailEnabled;
+ private boolean isIdentityVerified;
+ private boolean isRewardApproved;
+ private String language;
+ private long manualApprovalUserId;
+ private String primaryEmail;
+ private String rewardStatusChangeTrigger;
+ private String updatedAt;
+ private List youtubeChannels;
+ private List deviceTypes;
+
+ @Data
+ public static class YoutubeChannel {
+ String ytChannelName;
+ String lbryChannelName;
+ String channelClaimId;
+ String syncStatus;
+ String statusToken;
+ boolean transferable;
+ String transferState;
+ List publishToAddress;
+ String publicKey;
+ }
+}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/BackgroundMediaModule.java b/app/src/main/java/io/lbry/browser/reactmodules/BackgroundMediaModule.java
deleted file mode 100644
index 38a67202..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/BackgroundMediaModule.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.app.Activity;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.net.Uri;
-import android.os.Build;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
-
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-
-import io.lbry.browser.MainActivity;
-import io.lbry.browser.R;
-import io.lbry.lbrysdk.LbrynetService;
-
-public class BackgroundMediaModule extends ReactContextBaseJavaModule {
-
- public static final int NOTIFICATION_ID = 30;
-
- public static final String ACTION_PLAY = "io.lbry.browser.ACTION_MEDIA_PLAY";
-
- public static final String ACTION_PAUSE = "io.lbry.browser.ACTION_MEDIA_PAUSE";
-
- public static final String ACTION_STOP = "io.lbry.browser.ACTION_MEDIA_STOP";
-
- private Context context;
-
- public BackgroundMediaModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- }
-
- @Override
- public String getName() {
- return "BackgroundMedia";
- }
-
- @ReactMethod
- public void showPlaybackNotification(String title, String publisher, String uri, boolean paused) {
- Intent contextIntent = new Intent(context, MainActivity.class);
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contextIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- Intent playIntent = new Intent();
- playIntent.setAction(ACTION_PLAY);
- PendingIntent playPendingIntent = PendingIntent.getBroadcast(context, 0, playIntent, 0);
-
- Intent pauseIntent = new Intent();
- pauseIntent.setAction(ACTION_PAUSE);
- PendingIntent pausePendingIntent = PendingIntent.getBroadcast(context, 0, pauseIntent, 0);
-
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- NotificationCompat.Builder builder = new NotificationCompat.Builder(context, LbrynetService.NOTIFICATION_CHANNEL_ID);
- builder.setColor(ContextCompat.getColor(context, R.color.lbryGreen))
- .setContentIntent(pendingIntent)
- .setContentTitle(title)
- .setContentText(publisher)
- .setGroup(LbrynetService.GROUP_SERVICE)
- .setOngoing(!paused)
- .setSmallIcon(paused ? android.R.drawable.ic_media_pause : android.R.drawable.ic_media_play)
- .setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
- .setShowActionsInCompactView(0))
- .addAction(paused ? android.R.drawable.ic_media_play : android.R.drawable.ic_media_pause,
- paused ? "Play" : "Pause",
- paused ? playPendingIntent : pausePendingIntent)
- .build();
-
- notificationManager.notify(NOTIFICATION_ID, builder.build());
- }
-
- @ReactMethod
- public void hidePlaybackNotification() {
- NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- notificationManager.cancel(NOTIFICATION_ID);
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/DaemonServiceControlModule.java b/app/src/main/java/io/lbry/browser/reactmodules/DaemonServiceControlModule.java
deleted file mode 100644
index 715414d2..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/DaemonServiceControlModule.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.app.Activity;
-import android.app.NotificationChannel;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-import android.content.SharedPreferences;
-
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-
-import io.lbry.browser.MainActivity;
-import io.lbry.lbrysdk.LbrynetService;
-import io.lbry.lbrysdk.ServiceHelper;
-
-public class DaemonServiceControlModule extends ReactContextBaseJavaModule {
-
- private Context context;
-
- public DaemonServiceControlModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- }
-
- @Override
- public String getName() {
- return "DaemonServiceControl";
- }
-
- @ReactMethod
- public void startService() {
- ServiceHelper.start(context, "", LbrynetService.class, "lbrynetservice");
- }
-
- @ReactMethod
- public void stopService() {
- ServiceHelper.stop(context, LbrynetService.class);
- }
-
- @ReactMethod
- public void setKeepDaemonRunning(boolean value) {
- if (context != null) {
- SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- SharedPreferences.Editor editor = sp.edit();
- editor.putBoolean(MainActivity.SETTING_KEEP_DAEMON_RUNNING, value);
- editor.commit();
- }
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/FirebaseModule.java b/app/src/main/java/io/lbry/browser/reactmodules/FirebaseModule.java
deleted file mode 100644
index 74dfe9cc..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/FirebaseModule.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.app.Activity;
-import android.content.Context;
-import android.os.Bundle;
-import android.widget.Toast;
-
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.ReadableMap;
-import com.google.firebase.analytics.FirebaseAnalytics;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.iid.InstanceIdResult;
-
-import io.lbry.browser.BuildConfig;
-import io.lbry.browser.MainActivity;
-import io.lbry.lbrysdk.Utils;
-
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-import org.json.JSONObject;
-import org.json.JSONException;
-
-public class FirebaseModule extends ReactContextBaseJavaModule {
-
- private Context context;
-
- private FirebaseAnalytics firebaseAnalytics;
-
- public FirebaseModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- this.firebaseAnalytics = FirebaseAnalytics.getInstance(context);
- }
-
- @Override
- public String getName() {
- return "Firebase";
- }
-
- @ReactMethod
- public void setCurrentScreen(String name, final Promise promise) {
- final Activity activity = getCurrentActivity();
- if (activity != null && firebaseAnalytics != null) {
- activity.runOnUiThread(new Runnable() {
- public void run() {
- firebaseAnalytics.setCurrentScreen(activity, name, Utils.capitalizeAndStrip(name));
- }
- });
- }
- promise.resolve(true);
- }
-
- @ReactMethod
- public void track(String name, ReadableMap payload) {
- Bundle bundle = new Bundle();
- if (payload != null) {
- HashMap payloadMap = payload.toHashMap();
- for (Map.Entry entry : payloadMap.entrySet()) {
- Object value = entry.getValue();
- if (value != null) {
- bundle.putString(entry.getKey(), entry.getValue().toString());
- }
- }
- }
-
- if (firebaseAnalytics != null) {
- firebaseAnalytics.logEvent(name, bundle);
- }
- }
-
- @ReactMethod
- public void logException(boolean fatal, String message, String error) {
- Bundle bundle = new Bundle();
- bundle.putString("exception_message", message);
- bundle.putString("exception_error", error);
- if (firebaseAnalytics != null) {
- firebaseAnalytics.logEvent(fatal ? "reactjs_exception" : "reactjs_warning", bundle);
- }
-
- if (fatal) {
- Toast.makeText(context,
- "An application error occurred which has been automatically logged. " +
- "If you keep seeing this message, please provide feedback to the LBRY " +
- "team by emailing hello@lbry.com.",
- Toast.LENGTH_LONG).show();
- }
- }
-
- @ReactMethod
- public void getMessagingToken(final Promise promise) {
- FirebaseInstanceId.getInstance().getInstanceId()
- .addOnCompleteListener(new OnCompleteListener() {
- @Override
- public void onComplete(Task task) {
- if (!task.isSuccessful()) {
- promise.reject("Firebase getInstanceId call failed");
- return;
- }
-
- // Get new Instance ID token
- String token = task.getResult().getToken();
- promise.resolve(token);
- }
- });
- }
-
- @ReactMethod
- public void logLaunchTiming() {
- Date end = new Date();
- MainActivity.LaunchTiming currentTiming = MainActivity.CurrentLaunchTiming;
- if (currentTiming == null) {
- // no start timing data, so skip this
- return;
- }
-
- long totalTimeMs = end.getTime() - currentTiming.getStart().getTime();
- String eventName = currentTiming.isColdStart() ? "app_cold_start" : "app_warm_start";
- Bundle bundle = new Bundle();
- bundle.putLong("total_ms", totalTimeMs);
- bundle.putLong("total_seconds", new Double(Math.ceil(totalTimeMs / 1000.0)).longValue());
- if (firebaseAnalytics != null) {
- firebaseAnalytics.logEvent(eventName, bundle);
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/FirstRunModule.java b/app/src/main/java/io/lbry/browser/reactmodules/FirstRunModule.java
deleted file mode 100644
index 73212018..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/FirstRunModule.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-
-import com.google.firebase.analytics.FirebaseAnalytics;
-
-import io.lbry.browser.MainActivity;
-
-public class FirstRunModule extends ReactContextBaseJavaModule {
- private Context context;
-
- private SharedPreferences sp;
-
- public FirstRunModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- this.sp = reactContext.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
- }
-
- @Override
- public String getName() {
- return "FirstRun";
- }
-
- @ReactMethod
- public void isFirstRun(final Promise promise) {
- // If firstRun flag does not exist, default to true
- boolean firstRun = sp.getBoolean("firstRun", true);
- promise.resolve(firstRun);
- }
-
- @ReactMethod
- public void firstRunCompleted() {
- SharedPreferences.Editor editor = sp.edit();
- editor.putBoolean("firstRun", false);
- editor.commit();
-
- FirebaseAnalytics firebase = FirebaseAnalytics.getInstance(context);
- if (firebase != null) {
- Bundle bundle = new Bundle();
- firebase.logEvent("first_run_completed", bundle);
- }
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java b/app/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java
deleted file mode 100644
index fd1e25f2..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/GalleryModule.java
+++ /dev/null
@@ -1,313 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.content.Context;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.Manifest;
-import android.media.ThumbnailUtils;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.MediaStore;
-
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.WritableArray;
-import com.facebook.react.bridge.WritableMap;
-import com.facebook.react.modules.core.DeviceEventManagerModule;
-
-import io.lbry.browser.MainActivity;
-import io.lbry.lbrysdk.Utils;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.List;
-import java.util.ArrayList;
-
-public class GalleryModule extends ReactContextBaseJavaModule {
- private Context context;
-
- public GalleryModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- }
-
- @Override
- public String getName() {
- return "Gallery";
- }
-
- @ReactMethod
- public void getVideos(Promise promise) {
- WritableArray items = Arguments.createArray();
- List videos = loadVideos();
- for (int i = 0; i < videos.size(); i++) {
- items.pushMap(videos.get(i).toMap());
- }
-
- promise.resolve(items);
- }
-
- @ReactMethod
- public void getThumbnailPath(Promise promise) {
- if (context != null) {
- File cacheDir = context.getExternalCacheDir();
- String thumbnailPath = String.format("%s/thumbnails", cacheDir.getAbsolutePath());
- promise.resolve(thumbnailPath);
- return;
- }
-
- promise.resolve(null);
- }
-
- @ReactMethod
- public void getUploadsPath(Promise promise) {
- if (context != null) {
- String baseFolder = Utils.getExternalStorageDir(context);
- String uploadsPath = String.format("%s/LBRY/Uploads", baseFolder);
- File uploadsDir = new File(uploadsPath);
- if (!uploadsDir.isDirectory()) {
- uploadsDir.mkdirs();
- }
- promise.resolve(uploadsPath);
- }
-
- promise.reject("The content could not be saved to the device. Please check your storage permissions.");
- }
-
- @ReactMethod
- public void createVideoThumbnail(String targetId, String filePath, Promise promise) {
- (new AsyncTask() {
- protected String doInBackground(Void... param) {
- String thumbnailPath = null;
-
- if (context != null) {
- Bitmap thumbnail = ThumbnailUtils.createVideoThumbnail(filePath, MediaStore.Video.Thumbnails.MINI_KIND);
- File cacheDir = context.getExternalCacheDir();
- thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
-
- File file = new File(thumbnailPath);
- try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
- thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
- os.close();
- } catch (IOException ex) {
- promise.reject("Could not create a thumbnail for the video");
- return null;
- }
- }
-
- return thumbnailPath;
- }
-
- public void onPostExecute(String thumbnailPath) {
- if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
- promise.resolve(thumbnailPath);
- }
- }
- }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- @ReactMethod
- public void createImageThumbnail(String targetId, String filePath, Promise promise) {
- (new AsyncTask() {
- protected String doInBackground(Void... param) {
- String thumbnailPath = null;
- FileOutputStream os = null;
- try {
- Bitmap source = BitmapFactory.decodeFile(filePath);
- // MINI_KIND dimensions
- Bitmap thumbnail = Bitmap.createScaledBitmap(source, 512, 384, false);
-
- if (context != null) {
- File cacheDir = context.getExternalCacheDir();
- thumbnailPath = String.format("%s/thumbnails/%s.png", cacheDir.getAbsolutePath(), targetId);
- os = new FileOutputStream(thumbnailPath);
- if (thumbnail != null) {
- thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
- }
- os.close();
- }
- } catch (IOException ex) {
- promise.reject("Could not create a thumbnail for the image");
- return null;
- } finally {
- if (os != null) {
- try {
- os.close();
- } catch (IOException ex) {
- // ignoe
- }
- }
- }
-
- return thumbnailPath;
- }
-
- public void onPostExecute(String thumbnailPath) {
- if (thumbnailPath != null && thumbnailPath.trim().length() > 0) {
- promise.resolve(thumbnailPath);
- }
- }
- }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private List loadVideos() {
- String[] projection = {
- MediaStore.MediaColumns._ID,
- MediaStore.MediaColumns.DATA,
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.MIME_TYPE,
- MediaStore.Video.Media.DURATION
- };
-
- List ids = new ArrayList();
- List items = new ArrayList();
- Cursor cursor = context.getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection, null, null,
- String.format("%s DESC", MediaStore.MediaColumns.DATE_MODIFIED));
- while (cursor.moveToNext()) {
- int idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID);
- int nameColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
- int typeColumn = cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE);
- int pathColumn = cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
- int durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION);
-
- String id = cursor.getString(idColumn);
- GalleryItem item = new GalleryItem();
- item.setId(id);
- item.setName(cursor.getString(nameColumn));
- item.setType(cursor.getString(typeColumn));
- item.setFilePath(cursor.getString(pathColumn));
- items.add(item);
- ids.add(id);
- }
-
- checkThumbnails(ids);
-
- return items;
- }
-
- private void checkThumbnails(final List ids) {
- (new AsyncTask() {
- protected Void doInBackground(Void... param) {
- if (context != null) {
- ContentResolver resolver = context.getContentResolver();
- for (int i = 0; i < ids.size(); i++) {
- String id = ids.get(i);
- File cacheDir = context.getExternalCacheDir();
- File thumbnailsDir = new File(String.format("%s/thumbnails", cacheDir.getAbsolutePath()));
- if (!thumbnailsDir.isDirectory()) {
- thumbnailsDir.mkdirs();
- }
-
- String thumbnailPath = String.format("%s/%s.png", thumbnailsDir.getAbsolutePath(), id);
- File file = new File(thumbnailPath);
- if (!file.exists()) {
- // save the thumbnail to the path
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 1;
- Bitmap thumbnail = MediaStore.Video.Thumbnails.getThumbnail(
- resolver, Long.parseLong(id), MediaStore.Video.Thumbnails.MINI_KIND, options);
- if (thumbnail != null) {
- try (FileOutputStream os = new FileOutputStream(thumbnailPath)) {
- thumbnail.compress(Bitmap.CompressFormat.PNG, 80, os);
- } catch (IOException ex) {
- // skip
- }
- }
- }
-
- if (file.exists() && file.length() > 0 && GalleryModule.this.context != null) {
- WritableMap params = Arguments.createMap();
- params.putString("id", id);
- ((ReactApplicationContext) GalleryModule.this.context).getJSModule(
- DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onGalleryThumbnailChecked", params);
- }
- }
- }
-
- return null;
- }
-
- public void onPostExecute(Void result) {
- if (GalleryModule.this.context != null) {
- ((ReactApplicationContext) GalleryModule.this.context).getJSModule(
- DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onAllGalleryThumbnailsChecked", null);
- }
- }
- }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private static class GalleryItem {
- private String id;
-
- private int duration;
-
- private String filePath;
-
- private String name;
-
- private String type;
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public int getDuration() {
- return duration;
- }
-
- public void setDuration(int duration) {
- this.duration = duration;
- }
-
- public String getFilePath() {
- return filePath;
- }
-
- public void setFilePath(String filePath) {
- this.filePath = filePath;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public WritableMap toMap() {
- WritableMap map = Arguments.createMap();
- map.putString("id", id);
- map.putString("name", name);
- map.putString("filePath", filePath);
- map.putString("type", type);
- map.putInt("duration", duration);
-
- return map;
- }
- }
-
- @ReactMethod
- public void canUseCamera(final Promise promise) {
- promise.resolve(MainActivity.hasPermission(Manifest.permission.CAMERA, MainActivity.getActivity()));
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/RequestsModule.java b/app/src/main/java/io/lbry/browser/reactmodules/RequestsModule.java
deleted file mode 100644
index a82a1707..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/RequestsModule.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.os.Bundle;
-
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.ReadableArray;
-import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.bridge.ReadableMapKeySetIterator;
-import com.facebook.react.bridge.ReadableType;
-import com.facebook.react.bridge.WritableMap;
-
-import io.lbry.browser.MainActivity;
-import io.lbry.lbrysdk.Utils;
-
-import java.util.List;
-import java.util.ArrayList;
-
-import org.json.JSONObject;
-import org.json.JSONArray;
-import org.json.JSONException;
-
-public class RequestsModule extends ReactContextBaseJavaModule {
- private Context context;
-
- public RequestsModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- }
-
- @Override
- public String getName() {
- return "Requests";
- }
-
- @ReactMethod
- public void get(final String url, final Promise promise) {
- (new AsyncTask() {
- @Override
- protected String doInBackground(Void... params) {
- try {
- return Utils.performRequest(url);
- } catch (Exception ex) {
- return null;
- }
- }
-
- protected void onPostExecute(String response) {
- if (response == null) {
- promise.reject(String.format("Request to %s returned null.", url));
- return;
- }
-
- promise.resolve(response);
- }
- }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-
- }
-
- @ReactMethod
- public void lbryioCall(String authToken, final Promise promise) {
- // get the auth token here, or let the app pass it in?
- }
-
- @ReactMethod
- public void lbryCall(final Promise promise) {
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/ScreenOrientationModule.java b/app/src/main/java/io/lbry/browser/reactmodules/ScreenOrientationModule.java
deleted file mode 100644
index 58abfa27..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/ScreenOrientationModule.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.pm.ActivityInfo;
-
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-
-public class ScreenOrientationModule extends ReactContextBaseJavaModule {
- private Context context;
-
- public ScreenOrientationModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- }
-
- @Override
- public String getName() {
- return "ScreenOrientation";
- }
-
- @ReactMethod
- public void unlockOrientation() {
- Activity activity = getCurrentActivity();
- if (activity != null) {
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
- }
- }
-
- @ReactMethod
- public void lockOrientationLandscape() {
- Activity activity = getCurrentActivity();
- if (activity != null) {
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
- }
- }
-
- @ReactMethod
- public void lockOrientationPortrait() {
- Activity activity = getCurrentActivity();
- if (activity != null) {
- activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- }
- }
-}
diff --git a/app/src/main/java/io/lbry/browser/reactmodules/StatePersistorModule.java b/app/src/main/java/io/lbry/browser/reactmodules/StatePersistorModule.java
deleted file mode 100644
index 9424abcb..00000000
--- a/app/src/main/java/io/lbry/browser/reactmodules/StatePersistorModule.java
+++ /dev/null
@@ -1,193 +0,0 @@
-package io.lbry.browser.reactmodules;
-
-import android.content.Context;
-import android.os.AsyncTask;
-import android.os.Bundle;
-
-import com.facebook.react.bridge.Arguments;
-import com.facebook.react.bridge.Promise;
-import com.facebook.react.bridge.ReactApplicationContext;
-import com.facebook.react.bridge.ReactContextBaseJavaModule;
-import com.facebook.react.bridge.ReactMethod;
-import com.facebook.react.bridge.ReadableArray;
-import com.facebook.react.bridge.ReadableMap;
-import com.facebook.react.bridge.ReadableMapKeySetIterator;
-import com.facebook.react.bridge.ReadableType;
-import com.facebook.react.bridge.WritableMap;
-
-import io.lbry.browser.MainActivity;
-
-import java.util.List;
-import java.util.ArrayList;
-
-import org.json.JSONObject;
-import org.json.JSONArray;
-import org.json.JSONException;
-
-public class StatePersistorModule extends ReactContextBaseJavaModule {
- private Context context;
-
- private List queue;
-
- private ReadableMap filter;
-
- private ReadableMap lastState;
-
- private AsyncTask persistTask;
-
- public StatePersistorModule(ReactApplicationContext reactContext) {
- super(reactContext);
- this.context = reactContext;
- queue = new ArrayList();
- }
-
- @Override
- public String getName() {
- return "StatePersistor";
- }
-
- /*private WritableMap filterState(ReadableMap state) {
- WritableMap filteredState = Arguments.createMap();
-
- return state;
- }*/
-
- public boolean hasStateChanged(ReadableMap newState) {
- return false;
- }
-
- @ReactMethod
- public void update(ReadableMap state, ReadableMap filter) {
- if (this.filter == null) {
- this.filter = filter;
- }
- // process state updates from the queue using a background task
- synchronized (this) {
- queue.add(state);
- }
- persistState();
- }
-
- private void persistState() {
- persistState(false);
- }
-
- private void persistState(final boolean flush) {
- if (flush && persistTask != null) {
- persistTask.cancel(true);
- persistTask = null;
- }
-
- if (persistTask == null) {
- persistTask = (new AsyncTask