Native rewrite (#878)

* initial native rewrite commit
* update gitlab CI script
* add printVersionName gradle task
* fix Gitlab CI script
* Fix first time wallet sync. add Discover dialog to  Following page.
* Finish Following and All Content views. Add customize your tags view.
* Wallet sync get and set preferences, update interval.
* Editor's Choice. Reposts. Some ontent page tweaks.
* display no related content view when none loaded
* Search cache. File view updates. Floating wallet balance.
* Send tip dialog. Channel page share and follow/unfollow.
* Handle lbry:// url scheme. Properly set URL bar values. SDK 0.71.0.
* Channel follow/unfollow fixes. Display stream cost.
* Channel management and channel creation / editing
* phone number verification and rewards page
* add Invites page
* tweak player loading and playback when loading new claims
* tweak about page layout
* display text and markdown content
* purchase_uri for free content
* don't display invites history if none exist
* fix channel list adapters
* change launch mode from singleInstance to singleTask
* url history and player fixes
* Library page. URL and view history.
* bumpversion 0.15.0 --> 0.15.1
* Make file view a fragment to prevent headaches with multiple Android task recents
* Better handling of file view URLs. Some issue list fixes.
* Abandon channels and bulk delete files tasks. Some visual tweaks.
* bumpversion 0.15.1 --> 0.15.2
* fix some events
* Some visual tweaks. Wunderbar clear focus hotfix for some devices.
* sdk 0.74.0. Publish and Publishes pages.
* Fix displayed amounts. Send Firebase token with install_new.
* Some dark theme and crash fixes. Implement publish form.
* Fix minor typo for string in 'generate_address_hint'.
* Publish form and publish creation flow. UI tweaks and fixes.
* Basic native mobile publishing
* remove closeDatabase calls causing crashes
* Implement file and channel page delete actions. UI action cleanup.
* publish drivers for unresolved file page and featured search result item
* show URL suggestions and data network (DHT) settings
* Filter own claims from downloads. Fix address input.
* fix edit channel crash
* fix for possible blank / invalid video thumbnails
* adjust minimum deposit. fix channel edit mode.
* quick skip and playback speed media controls
* change play and pause icons
* Fix file size display. Tweak playback speed control.
* Add exoplayer mediasession extension. Set player auto attributes.
* Inline publish address validation error. Increase image upload request timeout.
* fix no related content display
* Claim new_android reward. Use canonical_url for share links.
* force US locale for amount / bid values sent with sdk requests
* Afrikaans and Spanish strings
* Add media player error handling policy. Use : in share links.
* don't proceed with publish if optimization is in progress.
This commit is contained in:
Akinwale Ariwodola 2020-05-23 07:49:00 +01:00 committed by GitHub
parent 6eb20d0e08
commit 40c36df414
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
858 changed files with 39058 additions and 8188 deletions

1
.gitignore vendored
View file

@ -60,7 +60,6 @@ buck-out/
# Other Files # Other Files
app/google-services.json app/google-services.json
app/src/main/assets/index.android.bundle
*.log *.log
.vagrant .vagrant
*.hprof *.hprof

View file

@ -3,18 +3,12 @@ stages:
- deploy - deploy
- release - release
variables:
REACT_NATIVE_BRANCH: "master"
build apk: build apk:
stage: build stage: build
image: lbry/android-base:platform-28 image: lbry/android-base:platform-28
before_script: 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 - 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 - chmod u+x $CI_PROJECT_DIR/gradlew
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23 - export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1) - export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1)
@ -26,25 +20,13 @@ build apk:
script: script:
- export PATH=/usr/bin:$PATH - export PATH=/usr/bin:$PATH
- echo "$PGP_PRIVATE_KEY" | gpg --batch --import - 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 - 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 - - 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 - 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 - yarn
- rm -rf android # temporary, should be a submodule init?
- cp -rf $CI_PROJECT_DIR/ android/
- cd android
- chmod u+x ./release.sh - chmod u+x ./release.sh
- ./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__arm.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null - cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null

View file

@ -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"]

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -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",
],
)

View file

@ -1,137 +1,8 @@
apply plugin: "com.android.application" 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);
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion 29
buildToolsVersion "29.0.1"
flavorDimensions "default" flavorDimensions "default"
compileOptions { compileOptions {
@ -141,18 +12,14 @@ android {
defaultConfig { defaultConfig {
applicationId "io.lbry.browser" applicationId "io.lbry.browser"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion 21
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion 29
versionCode 1403 versionCode 1503
versionName "0.14.3" versionName "0.15.3"
missingDimensionStrategy 'react-native-camera', 'general'
multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
dexOptions {
javaMaxHeapSize "2048M"
preDexLibraries false
jumboMode true
} }
productFlavors { productFlavors {
__32bit { __32bit {
versionCode android.defaultConfig.versionCode * 10 + 1 versionCode android.defaultConfig.versionCode * 10 + 1
@ -167,66 +34,70 @@ android {
} }
} }
} }
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes { buildTypes {
debug {
signingConfig signingConfigs.debug
}
release { release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
} }
task printVersionName {
doLast {
println android.defaultConfig.versionName
}
}
dependencies { dependencies {
implementation project(':@react-native-community_async-storage') implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':react-native-camera')
implementation project(':react-native-exception-handler') implementation 'androidx.appcompat:appcompat:1.1.0'
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 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.media:media:1.0.0' implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.appcompat:appcompat:1.0.0' implementation "androidx.cardview:cardview:1.0.0"
implementation 'com.facebook.react:react-native:0.61.5' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.facebook.fresco:fresco:1.9.0' implementation 'androidx.navigation:navigation-fragment:2.2.2'
implementation 'com.facebook.fresco:animated-gif:1.9.0' implementation 'androidx.navigation:navigation-ui:2.2.2'
implementation 'com.squareup.picasso:picasso:2.71828' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'com.google.firebase:firebase-analytics:17.2.1' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'com.google.android.gms:play-services-base:17.1.0' implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation 'androidx.camera:camera-camera2:1.0.0-beta03'
implementation 'com.facebook.fresco:animated-base-support:1.3.0' implementation 'androidx.camera:camera-lifecycle:1.0.0-beta03'
implementation 'com.facebook.fresco:animated-gif:1.10.0' implementation 'androidx.camera:camera-view:1.0.0-alpha10'
implementation 'com.google.firebase:firebase-messaging:20.1.0'
__32bitImplementation files('libs/lbrysdk-0.73.1-release__arm.aar') implementation 'com.github.bumptech.glide:glide:4.11.0'
__64bitImplementation files('libs/lbrysdk-0.73.1-release__arm64.aar') 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) { implementation 'com.google.code.gson:gson:2.8.6'
def hermesPath = "../../node_modules/hermes-engine/android/"; implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'
debugImplementation files(hermesPath + "hermes-debug.aar") implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4'
releaseImplementation files(hermesPath + "hermes-release.aar") implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.4'
} else { implementation 'com.google.android.exoplayer:extension-cast:2.11.4'
implementation jscFlavor implementation 'com.google.android.exoplayer:extension-mediasession:2.11.4'
}
}
// Run this once to be able to run the application with BUCK implementation 'com.google.android:flexbox:2.0.1'
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) { implementation 'com.hbb20:ccp:2.3.8'
from configurations.compile
into 'libs' 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' apply plugin: 'com.google.gms.google-services'

View file

@ -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,
)

Binary file not shown.

View file

@ -1,10 +1,21 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified # You can control the set of applied configuration files using the
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt # proguardFiles setting in build.gradle.
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # 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

View file

@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@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());
}
}

View file

@ -1,30 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.lbry.browser" package="io.lbry.browser"
android:installLocation="auto"> android:installLocation="auto">
<supports-screens <uses-feature android:name="android.hardware.camera" android:required="false" />
android:smallScreens="true"
android:normalScreens="true"
android:largeScreens="true"
android:anyDensity="true"
android:xlargeScreens="true"
/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:label="@string/app_name" <application
android:icon="@drawable/icon"
android:allowBackup="true" android:allowBackup="true"
android:theme="@style/LbryAppTheme" android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true" android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.google.android.exoplayer2.ext.cast.DefaultCastOptionsProvider"/>
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_icon" android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_lbry" /> android:resource="@drawable/ic_lbry" />
@ -37,16 +37,18 @@
<meta-data android:name="wakelock" android:value="0"/> <meta-data android:name="wakelock" android:value="0"/>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" /> <activity
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"
<activity android:name="io.lbry.browser.MainActivity" android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/LbryAppTheme" android:supportsPictureInPicture="true"
android:configChanges="keyboardHidden|orientation|screenSize" android:theme="@style/AppTheme.NoActionBar"
android:screenOrientation="portrait" android:launchMode="singleTask"
android:launchMode="singleInstance" android:windowSoftInputMode="adjustResize">
> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -54,14 +56,19 @@
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lbry" /> <data android:scheme="lbry" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity> </activity>
<receiver android:name="io.lbry.browser.receivers.NotificationDeletedReceiver" /> <activity
android:name=".FirstRunActivity"
android:launchMode="singleTask"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBarTranslucent" />
<activity
android:name=".VerificationActivity"
android:launchMode="singleTask"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBarTranslucent"
android:windowSoftInputMode="adjustResize" />
<service <service
android:name="io.lbry.browser.LbrynetMessagingService" android:name="io.lbry.browser.LbrynetMessagingService"
@ -81,5 +88,4 @@
android:resource="@xml/filepaths" /> android:resource="@xml/filepaths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -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<String> activeDownloads = new ArrayList<String>();
private List<String> completedDownloads = new ArrayList<String>();
private Map<String, String> downloadIdOutpointsMap = new HashMap<String, String>();
// 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<String, Double> writtenDownloadBytes = new HashMap<String, Double>();
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
private HashMap<String, Boolean> stoppedDownloadsMap = new HashMap<String, Boolean>();
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<String> getActiveDownloads() {
return activeDownloads;
}
public List<String> 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);
}
}

View file

@ -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<Void, Void, Void> {
private Context context;
public AuthenticateTask(Context context) {
this.context = context;
}
protected Void doInBackground(Void... params) {
Lbryio.authenticate(context);
return null;
}
}
}

View file

@ -12,14 +12,16 @@ import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import android.util.Log; import android.util.Log;
import com.google.firebase.analytics.FirebaseAnalytics; import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage; import com.google.firebase.messaging.RemoteMessage;
import io.lbry.browser.utils.LbryAnalytics;
import io.lbry.lbrysdk.LbrynetService; import io.lbry.lbrysdk.LbrynetService;
import io.lbry.browser.reactmodules.UtilityModule;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
@ -30,22 +32,15 @@ import java.util.Map;
public class LbrynetMessagingService extends FirebaseMessagingService { public class LbrynetMessagingService extends FirebaseMessagingService {
private static final String TAG = "LbrynetMessagingService"; private static final String TAG = "LbrynetMessagingService";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL"; 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_SUBSCRIPTION = "subscription";
private static final String TYPE_REWARD = "reward"; private static final String TYPE_REWARD = "reward";
private static final String TYPE_INTERESTS = "interests"; private static final String TYPE_INTERESTS = "interests";
private static final String TYPE_CREATOR = "creator"; private static final String TYPE_CREATOR = "creator";
private FirebaseAnalytics firebaseAnalytics; private FirebaseAnalytics firebaseAnalytics;
@Override @Override
public void onMessageReceived(RemoteMessage remoteMessage) { public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
if (firebaseAnalytics == null) { if (firebaseAnalytics == null) {
firebaseAnalytics = FirebaseAnalytics.getInstance(this); firebaseAnalytics = FirebaseAnalytics.getInstance(this);
} }
@ -67,7 +62,7 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
if (firebaseAnalytics != null) { if (firebaseAnalytics != null) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString("name", name); 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); sendNotification(title, body, type, url, name, contentTitle, channelUrl, publishTime);
@ -113,29 +108,6 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
// default to home page // default to home page
url = "lbry://?discover"; 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)); Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@ -164,23 +136,23 @@ public class LbrynetMessagingService extends FirebaseMessagingService {
notificationManager.createNotificationChannel(channel); notificationManager.createNotificationChannel(channel);
} }
notificationManager.notify(9898, notificationBuilder.build()); notificationManager.notify(3, notificationBuilder.build());
} }
public List<String> getEnabledTypes() { public List<String> getEnabledTypes() {
SharedPreferences sp = getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
List<String> enabledTypes = new ArrayList<String>(); List<String> enabledTypes = new ArrayList<String>();
if (sp.getBoolean(UtilityModule.RECEIVE_SUBSCRIPTION_NOTIFICATIONS, true)) { if (sp.getBoolean(MainActivity.PREFERENCE_KEY_NOTIFICATION_SUBSCRIPTIONS, true)) {
enabledTypes.add(TYPE_SUBSCRIPTION); 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); 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); 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); enabledTypes.add(TYPE_CREATOR);
} }

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}

View file

@ -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<ChannelFilterListAdapter.ViewHolder> {
private Context context;
private List<Claim> 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<Claim> 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();
});
}
}

View file

@ -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<ClaimListAdapter.ViewHolder> {
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<String, Claim> quickClaimIdMap;
private Map<String, Claim> quickClaimUrlMap;
private Map<String, Boolean> notFoundClaimIdMap;
private Map<String, Boolean> notFoundClaimUrlMap;
@Setter
private boolean hideFee;
@Setter
private boolean canEnterSelectionMode;
private Context context;
private List<Claim> items;
private List<Claim> selectedItems;
@Setter
private ClaimListItemListener listener;
@Getter
@Setter
private boolean inSelectionMode;
@Setter
private SelectionModeListener selectionModeListener;
private float scale;
public ClaimListAdapter(List<Claim> 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<Claim> 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<Claim> 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<Claim> claims) {
for (Claim claim : claims) {
if (!items.contains(claim)) {
items.add(claim);
}
}
notFoundClaimUrlMap.clear();
notFoundClaimIdMap.clear();
notifyDataSetChanged();
}
public void setItems(List<Claim> claims) {
items = new ArrayList<>(claims);
notifyDataSetChanged();
}
public void removeItems(List<Claim> 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);
}
}

View file

@ -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<EditorsChoiceItemAdapter.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 1;
private static final int VIEW_TYPE_CONTENT = 2;
private Context context;
private List<EditorsChoiceItem> items;
@Setter
private EditorsChoiceItemListener listener;
public EditorsChoiceItemAdapter(List<EditorsChoiceItem> 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<EditorsChoiceItem> 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);
}
}

View file

@ -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<GalleryGridAdapter.ViewHolder> {
private Context context;
private List<GalleryItem> items;
@Setter
private GalleryItemClickListener listener;
public GalleryGridAdapter(List<GalleryItem> 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<GalleryItem> 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
}
}
}
}

View file

@ -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<Claim> {
private List<Claim> channels;
private int layoutResourceId;
private LayoutInflater inflater;
public InlineChannelSpinnerAdapter(Context context, int resource, List<Claim> 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;
}
}

View file

@ -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<InviteeListAdapter.ViewHolder> {
private Context context;
private List<Invitee> items;
public InviteeListAdapter(List<Invitee> invitees, Context context) {
this.context = context;
this.items = new ArrayList<>(invitees);
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<Invitee> getItems() {
return new ArrayList<>(items);
}
public void addHeader() {
Invitee header = new Invitee();
header.setHeader(true);
items.add(0, header);
}
public void addInvitees(List<Invitee> 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);
}
}
}

View file

@ -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<Language> {
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;
}
}

View file

@ -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<License> {
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;
}
}

View file

@ -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<NavigationMenuAdapter.ViewHolder> {
private static final int TYPE_GROUP = 1;
private static final int TYPE_ITEM = 2;
private Context context;
private List<NavMenuItem> menuItems;
private NavMenuItem currentItem;
@Setter
private NavigationMenuItemClickListener listener;
public NavigationMenuAdapter(List<NavMenuItem> 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);
}
}

View file

@ -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<RewardListAdapter.ViewHolder> {
public static final int DISPLAY_MODE_ALL = 1;
public static final int DISPLAY_MODE_UNCLAIMED = 2;
private Context context;
@Setter
private List<Reward> all;
private List<Reward> items;
@Setter
private RewardClickListener clickListener;
@Getter
private int displayMode;
public RewardListAdapter(List<Reward> 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<Reward> 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<Reward> 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);
}
}

View file

@ -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<SuggestedChannelGridAdapter.ViewHolder> {
private Context context;
private List<Claim> items;
private List<Claim> selectedItems;
@Setter
private ChannelItemSelectionListener listener;
public SuggestedChannelGridAdapter(List<Claim> 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<Claim> getSelectedItems() {
return this.selectedItems;
}
public void clearSelectedItems() {
this.selectedItems.clear();
}
public boolean isClaimSelected(Claim claim) {
return selectedItems.contains(claim);
}
public void addClaims(List<Claim> 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();
});
}
}

View file

@ -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<TagListAdapter.ViewHolder> {
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<Tag> items;
@Setter
private TagClickListener clickListener;
@Getter
@Setter
private int customizeMode;
public TagListAdapter(List<Tag> 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<Tag> getTags() {
return new ArrayList<>(items);
}
public void setTags(List<Tag> tags) {
items = new ArrayList<>(tags);
notifyDataSetChanged();
}
public void addTags(List<Tag> 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);
}
}

View file

@ -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<TransactionListAdapter.ViewHolder> {
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<Transaction> items;
@Setter
private TransactionClickListener listener;
public TransactionListAdapter(List<Transaction> transactions, Context context) {
this.context = context;
this.items = new ArrayList<>(transactions);
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<Transaction> getItems() {
return new ArrayList<>(items);
}
public void addTransactions(List<Transaction> 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);
}
}

View file

@ -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<UrlSuggestionListAdapter.ViewHolder> {
private Context context;
private List<UrlSuggestion> items;
@Setter
private UrlSuggestionClickListener listener;
public UrlSuggestionListAdapter(Context context) {
this.context = context;
this.items = new ArrayList<>();
}
public void clear() {
items.clear();
notifyDataSetChanged();
}
public List<UrlSuggestion> getItems() {
return new ArrayList<>(items);
}
public List<String> getItemUrls() {
List<String> 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<UrlSuggestion> 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);
}
}

View file

@ -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;
}
}

View file

@ -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<UrlSuggestion> getRecentHistory(SQLiteDatabase db) {
List<UrlSuggestion> 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<ViewHistory> getViewHistory(String lastTimestamp, int pageLimit, SQLiteDatabase db) {
List<ViewHistory> 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<Tag> getTags(SQLiteDatabase db) {
List<Tag> 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<Subscription> getSubscriptions(SQLiteDatabase db) {
List<Subscription> 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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<Tag> 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);
}
}

View file

@ -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();
}
}

View file

@ -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<Claim> 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<Claim> 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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.listener;
public interface CameraPermissionListener {
void onCameraPermissionGranted();
void onCameraPermissionRefused();
void onRecordAudioPermissionGranted();
void onRecordAudioPermissionRefused();
}

View file

@ -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();
}

View file

@ -0,0 +1,5 @@
package io.lbry.browser.listener;
public interface DownloadActionListener {
void onDownloadAction(String downloadAction, String uri, String outpoint, String fileInfoJson, double progress);
}

View file

@ -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<Claim> channels);
}

View file

@ -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<Claim> claims);
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface FilePickerListener {
void onFilePicked(String filePath);
void onFilePickerCancelled();
}

View file

@ -0,0 +1,5 @@
package io.lbry.browser.listener;
public interface SdkStatusListener {
void onSdkReady();
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.listener;
public interface SelectionModeListener {
void onEnterSelectionMode();
void onExitSelectionMode();
void onItemSelectionToggled();
}

View file

@ -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();
}

View file

@ -0,0 +1,6 @@
package io.lbry.browser.listener;
public interface StoragePermissionListener {
void onStoragePermissionGranted();
void onStoragePermissionRefused();
}

View file

@ -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);
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.listener;
import io.lbry.browser.model.WalletBalance;
public interface WalletBalanceListener {
void onWalletBalanceUpdated(WalletBalance walletBalance);
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.listener;
public interface WalletSyncListener {
void onWalletSyncProcessing();
void onWalletSyncWaitingForInput();
void onWalletSyncEnabled();
void onWalletSyncFailed(Exception error);
}

View file

@ -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<String> CLAIM_TYPES = Arrays.asList(TYPE_CHANNEL, TYPE_STREAM);
public static final List<String> 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<String> 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<String> getTags() {
return (value != null && value.getTags() != null) ? new ArrayList<>(value.getTags()) : new ArrayList<>();
}
public List<Tag> getTagObjects() {
List<Tag> 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<Claim>(){}.getType();
Type streamMetadataType = new TypeToken<StreamMetadata>(){}.getType();
Type channelMetadataType = new TypeToken<ChannelMetadata>(){}.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<String> languages;
private List<String> tags;
private List<Location> 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<String> 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
}
}

View file

@ -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();
}
}

View file

@ -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<Claim> claims;
@Getter
@Setter
private long timestamp;
public ClaimSearchCacheValue(List<Claim> claims, long timestamp) {
this.claims = new ArrayList<>(claims);
this.timestamp = timestamp;
}
public boolean isExpired(long ttl) {
return System.currentTimeMillis() - timestamp > ttl;
}
}

View file

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

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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<LbryFile>(){}.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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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<NavMenuItem> 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;
}
}

View file

@ -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<Tag> {
@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());
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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<Reward>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
return gson.fromJson(rewardJson, type);
}
}

View file

@ -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();
}
}

View file

@ -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<String> 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<YoutubeChannel> youtubeChannels;
private List<String> deviceTypes;
@Data
public static class YoutubeChannel {
String ytChannelName;
String lbryChannelName;
String channelClaimId;
String syncStatus;
String statusToken;
boolean transferable;
String transferState;
List<String> publishToAddress;
String publicKey;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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<String, Object> payloadMap = payload.toHashMap();
for (Map.Entry<String, Object> 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<InstanceIdResult>() {
@Override
public void onComplete(Task<InstanceIdResult> 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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<GalleryItem> 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<Void, Void, String>() {
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<Void, Void, String>() {
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<GalleryItem> loadVideos() {
String[] projection = {
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION
};
List<String> ids = new ArrayList<String>();
List<GalleryItem> items = new ArrayList<GalleryItem>();
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<String> ids) {
(new AsyncTask<Void, Void, Void>() {
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()));
}
}

View file

@ -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<Void, Void, String>() {
@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) {
}
}

View file

@ -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);
}
}
}

View file

@ -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<ReadableMap> queue;
private ReadableMap filter;
private ReadableMap lastState;
private AsyncTask persistTask;
public StatePersistorModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
queue = new ArrayList<ReadableMap>();
}
@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<Object, Void, Boolean>() {
protected Boolean doInBackground(Object... param) {
// get the first item in the queue
ReadableMap queuedState = null;
if (queue.size() > 0) {
synchronized (StatePersistorModule.this) {
queuedState = queue.remove(flush ? queue.size() - 1 : 0);
if (flush) {
// we only want the final state in this scenario
queue.clear();
}
}
}
if (queuedState != null) {
ReadableMap state = queuedState; //(ReadableMap) filterState(queuedState);
// convert to JSON object
try {
JSONObject json = readableMapToJSON(state);
// save the state file
// TODO: explore this option at a later date
throw new UnsupportedOperationException();
} catch (JSONException ex) {
// normally shouldn't happen, but if it does, reinsert into the queue
if (queuedState != null) {
synchronized (StatePersistorModule.this) {
queue.add(0, queuedState);
}
}
return false;
}
}
return false;
}
public void onPostExecute(Boolean result) {
if (queue.size() > 0) {
persistState();
}
persistTask = null;
}
});
persistTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@ReactMethod
public void flush() {
persistState(true);
}
private static JSONObject readableMapToJSON(ReadableMap readableMap) throws JSONException {
JSONObject json = new JSONObject();
ReadableMapKeySetIterator iterator = readableMap.keySetIterator();
while (iterator.hasNextKey()) {
String key = iterator.nextKey();
switch (readableMap.getType(key)) {
case Map:
json.put(key, readableMapToJSON(readableMap.getMap(key)));
break;
case Array:
json.put(key, readableArrayToJSON(readableMap.getArray(key)));
break;
case Boolean:
json.put(key, readableMap.getBoolean(key));
break;
case Null:
json.put(key, JSONObject.NULL);
break;
case Number:
json.put(key, readableMap.getDouble(key));
break;
case String:
json.put(key, readableMap.getString(key));
break;
}
}
return json;
}
private static JSONArray readableArrayToJSON(ReadableArray readableArray) throws JSONException {
JSONArray array = new JSONArray();
for (int i = 0; i < readableArray.size(); i++) {
switch (readableArray.getType(i)) {
case Null:
break;
case Boolean:
array.put(readableArray.getBoolean(i));
break;
case Number:
array.put(readableArray.getDouble(i));
break;
case String:
array.put(readableArray.getString(i));
break;
case Map:
array.put(readableMapToJSON(readableArray.getMap(i)));
break;
case Array:
array.put(readableArrayToJSON(readableArray.getArray(i)));
break;
}
}
return array;
}
}

View file

@ -1,537 +0,0 @@
package io.lbry.browser.reactmodules;
import android.app.Activity;
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.graphics.Bitmap;
import android.Manifest;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import androidx.core.content.FileProvider;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import android.telephony.TelephonyManager;
import android.view.View;
import android.view.WindowManager;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.squareup.picasso.Picasso;
import java.io.File;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.security.KeyStore;
import io.lbry.browser.DownloadManager;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.lbrysdk.Utils;
public class UtilityModule extends ReactContextBaseJavaModule {
private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>();
private static final String FILE_PROVIDER = "io.lbry.browser.fileprovider";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.SUBSCRIPTIONS_NOTIFICATION_CHANNEL";
public static final String ACTION_NOTIFICATION_PLAY = "io.lbry.browser.ACTION_NOTIFICATION_PLAY";
public static final String ACTION_NOTIFICATION_LATER = "io.lbry.browser.ACTION_NOTIFICATION_LATER";
// Setting keys from React Native
public static final String RECEIVE_SUBSCRIPTION_NOTIFICATIONS = "receiveSubscriptionNotifications";
public static final String RECEIVE_REWARD_NOTIFICATIONS = "receiveRewardNotifications";
public static final String RECEIVE_INTERESTS_NOTIFICATIONS = "receiveInterestsNotifications";
public static final String RECEIVE_CREATOR_NOTIFICATIONS = "receiveCreatorNotifications";
public static final String DHT_ENABLED = "dhtEnabled";
// the last language set to be loaded
private static final String LANGUAGE_SETTING_KEY = "language";
private String language;
private Context context;
private KeyStore keyStore;
public UtilityModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
try {
this.keyStore = Utils.initKeyStore(context);
} catch (Exception ex) {
// continue without keystore
}
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
language = sp.getString(LANGUAGE_SETTING_KEY, "en");
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> constants = MapBuilder.newHashMap();
constants.put("language", language);
constants.put("dhtEnabled", LbrynetService.isDHTEnabled());
return constants;
}
@Override
public String getName() {
return "UtilityModule";
}
@ReactMethod
public void keepAwakeOn() {
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
@ReactMethod
public void keepAwakeOff() {
final Activity activity = getCurrentActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
});
}
}
@ReactMethod
public void hideNavigationBar() {
final Activity activity = MainActivity.getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
public void run() {
View decorView = activity.getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
});
}
}
@ReactMethod
public void showNavigationBar() {
final Activity activity = MainActivity.getActivity();
if (activity != null) {
activity.runOnUiThread(new Runnable() {
public void run() {
View decorView = activity.getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_VISIBLE);
}
});
}
}
@ReactMethod
public void getDeviceId(boolean requestPermission, final Promise promise) {
if (isEmulator()) {
promise.reject("Rewards cannot be claimed from an emulator nor virtual device.");
return;
}
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
String id = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
id = telephonyManager.getImei(); // GSM
if (id == null) {
id = telephonyManager.getMeid(); // CDMA
}
} else {
id = telephonyManager.getDeviceId();
}
} catch (SecurityException ex) {
// Maybe the permission was not granted? Try to acquire permission
/*if (requestPermission) {
requestPhoneStatePermission();
}*/
} catch (Exception ex) {
// id could not be obtained. Display a warning that rewards cannot be claimed.
promise.reject(ex.getMessage());
}
if (id == null || id.trim().length() == 0) {
promise.reject("Rewards cannot be claimed because your device could not be identified.");
return;
}
promise.resolve(id);
}
@ReactMethod
public void canReceiveSms(final Promise promise) {
promise.resolve(MainActivity.hasPermission(Manifest.permission.RECEIVE_SMS, MainActivity.getActivity()));
}
@ReactMethod
public void requestReceiveSmsPermission() {
MainActivity activity = (MainActivity) MainActivity.getActivity();
if (activity != null) {
// Request for the RECEIVE_SMS permission
MainActivity.checkReceiveSmsPermission(activity);
}
}
@ReactMethod
public void canReadWriteStorage(final Promise promise) {
promise.resolve(MainActivity.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, MainActivity.getActivity()));
}
@ReactMethod
public void requestStoragePermission() {
MainActivity activity = (MainActivity) MainActivity.getActivity();
if (activity != null) {
MainActivity.checkStoragePermission(activity);
}
}
@ReactMethod
public void shareLogFile(Callback errorCallback) {
String logFileName = "lbrynet.log";
File logFile = new File(String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet"), logFileName);
if (!logFile.exists()) {
errorCallback.invoke("The lbrynet.log file could not be found.");
return;
}
try {
Uri fileUri = FileProvider.getUriForFile(context, FILE_PROVIDER, logFile);
if (fileUri != null) {
Intent shareIntent = new Intent();
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
Intent sendLogIntent = Intent.createChooser(shareIntent, "Send LBRY log");
sendLogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(sendLogIntent);
}
} catch (IllegalArgumentException e) {
errorCallback.invoke("The lbrynet.log file cannot be shared due to permission restrictions.");
}
}
@ReactMethod
public void shareUrl(String url) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, url);
Intent shareUrlIntent = Intent.createChooser(shareIntent, "Share LBRY content");
shareUrlIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(shareUrlIntent);
}
@ReactMethod
public void showNotificationForContent(final String uri, String title, String publisher, final String thumbnail, boolean isPlayable) {
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Subscriptions", NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("LBRY subscription notifications");
notificationManager.createNotificationChannel(channel);
}
if (activeNotifications.containsKey(uri)) {
// the notification for the specified uri is already present, don't try to create another one
return;
}
int id = 0;
Random random = new Random();
do {
id = random.nextInt();
} while (id < 100);
final int notificationId = id;
String uriWithParam = String.format("%s?download=true", uri);
Intent playIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uriWithParam));
playIntent.putExtra(MainActivity.SOURCE_NOTIFICATION_ID_KEY, notificationId);
playIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent playPendingIntent = PendingIntent.getActivity(context, 0, playIntent, PendingIntent.FLAG_CANCEL_CURRENT);
boolean hasThumbnail = false;
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
builder.setAutoCancel(true)
.setColor(ContextCompat.getColor(context, R.color.lbryGreen))
.setContentIntent(DownloadManager.getLaunchPendingIntent(uri, context))
.setContentTitle(publisher)
.setContentText(title)
.setSmallIcon(R.drawable.ic_lbry)
.addAction(android.R.drawable.ic_media_play, (isPlayable ? "Play" : "Open"), playPendingIntent);
activeNotifications.put(uri, notificationId);
if (thumbnail != null) {
// attempt to load the thumbnail Bitmap before displaying the notification
final Uri thumbnailUri = Uri.parse(thumbnail);
if (thumbnailUri != null) {
hasThumbnail = true;
(new AsyncTask<Void, Void, Bitmap>() {
protected Bitmap doInBackground(Void... params) {
try {
return Picasso.get().load(thumbnailUri).get();
} catch (Exception e) {
return null;
}
}
protected void onPostExecute(Bitmap result) {
if (result != null) {
builder.setLargeIcon(result)
.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(result).bigLargeIcon(null));
}
notificationManager.notify(notificationId, builder.build());
}
}).execute();
}
}
if (!hasThumbnail) {
notificationManager.notify(notificationId, builder.build());
}
}
private static boolean isEmulator() {
String buildModel = Build.MODEL.toLowerCase();
return (// Check FINGERPRINT
Build.FINGERPRINT.startsWith("generic") ||
Build.FINGERPRINT.startsWith("unknown") ||
Build.FINGERPRINT.contains("test-keys") ||
// Check MODEL
buildModel.contains("google_sdk") ||
buildModel.contains("emulator") ||
buildModel.contains("android sdk built for x86") ||
// Check MANUFACTURER
Build.MANUFACTURER.contains("Genymotion") ||
"unknown".equals(Build.MANUFACTURER) ||
// Check HARDWARE
Build.HARDWARE.contains("goldfish") ||
Build.HARDWARE.contains("vbox86") ||
// Check PRODUCT
"google_sdk".equals(Build.PRODUCT) ||
"sdk_google_phone_x86".equals(Build.PRODUCT) ||
"sdk".equals(Build.PRODUCT) ||
"sdk_x86".equals(Build.PRODUCT) ||
"vbox86p".equals(Build.PRODUCT) ||
// Check BRAND and DEVICE
(Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
);
}
@ReactMethod
public void setSecureValue(String key, String value) {
if (keyStore != null) {
Utils.setSecureValue(key, value, context, keyStore);
}
}
@ReactMethod
public void getSecureValue(String key, Promise promise) {
if (keyStore == null) {
promise.reject("no keyStore found");
return;
}
promise.resolve(Utils.getSecureValue(key, context, keyStore));
}
@ReactMethod
public void checkDownloads() {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_CHECK_DOWNLOADS);
if (context != null) {
context.sendBroadcast(intent);
}
}
@ReactMethod
public void queueDownload(String outpoint) {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_QUEUE_DOWNLOAD);
intent.putExtra("outpoint", outpoint);
if (context != null) {
context.sendBroadcast(intent);
}
}
@ReactMethod
public void deleteDownload(String uri) {
Intent intent = new Intent();
intent.setAction(LbrynetService.ACTION_DELETE_DOWNLOAD);
intent.putExtra("uri", uri);
if (context != null) {
context.sendBroadcast(intent);
}
}
@ReactMethod
public void openDocumentPicker(String type) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.setType(type);
Activity activity = MainActivity.getActivity();
if (activity != null) {
activity.startActivityForResult(
Intent.createChooser(intent, "Select a file"), MainActivity.DOCUMENT_PICKER_RESULT_CODE);
}
}
@ReactMethod
public void setNativeBooleanSetting(String key, final boolean value) {
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putBoolean(key, value);
editor.commit();
}
if (DHT_ENABLED.equalsIgnoreCase(key)) {
(new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... params) {
String fileContent = value ? "on" : "off";
String path = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "dht");
PrintStream out = null;
try {
out = new PrintStream(new FileOutputStream(path));
out.print(fileContent);
} catch (Exception ex) {
// pass
} finally {
if (out != null) {
out.close();
}
}
return null;
}
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
@ReactMethod
public void getNativeBooleanSetting(String key, boolean defaultValue, Promise promise) {
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
promise.resolve(sp.getBoolean(key, defaultValue));
} else {
promise.resolve(null);
}
}
@ReactMethod
public void setNativeStringSetting(String key, String value) {
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(key, value);
editor.commit();
}
}
@ReactMethod
public void getNativeStringSetting(String key, String defaultValue, Promise promise) {
if (context != null) {
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
promise.resolve(sp.getString(key, defaultValue));
} else {
promise.resolve(null);
}
}
@ReactMethod
public void getNotificationLaunchTarget(Promise promise) {
Activity activity = MainActivity.getActivity();
if (activity != null) {
Intent intent = activity.getIntent();
if (intent != null) {
String target = intent.getStringExtra("target");
if (target != null && target.trim().length() > 0) {
promise.resolve(target);
return;
}
}
}
promise.resolve(null);
}
@ReactMethod
public void getDownloadDirectory(Promise promise) {
// This obtains a public default download directory after the storage permission has been granted
promise.resolve(Utils.getConfiguredDownloadDirectory(context));
}
@ReactMethod
public void getLbrynetDirectory(Promise promise) {
String path = String.format("%s/%s", Utils.getAppInternalStorageDir(context), "lbrynet");
promise.resolve(path);
}
@ReactMethod
public void getPlatform(Promise promise) {
String platform = String.format("Android %s (API %s)", Utils.getAndroidRelease(), Utils.getAndroidSdk());
promise.resolve(platform);
}
@ReactMethod
public void checkSdkReady() {
// check that the sdk ready when the service is already running so that we can send the ready event
ReactContext reactContext = (ReactContext) context;
if (MainActivity.lbrySdkReady && reactContext != null) {
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("onSdkReady", null);
}
}
@ReactMethod
public void log(String tag, String message) {
android.util.Log.d(tag, message);
}
}

View file

@ -1,37 +0,0 @@
package io.lbry.browser.reactmodules;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
public class VersionInfoModule extends ReactContextBaseJavaModule {
private Context context;
public VersionInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
this.context = reactContext;
}
@Override
public String getName() {
return "VersionInfo";
}
@ReactMethod
public void getAppVersion(final Promise promise) {
PackageManager packageManager = this.context.getPackageManager();
String packageName = this.context.getPackageName();
try {
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
promise.resolve(packageInfo.versionName);
} catch (PackageManager.NameNotFoundException e) {
// normally shouldn't happen
promise.resolve("Unknown");
}
}
}

Some files were not shown because too many files have changed in this diff Show more