initial native rewrite commit

This commit is contained in:
Akinwale Ariwodola 2020-04-28 14:25:39 +01:00
parent cc3055f1c9
commit cf052f9c80
639 changed files with 14299 additions and 8391 deletions

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 1402 versionCode 1500
versionName "0.14.2" versionName "0.15.0"
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,53 @@ 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'
} }
} }
} }
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 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.facebook.fresco:animated-base-support:1.3.0' implementation 'com.squareup.okhttp3:okhttp:4.4.1'
implementation 'com.facebook.fresco:animated-gif:1.10.0' implementation 'com.google.firebase:firebase-analytics:17.4.0'
implementation 'com.google.firebase:firebase-messaging:20.1.0' implementation 'com.google.android.gms:play-services-base:17.2.1'
implementation 'com.google.firebase:firebase-messaging:20.1.6'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.4'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.4'
implementation 'com.google.android.exoplayer:extension-cast:2.11.4'
implementation 'com.google.android:flexbox:2.0.1'
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.67.1-release__arm.aar') __32bitImplementation files('libs/lbrysdk-0.67.1-release__arm.aar')
__64bitImplementation files('libs/lbrysdk-0.67.1-release__arm64.aar') __64bitImplementation files('libs/lbrysdk-0.67.1-release__arm64.aar')
if (enableHermes) {
def hermesPath = "../../node_modules/hermes-engine/android/";
debugImplementation files(hermesPath + "hermes-debug.aar")
releaseImplementation files(hermesPath + "hermes-release.aar")
} else {
implementation jscFlavor
}
}
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
into 'libs'
} }
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,16 +1,8 @@
<?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
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" />
@ -18,68 +10,48 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <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:usesCleartextTraffic="true">
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_lbry" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/lbryGreen" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id"/>
<meta-data android:name="wakelock" android:value="0"/>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<activity android:name="io.lbry.browser.MainActivity"
android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/LbryAppTheme" android:requestLegacyExternalStorage="true"
android:configChanges="keyboardHidden|orientation|screenSize" android:roundIcon="@mipmap/ic_launcher_round"
android:screenOrientation="portrait" android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"
android:name=".MainActivity"
android:label="@string/app_name"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar"
android:launchMode="singleInstance" android:launchMode="singleInstance"
> android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lbry" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name="io.lbry.browser.receivers.NotificationDeletedReceiver" /> <activity
android:name=".FirstRunActivity"
<service android:launchMode="singleInstance"
android:name="io.lbry.browser.LbrynetMessagingService" android:parentActivityName=".MainActivity"
android:exported="false"> android:theme="@style/AppTheme.NoActionBarTranslucent" />
<intent-filter> <activity
<action android:name="com.google.firebase.MESSAGING_EVENT" /> android:name=".VerificationActivity"
</intent-filter> android:launchMode="singleInstance"
</service> android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBarTranslucent"
<provider android:windowSoftInputMode="adjustResize" />
android:name="io.lbry.browser.LocalFileProvider" <activity
android:authorities="io.lbry.browser.fileprovider" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout"
android:grantUriPermissions="true" android:name=".FileViewActivity"
android:exported="false"> android:launchMode="singleInstance"
<meta-data android:parentActivityName=".MainActivity"
android:name="android.support.FILE_PROVIDER_PATHS" android:supportsPictureInPicture="true"
android:resource="@xml/filepaths" /> android:theme="@style/AppTheme.NoActionBarBlack" />
</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,468 @@
package io.lbry.browser;
import android.app.PictureInPictureParams;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.widget.NestedScrollView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.google.android.flexbox.FlexboxLayoutManager;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.adapter.TagListAdapter;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.ClaimCacheKey;
import io.lbry.browser.model.File;
import io.lbry.browser.model.Tag;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.tasks.FileListTask;
import io.lbry.browser.tasks.LighthouseSearchTask;
import io.lbry.browser.tasks.ResolveTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class FileViewActivity extends AppCompatActivity {
public static FileViewActivity instance = null;
private static final int RELATED_CONTENT_SIZE = 16;
private SimpleExoPlayer player;
private boolean loadFilePending;
private boolean resolving;
private Claim claim;
private ClaimListAdapter relatedContentAdapter;
private File file;
private BroadcastReceiver sdkReadyReceiver;
private Player.EventListener fileViewPlayerListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String claimId = null;
String url = null;
Intent intent = getIntent();
if (intent != null) {
claimId = intent.getStringExtra("claimId");
url = intent.getStringExtra("url");
}
if (Helper.isNullOrEmpty(url)) {
// This activity should not be opened without a url set
finish();
return;
}
instance = this;
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(claimId);
if (url.contains("#")) {
key.setPermanentUrl(url); // use the same url for the key so that we can match the key for any value that's the same
key.setCanonicalUrl(url);
key.setShortUrl(url);
}
if (Lbry.claimCache.containsKey(key)) {
claim = Lbry.claimCache.get(key);
checkAndResetNowPlayingClaim();
file = claim.getFile();
if (file == null) {
loadFile();
}
}
setContentView(R.layout.activity_file_view);
if (claim == null) {
resolveUrl(url);
}
registerSdkReadyReceiver();
fileViewPlayerListener = new Player.EventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
/*if (playbackState == Player.STATE_READY) {
MainActivity.setNowPlayingClaim(claim, FileViewActivity.this);
}*/
}
};
initUi();
renderClaim();
}
private void checkAndResetNowPlayingClaim() {
if (MainActivity.nowPlayingClaim != null &&
!MainActivity.nowPlayingClaim.getClaimId().equalsIgnoreCase(claim.getClaimId())) {
MainActivity.clearNowPlayingClaim(this);
}
}
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
MainActivity.startingFileViewActivity = false;
if (intent != null) {
String newClaimId = intent.getStringExtra("claimId");
String newUrl = intent.getStringExtra("url");
String oldClaimId = claim != null ? claim.getClaimId() : null;
if (!Helper.isNullOrEmpty(newClaimId)) {
if (newClaimId.equalsIgnoreCase(oldClaimId)) {
// it's the same claim, so we do nothing
if (MainActivity.appPlayer != null) {
PlayerView view = findViewById(R.id.file_view_exoplayer_view);
view.setPlayer(null);
view.setPlayer(MainActivity.appPlayer);
}
return;
}
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(newClaimId);
if (!Helper.isNullOrEmpty(newUrl) && newUrl.contains("#")) {
key.setPermanentUrl(newUrl);
key.setCanonicalUrl(newUrl);
key.setShortUrl(newUrl);
}
if (Lbry.claimCache.containsKey(key)) {
claim = Lbry.claimCache.get(key);
checkAndResetNowPlayingClaim();
file = claim.getFile();
if (file == null) {
loadFile();
}
renderClaim();
} else {
findViewById(R.id.file_view_claim_display_area).setVisibility(View.INVISIBLE);
resolveUrl(newUrl);
}
}
}
}
private void registerSdkReadyReceiver() {
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
if (loadFilePending) {
loadFile();
}
}
};
registerReceiver(sdkReadyReceiver, filter);
}
private String getStreamingUrl() {
if (file != null && !Helper.isNullOrEmpty(file.getStreamingUrl())) {
return file.getStreamingUrl();
}
return buildLbryTvStreamingUrl();
}
private String buildLbryTvStreamingUrl() {
return String.format("https://player.lbry.tv/content/claims/%s/%s/stream", claim.getName(), claim.getClaimId());
}
private void loadFile() {
if (!Lbry.SDK_READY) {
// make use of the lbry.tv streaming URL
loadFilePending = true;
return;
}
loadFilePending = false;
// TODO: Check if it's paid content and then wait for the user to explicity request the file
String claimId = claim.getClaimId();
FileListTask task = new FileListTask(claimId, null, new FileListTask.FileListResultHandler() {
@Override
public void onSuccess(List<File> files) {
if (files.size() > 0) {
file = files.get(0);
claim.setFile(file);
}
}
@Override
public void onError(Exception error) {
}
});
}
protected void onResume() {
super.onResume();
MainActivity.startingFileViewActivity = false;
}
private void resolveUrl(String url) {
resolving = true;
View loadingView = findViewById(R.id.file_view_loading_container);
ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, loadingView, new ResolveTask.ResolveResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
if (claims.size() > 0) {
claim = claims.get(0);
checkAndResetNowPlayingClaim();
loadFile();
renderClaim();
}
}
@Override
public void onError(Exception error) {
resolving = false;
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void initUi() {
findViewById(R.id.file_view_title_area).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ImageView descIndicator = findViewById(R.id.file_view_desc_toggle_arrow);
View descriptionArea = findViewById(R.id.file_view_description_area);
if (descriptionArea.getVisibility() != View.VISIBLE) {
descriptionArea.setVisibility(View.VISIBLE);
descIndicator.setImageResource(R.drawable.ic_arrow_dropup);
} else {
descriptionArea.setVisibility(View.GONE);
descIndicator.setImageResource(R.drawable.ic_arrow_dropdown);
}
}
});
RecyclerView relatedContentList = findViewById(R.id.file_view_related_content_list);
relatedContentList.setNestedScrollingEnabled(false);
LinearLayoutManager llm = new LinearLayoutManager(this);
relatedContentList.setLayoutManager(llm);
}
private void renderClaim() {
if (claim == null) {
return;
}
((NestedScrollView) findViewById(R.id.file_view_scroll_view)).scrollTo(0, 0);
findViewById(R.id.file_view_claim_display_area).setVisibility(View.VISIBLE);
ImageView descIndicator = findViewById(R.id.file_view_desc_toggle_arrow);
descIndicator.setImageResource(R.drawable.ic_arrow_dropdown);
findViewById(R.id.file_view_description_area).setVisibility(View.GONE);
((TextView) findViewById(R.id.file_view_title)).setText(claim.getTitle());
((TextView) findViewById(R.id.file_view_description)).setText(claim.getDescription());
((TextView) findViewById(R.id.file_view_publisher_name)).setText(
Helper.isNullOrEmpty(claim.getPublisherName()) ? getString(R.string.anonymous) : claim.getPublisherName());
RecyclerView descTagsList = findViewById(R.id.file_view_tag_list);
FlexboxLayoutManager flm = new FlexboxLayoutManager(this);
descTagsList.setLayoutManager(flm);
List<Tag> tags = claim.getTagObjects();
TagListAdapter tagListAdapter = new TagListAdapter(tags, this);
tagListAdapter.setClickListener(new TagListAdapter.TagClickListener() {
@Override
public void onTagClicked(Tag tag) {
Intent intent = new Intent(MainActivity.ACTION_OPEN_ALL_CONTENT_TAG);
intent.putExtra("tag", tag.getName());
sendBroadcast(intent);
moveTaskToBack(true);
}
});
descTagsList.setAdapter(tagListAdapter);
findViewById(R.id.file_view_tag_area).setVisibility(tags.size() > 0 ? View.VISIBLE : View.GONE);
Claim.GenericMetadata metadata = claim.getValue();
if (metadata instanceof Claim.StreamMetadata) {
Claim.StreamMetadata streamMetadata = (Claim.StreamMetadata) metadata;
long publishTime = streamMetadata.getReleaseTime() > 0 ? streamMetadata.getReleaseTime() * 1000 : claim.getTimestamp() * 1000;
((TextView) findViewById(R.id.file_view_publish_time)).setText(DateUtils.getRelativeTimeSpanString(
publishTime, System.currentTimeMillis(), 0, DateUtils.FORMAT_ABBREV_RELATIVE));
// Check the metadata type
String mediaType = streamMetadata.getSource().getMediaType();
// Use Exoplayer view if it's video / audio
if (mediaType.startsWith("audio") || mediaType.startsWith("video")) {
showExoplayerView();
playMedia();
} else if (mediaType.startsWith("text")) {
} else if (mediaType.startsWith("image")) {
} else {
// unsupported type
showUnsupportedView();
}
}
loadRelatedContent();
}
private void showUnsupportedView() {
findViewById(R.id.file_view_exoplayer_container).setVisibility(View.GONE);
findViewById(R.id.file_view_unsupported_container).setVisibility(View.VISIBLE);
}
private void showExoplayerView() {
findViewById(R.id.file_view_unsupported_container).setVisibility(View.GONE);
findViewById(R.id.file_view_exoplayer_container).setVisibility(View.VISIBLE);
}
private void playMedia() {
boolean newPlayerCreated = false;
if (MainActivity.appPlayer == null) {
MainActivity.appPlayer = new SimpleExoPlayer.Builder(this).build();
MainActivity.appPlayer.setPlayWhenReady(true);
MainActivity.appPlayer.addListener(fileViewPlayerListener);
newPlayerCreated = true;
}
PlayerView view = findViewById(R.id.file_view_exoplayer_view);
PlayerControlView controlView = findViewById(R.id.file_view_exoplayer_control_view);
view.setPlayer(MainActivity.appPlayer);
controlView.setPlayer(MainActivity.appPlayer);
if (MainActivity.nowPlayingClaim != null &&
MainActivity.nowPlayingClaim.getClaimId().equalsIgnoreCase(claim.getClaimId()) &&
!newPlayerCreated) {
// if the claim is already playing, we don't need to reload the media source
return;
}
MainActivity.setNowPlayingClaim(claim, FileViewActivity.this);
String userAgent = Util.getUserAgent(this, getString(R.string.app_name));
MediaSource mediaSource = new ProgressiveMediaSource.Factory(
new DefaultDataSourceFactory(this, userAgent),
new DefaultExtractorsFactory()
).createMediaSource(Uri.parse(getStreamingUrl()));
MainActivity.appPlayer.prepare(mediaSource, true, true);
}
private void loadRelatedContent() {
// reset the list view
((RecyclerView) findViewById(R.id.file_view_related_content_list)).setAdapter(null);
String title = claim.getTitle();
String claimId = claim.getClaimId();
ProgressBar relatedLoading = findViewById(R.id.file_view_related_content_progress);
LighthouseSearchTask relatedTask = new LighthouseSearchTask(title, RELATED_CONTENT_SIZE, 0, false, claimId, relatedLoading, new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
List<Claim> filteredClaims = new ArrayList<>();
for (Claim c : claims) {
if (!c.getClaimId().equalsIgnoreCase(claim.getClaimId())) {
filteredClaims.add(c);
}
}
relatedContentAdapter = new ClaimListAdapter(filteredClaims, FileViewActivity.this);
relatedContentAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override
public void onClaimClicked(Claim claim) {
Intent intent = new Intent(FileViewActivity.this, FileViewActivity.class);
intent.putExtra("claimId", claim.getClaimId());
intent.putExtra("url", claim.getPermanentUrl());
MainActivity.startingFileViewActivity = true;
startActivity(intent);
}
});
RecyclerView relatedContentList = findViewById(R.id.file_view_related_content_list);
relatedContentList.setAdapter(relatedContentAdapter);
relatedContentAdapter.notifyDataSetChanged();
}
@Override
public void onError(Exception error) {
}
});
relatedTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void onBackPressed() {
MainActivity.mainActive = true;
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
protected void onUserLeaveHint() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !MainActivity.mainActive) {
PictureInPictureParams params = new PictureInPictureParams.Builder().build();
enterPictureInPictureMode(params);
}
}
protected void onStop() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (isInPictureInPictureMode() && MainActivity.appPlayer != null) {
MainActivity.appPlayer.setPlayWhenReady(false);
}
}
super.onStop();
}
protected void onDestroy() {
Helper.unregisterReceiver(sdkReadyReceiver, this);
if (MainActivity.appPlayer != null && fileViewPlayerListener != null) {
MainActivity.appPlayer.removeListener(fileViewPlayerListener);
}
instance = null;
super.onDestroy();
}
private void renderPictureInPictureMode() {
findViewById(R.id.file_view_scroll_view).setVisibility(View.GONE);
findViewById(R.id.file_view_exoplayer_control_view).setVisibility(View.GONE);
}
private void renderFullMode() {
findViewById(R.id.file_view_scroll_view).setVisibility(View.VISIBLE);
PlayerControlView controlView = findViewById(R.id.file_view_exoplayer_control_view);
controlView.setPlayer(null);
controlView.setPlayer(MainActivity.appPlayer);
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
if (isInPictureInPictureMode) {
renderPictureInPictureMode();
} else {
renderFullMode();
}
}
}

View file

@ -0,0 +1,128 @@
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.Html;
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.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();
}
}
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
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_FIRST_RUN_COMPLETED, true).apply();
// first_run_completed event
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

@ -1,189 +0,0 @@
package io.lbry.browser;
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.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import android.util.Log;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import io.lbry.lbrysdk.LbrynetService;
import io.lbry.browser.reactmodules.UtilityModule;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LbrynetMessagingService extends FirebaseMessagingService {
private static final String TAG = "LbrynetMessagingService";
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.LBRY_ENGAGEMENT_CHANNEL";
private static final String TYPE_SUBSCRIPTION = "subscription";
private static final String TYPE_REWARD = "reward";
private static final String TYPE_INTERESTS = "interests";
private static final String TYPE_CREATOR = "creator";
private FirebaseAnalytics firebaseAnalytics;
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
Log.d(TAG, "From: " + remoteMessage.getFrom());
if (firebaseAnalytics == null) {
firebaseAnalytics = FirebaseAnalytics.getInstance(this);
}
Map<String, String> payload = remoteMessage.getData();
if (payload != null) {
String type = payload.get("type");
String url = payload.get("target");
String title = payload.get("title");
String body = payload.get("body");
String name = payload.get("name"); // notification name
String contentTitle = payload.get("content_title");
String channelUrl = payload.get("channel_url");
//String publishTime = payload.get("publish_time");
String publishTime = null;
if (type != null && getEnabledTypes().indexOf(type) > -1 && body != null && body.trim().length() > 0) {
// only log the receive event for valid notifications received
if (firebaseAnalytics != null) {
Bundle bundle = new Bundle();
bundle.putString("name", name);
firebaseAnalytics.logEvent("lbry_notification_receive", bundle);
}
sendNotification(title, body, type, url, name, contentTitle, channelUrl, publishTime);
}
}
}
@Override
public void onNewToken(String token) {
Log.d(TAG, "Refreshed token: " + token);
// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
// Instance ID token to your app server.
sendRegistrationToServer(token);
}
/**
* Persist token to third-party servers.
*
* Modify this method to associate the user's FCM InstanceID token with any server-side account
* maintained by your application.
*
* @param token The new token.
*/
private void sendRegistrationToServer(String token) {
// TODO: Implement this method to send token to your app server.
}
/**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
*/
private void sendNotification(String title, String messageBody, String type, String url, String name,
String contentTitle, String channelUrl, String publishTime) {
//Intent intent = new Intent(this, MainActivity.class);
//intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (url == null) {
if (TYPE_REWARD.equals(type)) {
url = "lbry://?rewards";
} else {
// default to home page
url = "lbry://?discover";
}
} else {
if (!MainActivity.isServiceRunning(this, LbrynetService.class) &&
contentTitle != null &&
channelUrl != null &&
!url.startsWith("lbry://?") /* not a special url */
) {
// only enter lite mode when contentTitle and channelUrl are set (and the service isn't running yet)
// cold start
url = url + ((url.indexOf("?") > -1) ? "&liteMode=1" : "?liteMode=1");
try {
if (contentTitle != null) {
url = url + "&contentTitle=" + URLEncoder.encode(contentTitle, "UTF-8");
}
if (channelUrl != null) {
url = url + "&channelUrl=" + URLEncoder.encode(channelUrl, "UTF-8");
}
if (publishTime != null) {
url = url + "&publishTime=" + URLEncoder.encode(publishTime, "UTF-8");
}
} catch (UnsupportedEncodingException ex) {
// shouldn't happen
}
}
}
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
launchIntent.putExtra("notification_name", name);
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, launchIntent, PendingIntent.FLAG_ONE_SHOT);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setColor(ContextCompat.getColor(this, R.color.lbryGreen))
.setSmallIcon(R.drawable.ic_lbry)
.setContentTitle(title)
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
NOTIFICATION_CHANNEL_ID, "LBRY Engagement", NotificationManager.IMPORTANCE_DEFAULT);
notificationManager.createNotificationChannel(channel);
}
notificationManager.notify(9898, notificationBuilder.build());
}
public List<String> getEnabledTypes() {
SharedPreferences sp = getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
List<String> enabledTypes = new ArrayList<String>();
if (sp.getBoolean(UtilityModule.RECEIVE_SUBSCRIPTION_NOTIFICATIONS, true)) {
enabledTypes.add(TYPE_SUBSCRIPTION);
}
if (sp.getBoolean(UtilityModule.RECEIVE_REWARD_NOTIFICATIONS, true)) {
enabledTypes.add(TYPE_REWARD);
}
if (sp.getBoolean(UtilityModule.RECEIVE_INTERESTS_NOTIFICATIONS, true)) {
enabledTypes.add(TYPE_INTERESTS);
}
if (sp.getBoolean(UtilityModule.RECEIVE_CREATOR_NOTIFICATIONS, true)) {
enabledTypes.add(TYPE_CREATOR);
}
return enabledTypes;
}
}

View file

@ -1,7 +0,0 @@
package io.lbry.browser;
import androidx.core.content.FileProvider;
public class LocalFileProvider extends FileProvider {
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,152 @@
package io.lbry.browser;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager.widget.ViewPager;
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.FetchCurrentUserTask;
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));
if (Lbryio.isSignedIn() && flow == VERIFICATION_FLOW_WALLET) {
viewPager.setCurrentItem(1);
}
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();
}
});
}
@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);
}
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));
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
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) {
setResult(RESULT_CANCELED);
finish();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
// change pager view depending on flow
FetchCurrentUserTask task = new FetchCurrentUserTask(new FetchCurrentUserTask.FetchUserTaskHandler() {
@Override
public void onSuccess(User user) { Lbryio.currentUser = user; }
@Override
public void onError(Exception error) { }
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
ViewPager2 viewPager = findViewById(R.id.verification_pager);
// for rewards, (show phone verification if not done, or manual verification if required)
// for wallet sync, if password unlock is required, show password entry page
viewPager.setCurrentItem(1);
}
}
@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,119 @@
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
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 TextView allTextView;
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);
allTextView = v.findViewById(R.id.channel_filter_all);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public boolean isClaimSelected(Claim claim) {
return claim.equals(selectedItem);
}
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.GONE : View.VISIBLE);
vh.allTextView.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().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,217 @@
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.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.utils.Helper;
import io.lbry.browser.utils.LbryUri;
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 Context context;
private List<Claim> items;
private List<Claim> selectedItems;
@Setter
private ClaimListItemListener listener;
public ClaimListAdapter(List<Claim> items, Context context) {
this.context = context;
this.items = new ArrayList<>(items);
this.selectedItems = new ArrayList<>();
}
public List<Claim> getSelectedItems() {
return this.selectedItems;
}
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 clearItems() {
clearSelectedItems();
this.items.clear();
notifyDataSetChanged();
}
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);
}
}
notifyDataSetChanged();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected ImageView thumbnailView;
protected View noThumbnailView;
protected TextView alphaView;
protected TextView vanityUrlView;
protected TextView durationView;
protected TextView titleView;
protected TextView publisherView;
protected TextView publishTimeView;
public ViewHolder(View v) {
super(v);
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);
}
}
@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;
}
return Claim.TYPE_CHANNEL.equalsIgnoreCase(items.get(position).getValueType()) ? VIEW_TYPE_CHANNEL : VIEW_TYPE_STREAM;
}
@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);
}
@Override
public void onBindViewHolder(ClaimListAdapter.ViewHolder vh, int position) {
int type = getItemViewType(position);
Claim item = items.get(position);
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());
}
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onClaimClicked(item);
}
}
});
vh.publisherView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null && signingChannel != null) {
listener.onClaimClicked(signingChannel);
}
}
});
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.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.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));
} 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));
}
}
}
public interface ClaimListItemListener {
void onClaimClicked(Claim claim);
}
}

View file

@ -0,0 +1,105 @@
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 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 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);
vh.titleView.setText(item.getTitle());
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,120 @@
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 ImageView thumbnailView;
protected TextView alphaView;
protected TextView titleView;
protected TextView tagView;
public ViewHolder(View v) {
super(v);
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 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();
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
vh.alphaView.setVisibility(View.GONE);
vh.thumbnailView.setVisibility(View.VISIBLE);
Glide.with(context.getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(vh.thumbnailView);
} else {
vh.alphaView.setVisibility(View.VISIBLE);
vh.thumbnailView.setVisibility(View.GONE);
}
vh.alphaView.setText(claim.getFirstCharacter());
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,74 @@
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.model.Tag;
import lombok.Setter;
public class TagListAdapter extends RecyclerView.Adapter<TagListAdapter.ViewHolder> {
private Context context;
private List<Tag> items;
@Setter
private TagClickListener clickListener;
public TagListAdapter(List<Tag> tags, Context context) {
this.context = context;
this.items = new ArrayList<>(tags);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
protected TextView nameView;
public ViewHolder(View v) {
super(v);
nameView = v.findViewById(R.id.tag_name);
}
}
public int getItemCount() {
return items != null ? items.size() : 0;
}
public void addTags(List<Tag> tags) {
for (Tag tag : tags) {
if (!items.contains(tag)) {
items.add(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.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);
}
}
});
}
public interface TagClickListener {
void onTagClicked(Tag tag);
}
}

View file

@ -0,0 +1,126 @@
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.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.Transaction;
import io.lbry.browser.model.UrlSuggestion;
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 String EXPLORER_TX_PREFIX = "https://explorer.lbry.com/tx";
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, 8));
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.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", 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);
}
}

View file

@ -0,0 +1,138 @@
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.model.Claim;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.ui.controls.SolidIconView;
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();
if (thisUrl != null && thisUrl.equals(url)) {
items.get(i).setClaim(claim);
}
}
}
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() :
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() :
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,52 @@
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.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 {
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:
WalletVerificationFragment wvFragment = WalletVerificationFragment.class.newInstance();
if (activity instanceof WalletSyncListener) {
wvFragment.setListener((WalletSyncListener) activity);
}
return wvFragment;
}
}
@Override
public int getItemCount() {
return 2;
}
}

View file

@ -0,0 +1,74 @@
package io.lbry.browser.data;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
public class DatabaseHelper extends SQLiteOpenHelper {
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "LbryApp.db";
private static final String[] SQL_CREATE_TABLES = {
// local subscription store
"CREATE TABLE subscriptions (url TEXT PRIMARY KEY NOT NULL, channel_name TEXT NOT NULL)",
// local claim cache store for quick load / refresh
};
private static final String[] SQL_CREATE_INDEXES = {
"CREATE UNIQUE INDEX idx_subscription_url ON subscriptions (url)"
};
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";
public DatabaseHelper(Context context) {
super(context, String.format("%s/%s", context.getFilesDir().getAbsolutePath(), DATABASE_NAME), null, DATABASE_VERSION);
}
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) {
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
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,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,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,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 SdkStatusListener {
void onSdkReady();
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.listener;
public interface SignInListener {
void onEmailAdded(String email);
void onEmailEdit();
void onEmailVerified();
}

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,338 @@
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.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 lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@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 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 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
private Claim signingChannel;
private String repostChannelUrl;
private boolean isChannelSignatureValid;
private GenericMetadata value;
private File file; // associated file if it exists
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 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 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 fromJSONObject(JSONObject claimObject) {
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 claim = gson.fromJson(claimJson, type);
// Specific value parsing
try {
JSONObject value = claimObject.getJSONObject("value");
String valueType = claim.getValueType();
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"));
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(feeAmount));
}
((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"));
claimUri.setChannelClaimId(signingChannel.getClaimId());
claimUri.setChannelName(signingChannel.getName());
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,86 @@
package io.lbry.browser.model;
import androidx.annotation.Nullable;
import io.lbry.browser.utils.Helper;
import lombok.Getter;
import lombok.Setter;
/**
* Class to represent a key to check equality with another object
*/
public class ClaimCacheKey {
@Getter
@Setter
private String claimId;
@Getter
@Setter
private String canonicalUrl;
@Getter
@Setter
private String permanentUrl;
@Getter
@Setter
private String shortUrl;
@Getter
@Setter
private String vanityUrl;
public static ClaimCacheKey fromClaim(Claim claim) {
ClaimCacheKey key = new ClaimCacheKey();
key.setClaimId(claim.getClaimId());
key.setCanonicalUrl(claim.getCanonicalUrl());
key.setPermanentUrl(claim.getPermanentUrl());
key.setShortUrl(claim.getShortUrl());
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(permanentUrl) && !Helper.isNullOrEmpty(key.getPermanentUrl())) {
return permanentUrl.equalsIgnoreCase(key.getPermanentUrl());
}
if (!Helper.isNullOrEmpty(canonicalUrl) && !Helper.isNullOrEmpty(key.getCanonicalUrl())) {
return canonicalUrl.equalsIgnoreCase(key.getCanonicalUrl());
}
if (!Helper.isNullOrEmpty(shortUrl) && !Helper.isNullOrEmpty(key.getShortUrl())) {
return shortUrl.equalsIgnoreCase(key.getShortUrl());
}
if (!Helper.isNullOrEmpty(vanityUrl) && !Helper.isNullOrEmpty(key.getVanityUrl())) {
return vanityUrl.equalsIgnoreCase(key.getVanityUrl());
}
return false;
}
@Override
public int hashCode() {
if (!Helper.isNullOrEmpty(claimId)) {
return claimId.hashCode();
}
if (!Helper.isNullOrEmpty(permanentUrl)) {
return permanentUrl.hashCode();
}
if (!Helper.isNullOrEmpty(canonicalUrl)) {
return canonicalUrl.hashCode();
}
if (!Helper.isNullOrEmpty(shortUrl)) {
return shortUrl.hashCode();
}
if (!Helper.isNullOrEmpty(vanityUrl)) {
return vanityUrl.hashCode();
}
return super.hashCode();
}
}

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,54 @@
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 lombok.Data;
@Data
public class File {
private Claim.StreamMetadata metadata;
private long addedOn;
private int blobsCompleted;
private int blobsInStream;
private int blobsRemaining;
private String channelClaimId;
private String channelName;
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 totalBytes;
private long totalBytesLowerBound;
private String txid;
private long writtenBytes;
public static File fromJSONObject(JSONObject fileObject) {
String fileJson = fileObject.toString();
Type type = new TypeToken<File>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
File file = gson.fromJson(fileJson, type);
return file;
}
}

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_YOUR_TAGS = 103;
public static final int ID_ITEM_ALL_CONTENT = 104;
// 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 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,30 @@
package io.lbry.browser.model;
import io.lbry.browser.utils.Helper;
import lombok.Getter;
import lombok.Setter;
public class Tag {
@Getter
@Setter
private String name;
public Tag(String name) {
this.name = name;
}
public String getLowercaseName() {
return name.toLowerCase();
}
public boolean isMature() {
return Helper.MATURE_TAG_NAMES.contains(name.toLowerCase());
}
public boolean equals(Object o) {
return (o instanceof Tag) && ((Tag) o).getName().equalsIgnoreCase(name);
}
public int hashCode() {
return name.hashCode();
}
}

View file

@ -0,0 +1,92 @@
package io.lbry.browser.model;
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.utils.Helper;
import lombok.Data;
@Data
public class Transaction {
private int confirmations;
private Date txDate;
private String date;
private String claim;
private String txid;
private BigDecimal value;
private BigDecimal fee;
private long timestamp;
private int descriptionStringId;
private TransactionInfo abandonInfo;
private TransactionInfo claimInfo;
private TransactionInfo supportInfo;
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")) {
info = TransactionInfo.fromJSONObject(jsonObject.getJSONObject("abandon_info"));
descStringId = R.string.abandon;
transaction.setAbandonInfo(info);
} else if (jsonObject.has("claim_info")) {
info = TransactionInfo.fromJSONObject(jsonObject.getJSONObject("claim_info"));
descStringId = info.getClaimName().startsWith("@") ? R.string.channel : R.string.publish;
transaction.setClaimInfo(info);
} else if (jsonObject.has("support_info")) {
info = TransactionInfo.fromJSONObject(jsonObject.getJSONObject("support_info"));
descStringId = info.isTip() ? R.string.tip : R.string.support;
transaction.setSupportInfo(info);
}
if (info != null) {
transaction.setClaim(info.getClaimName());
}
} catch (JSONException ex) {
// pass
}
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 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.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,35 @@
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
public UrlSuggestion(int type, String text) {
this.type = type;
this.text = text;
}
public String getTitle() {
switch (type) {
case TYPE_CHANNEL:
return String.format("%s - %s", text.startsWith("@") ? text.substring(1) : text, uri.toString());
case TYPE_FILE:
return String.format("%s - %s", text, uri.toString());
case TYPE_TAG:
return String.format("%s - #%s", text, text);
}
return text;
}
}

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,16 @@
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, boolean changed) {
this.hash = hash;
this.data = data;
this.changed = changed;
}
}

View file

@ -0,0 +1,17 @@
package io.lbry.browser.model.lbryinc;
import lombok.Data;
@Data
public class Subscription {
private String channelName;
private String url;
public Subscription() {
}
public Subscription(String channelName, String url) {
this.channelName = channelName;
this.url = url;
}
}

View file

@ -0,0 +1,29 @@
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<String> youtubeChannels;
private List<String> deviceTypes;
}

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

View file

@ -1,46 +0,0 @@
package io.lbry.browser.reactpackages;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import io.lbry.browser.reactmodules.BackgroundMediaModule;
import io.lbry.browser.reactmodules.DaemonServiceControlModule;
import io.lbry.browser.reactmodules.FirstRunModule;
import io.lbry.browser.reactmodules.FirebaseModule;
import io.lbry.browser.reactmodules.GalleryModule;
import io.lbry.browser.reactmodules.RequestsModule;
import io.lbry.browser.reactmodules.ScreenOrientationModule;
import io.lbry.browser.reactmodules.StatePersistorModule;
import io.lbry.browser.reactmodules.VersionInfoModule;
import io.lbry.browser.reactmodules.UtilityModule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class LbryReactPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new BackgroundMediaModule(reactContext));
modules.add(new DaemonServiceControlModule(reactContext));
modules.add(new FirstRunModule(reactContext));
modules.add(new FirebaseModule(reactContext));
modules.add(new GalleryModule(reactContext));
modules.add(new RequestsModule(reactContext));
modules.add(new ScreenOrientationModule(reactContext));
modules.add(new StatePersistorModule(reactContext));
modules.add(new UtilityModule(reactContext));
modules.add(new VersionInfoModule(reactContext));
return modules;
}
}

View file

@ -1,17 +0,0 @@
package io.lbry.browser.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import io.lbry.browser.DownloadManager;
public class NotificationDeletedReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int notificationId = intent.getExtras().getInt(DownloadManager.NOTIFICATION_ID_KEY);
if (DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID == notificationId) {
DownloadManager.groupCreated = false;
}
}
}

View file

@ -0,0 +1,81 @@
package io.lbry.browser.tasks;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.AsyncTask;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class ChannelSubscribeTask extends AsyncTask<Void, Void, Boolean> {
private Context context;
private String channelClaimId;
private Subscription subscription;
private ChannelSubscribeHandler handler;
private Exception error;
private boolean isUnsubscribing;
public ChannelSubscribeTask(Context context, String channelClaimId, Subscription subscription, boolean isUnsubscribing, ChannelSubscribeHandler handler) {
this.context = context;
this.channelClaimId = channelClaimId;
this.subscription = subscription;
this.handler = handler;
this.isUnsubscribing = isUnsubscribing;
}
protected Boolean doInBackground(Void... params) {
SQLiteDatabase db = null;
try {
// Save to (or delete from) local store
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getWritableDatabase();
}
if (db != null) {
if (!isUnsubscribing) {
DatabaseHelper.createOrUpdateSubscription(subscription, db);
} else {
DatabaseHelper.deleteSubscription(subscription, db);
}
}
// Save with Lbryio
Map<String, String> options = new HashMap<>();
options.put("claim_id", channelClaimId);
if (!isUnsubscribing) {
options.put("channel_name", subscription.getChannelName());
}
String action = isUnsubscribing ? "delete" : "new";
Lbryio.call("subscription", action, options, context);
} catch (LbryioRequestException | LbryioResponseException | SQLiteException ex) {
error = ex;
return false;
} finally {
Helper.closeDatabase(db);
}
return true;
}
protected void onPostExecute(Boolean success) {
if (handler != null) {
if (success) {
handler.onSuccess();
} else {
handler.onError(error);
}
}
}
public interface ChannelSubscribeHandler {
void onSuccess();
void onError(Exception exception);
}
}

View file

@ -0,0 +1,54 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ProgressBar;
import java.util.List;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class ClaimSearchTask extends AsyncTask<Void, Void, List<Claim>> {
private Map<String, Object> options;
private String connectionString;
private ClaimSearchResultHandler handler;
private View progressView;
private ApiCallException error;
public ClaimSearchTask(Map<String, Object> options, String connectionString, View progressView, ClaimSearchResultHandler handler) {
this.options = options;
this.connectionString = connectionString;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected List<Claim> doInBackground(Void... params) {
try {
return Lbry.claimSearch(options, connectionString);
} catch (ApiCallException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(List<Claim> claims) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (claims != null) {
handler.onSuccess(claims, claims.size() < Helper.parseInt(options.get("page_size"), 0));
} else {
handler.onError(error);
}
}
}
public interface ClaimSearchResultHandler {
void onSuccess(List<Claim> claims, boolean hasReachedEnd);
void onError(Exception error);
}
}

View file

@ -0,0 +1,38 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import io.lbry.browser.model.lbryinc.User;
import io.lbry.browser.utils.Lbryio;
public class FetchCurrentUserTask extends AsyncTask<Void, Void, User> {
private Exception error;
private FetchUserTaskHandler handler;
public FetchCurrentUserTask(FetchUserTaskHandler handler) {
this.handler = handler;
}
protected User doInBackground(Void... params) {
try {
return Lbryio.fetchCurrentUser(null);
} catch (Exception ex) {
error = ex;
return null;
}
}
protected void onPostExecute(User result) {
if (handler != null) {
if (result != null) {
handler.onSuccess(result);
} else {
handler.onError(error);
}
}
}
public interface FetchUserTaskHandler {
void onSuccess(User user);
void onError(Exception error);
}
}

View file

@ -0,0 +1,88 @@
package io.lbry.browser.tasks;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ProgressBar;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
import io.lbry.browser.MainActivity;
import io.lbry.browser.data.DatabaseHelper;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
public class FetchSubscriptionsTask extends AsyncTask<Void, Void, List<Subscription>> {
private Context context;
private FetchSubscriptionsHandler handler;
private ProgressBar progressBar;
private Exception error;
public FetchSubscriptionsTask(Context context, ProgressBar progressBar, FetchSubscriptionsHandler handler) {
this.context = context;
this.progressBar = progressBar;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressBar, View.VISIBLE);
}
protected List<Subscription> doInBackground(Void... params) {
List<Subscription> subscriptions = new ArrayList<>();
SQLiteDatabase db = null;
try {
JSONArray array = (JSONArray) Lbryio.parseResponse(Lbryio.call("subscription", "list", context));
if (context instanceof MainActivity) {
db = ((MainActivity) context).getDbHelper().getWritableDatabase();
}
if (array != null) {
for (int i = 0; i < array.length(); i++) {
JSONObject item = array.getJSONObject(i);
String claimId = item.getString("claim_id");
String channelName = item.getString("channel_name");
LbryUri url = new LbryUri();
url.setChannelName(channelName);
url.setClaimId(claimId);
Subscription subscription = new Subscription(channelName, url.toString());
subscriptions.add(subscription);
// Persist the subscription locally if it doesn't exist
if (db != null) {
DatabaseHelper.createOrUpdateSubscription(subscription, db);
}
}
}
} catch (LbryioRequestException | LbryioResponseException | JSONException | ClassCastException ex) {
error = ex;
return null;
} finally {
Helper.closeDatabase(db);
}
return subscriptions;
}
protected void onPostExecute(List<Subscription> subscriptions) {
Helper.setViewVisibility(progressBar, View.GONE);
if (handler != null) {
if (subscriptions != null) {
handler.onSuccess(subscriptions);
} else {
handler.onError(error);
}
}
}
public interface FetchSubscriptionsHandler {
void onSuccess(List<Subscription> subscriptions);
void onError(Exception exception);
}
}

View file

@ -0,0 +1,54 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.view.View;
import java.util.List;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.File;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class FileListTask extends AsyncTask<Void, Void, List<File>> {
private String claimId;
private FileListResultHandler handler;
private View progressView;
private ApiCallException error;
public FileListTask(View progressView, FileListResultHandler handler) {
this(null, progressView, handler);
}
public FileListTask(String claimId, View progressView, FileListResultHandler handler) {
this.claimId = claimId;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected List<File> doInBackground(Void... params) {
try {
return Lbry.fileList(claimId);
} catch (ApiCallException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(List<File> files) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (files != null) {
handler.onSuccess(files);
} else {
handler.onError(error);
}
}
}
public interface FileListResultHandler {
void onSuccess(List<File> files);
void onError(Exception error);
}
}

View file

@ -0,0 +1,7 @@
package io.lbry.browser.tasks;
public interface GenericTaskHandler {
void beforeStart();
void onSuccess();
void onError(Exception error);
}

View file

@ -0,0 +1,51 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.view.View;
import java.util.List;
import io.lbry.browser.exceptions.LbryRequestException;
import io.lbry.browser.exceptions.LbryResponseException;
import io.lbry.browser.model.UrlSuggestion;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lighthouse;
public class LighthouseAutoCompleteTask extends AsyncTask<Void, Void, List<UrlSuggestion>> {
private String text;
private AutoCompleteResultHandler handler;
private View progressView;
private Exception error;
public LighthouseAutoCompleteTask(String text, View progressView, AutoCompleteResultHandler handler) {
this.text = text;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected List<UrlSuggestion> doInBackground(Void... params) {
try {
return Lighthouse.autocomplete(text);
} catch (LbryRequestException | LbryResponseException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(List<UrlSuggestion> suggestions) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (suggestions != null) {
handler.onSuccess(suggestions);
} else {
handler.onError(error);
}
}
}
public interface AutoCompleteResultHandler {
void onSuccess(List<UrlSuggestion> suggestions);
void onError(Exception error);
}
}

View file

@ -0,0 +1,55 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ProgressBar;
import java.util.List;
import io.lbry.browser.exceptions.LbryRequestException;
import io.lbry.browser.exceptions.LbryResponseException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lighthouse;
public class LighthouseSearchTask extends AsyncTask<Void, Void, List<Claim>> {
private String rawQuery;
private int size;
private int from;
private boolean nsfw;
private String relatedTo;
private ClaimSearchTask.ClaimSearchResultHandler handler;
private ProgressBar progressBar;
private Exception error;
public LighthouseSearchTask(String rawQuery, int size, int from, boolean nsfw, String relatedTo, ProgressBar progressBar, ClaimSearchTask.ClaimSearchResultHandler handler) {
this.rawQuery = rawQuery;
this.size = size;
this.from = from;
this.nsfw = nsfw;
this.relatedTo = relatedTo;
this.progressBar = progressBar;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressBar, View.VISIBLE);
}
protected List<Claim> doInBackground(Void... params) {
try {
return Lighthouse.search(rawQuery, size, from, nsfw, relatedTo);
} catch (LbryRequestException | LbryResponseException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(List<Claim> claims) {
Helper.setViewVisibility(progressBar, View.GONE);
if (handler != null) {
if (claims != null) {
handler.onSuccess(claims, claims.size() < size);
} else {
handler.onError(error);
}
}
}
}

View file

@ -0,0 +1,57 @@
package io.lbry.browser.tasks;
import android.os.AsyncTask;
import android.view.View;
import java.util.Arrays;
import java.util.List;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class ResolveTask extends AsyncTask<Void, Void, List<Claim>> {
private List<String> urls;
private String connectionString;
private ResolveResultHandler handler;
private View progressView;
private ApiCallException error;
public ResolveTask(String url, String connectionString, View progressView, ResolveResultHandler handler) {
this(Arrays.asList(url), connectionString, progressView, handler);
}
public ResolveTask(List<String> urls, String connectionString, View progressView, ResolveResultHandler handler) {
this.urls = urls;
this.connectionString = connectionString;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected List<Claim> doInBackground(Void... params) {
try {
return Lbry.resolve(urls, connectionString);
} catch (ApiCallException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(List<Claim> claims) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (claims != null) {
handler.onSuccess(claims);
} else {
handler.onError(error);
}
}
}
public interface ResolveResultHandler {
void onSuccess(List<Claim> claims);
void onError(Exception error);
}
}

View file

@ -0,0 +1,30 @@
package io.lbry.browser.tasks.verification;
import android.os.AsyncTask;
import io.lbry.browser.model.lbryinc.User;
import io.lbry.browser.utils.Lbryio;
public class CheckUserEmailVerifiedTask extends AsyncTask<Void, Void, Boolean> {
private CheckUserEmailVerifiedHandler handler;
public CheckUserEmailVerifiedTask(CheckUserEmailVerifiedHandler handler) {
this.handler = handler;
}
protected Boolean doInBackground(Void... params) {
User user = Lbryio.fetchCurrentUser(null);
return user != null && user.isHasVerifiedEmail();
}
protected void onPostExecute(Boolean result) {
if (handler != null && result) {
// we only care if the user has actually verified their email
handler.onUserEmailVerified();
}
}
public interface CheckUserEmailVerifiedHandler {
void onUserEmailVerified();
}
}

View file

@ -0,0 +1,81 @@
package io.lbry.browser.tasks.verification;
import android.os.AsyncTask;
import android.view.View;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class EmailNewTask extends AsyncTask<Void, Void, Boolean> {
private String email;
private View progressView;
private EmailNewHandler handler;
private Exception error;
public EmailNewTask(String email, View progressView, EmailNewHandler handler) {
this.email = email;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
if (handler != null) {
handler.beforeStart();
}
}
protected Boolean doInBackground(Void... params) {
try {
Map<String, String> options = new HashMap<>();
options.put("email", email);
options.put("send_verification_email", "true");
Lbryio.parseResponse(Lbryio.call("user_email", "new", options, Helper.METHOD_POST, null));
} catch (LbryioResponseException ex) {
if (ex.getStatusCode() == 409) {
if (handler != null) {
handler.onEmailExists();
}
// email already exists
Map<String, String> options = new HashMap<>();
options.put("email", email);
options.put("only_if_expired", "true");
try {
Lbryio.parseResponse(Lbryio.call("user_email", "resend_token", options, Helper.METHOD_POST, null));
} catch (LbryioRequestException | LbryioResponseException e) {
error = e;
return false;
}
} else {
error = ex;
return false;
}
} catch (LbryioRequestException ex) {
error = ex;
return false;
}
return true;
}
protected void onPostExecute(Boolean result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result) {
handler.onSuccess();
} else {
handler.onError(error);
}
}
}
public interface EmailNewHandler extends GenericTaskHandler {
void onEmailExists();
}
}

View file

@ -0,0 +1,54 @@
package io.lbry.browser.tasks.verification;
import android.os.AsyncTask;
import android.view.View;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class EmailResendTask extends AsyncTask<Void, Void, Boolean> {
private String email;
private View progressView;
private GenericTaskHandler handler;
private Exception error;
public EmailResendTask(String email, View progressView, GenericTaskHandler handler) {
this.email = email;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
if (handler != null) {
handler.beforeStart();
}
}
protected Boolean doInBackground(Void... params) {
try {
Map<String, String> options = new HashMap<>();
options.put("email", email);
Lbryio.parseResponse(Lbryio.call("user_email", "resend_token", options, Helper.METHOD_POST, null));
} catch (LbryioRequestException | LbryioResponseException ex) {
error = ex;
return false;
}
return true;
}
protected void onPostExecute(Boolean result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result) {
handler.onSuccess();
} else {
handler.onError(error);
}
}
}
}

View file

@ -0,0 +1,27 @@
package io.lbry.browser.tasks.wallet;
import io.lbry.browser.model.WalletSync;
public abstract class DefaultSyncTaskHandler implements SyncTaskHandler {
public void onSyncGetSuccess(WalletSync walletSync) {
throw new UnsupportedOperationException();
}
public void onSyncGetWalletNotFound() {
throw new UnsupportedOperationException();
}
public void onSyncGetError(Exception error) {
throw new UnsupportedOperationException();
}
public void onSyncSetSuccess(String hash) {
throw new UnsupportedOperationException();
}
public void onSyncSetError(Exception error) {
throw new UnsupportedOperationException();
}
public void onSyncApplySuccess(String hash, String data) {
throw new UnsupportedOperationException();
}
public void onSyncApplyError(Exception error) {
throw new UnsupportedOperationException();
}
}

View file

@ -0,0 +1,73 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import android.view.View;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class SyncApplyTask extends AsyncTask<Void, Void, Boolean> {
// flag to indicate if this sync_apply is to fetch wallet data or apply data
private boolean fetch;
private Exception error;
private String password;
private String data;
private View progressView;
private SyncTaskHandler handler;
private String syncHash;
private String syncData;
public SyncApplyTask(boolean fetch, SyncTaskHandler handler) {
this.fetch = fetch;
this.handler = handler;
}
public SyncApplyTask(String password, String data, View progressView, SyncTaskHandler handler) {
this.password = password;
this.data = data;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
public Boolean doInBackground(Void... params) {
Map<String, Object> options = new HashMap<>();
options.put("password", Helper.isNullOrEmpty(password) ? "" : password);
if (!fetch) {
options.put("data", data);
options.put("blocking", true);
}
try {
JSONObject response = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_SYNC_APPLY, options);
syncHash = Helper.getJSONString("hash", null, response);
syncData = Helper.getJSONString("data", null, response);
} catch (ApiCallException ex) {
error = ex;
return false;
}
return true;
}
protected void onPostExecute(Boolean result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result) {
handler.onSyncApplySuccess(syncHash, syncData);
} else {
handler.onSyncApplyError(error);
}
}
}
}

View file

@ -0,0 +1,116 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import android.view.View;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.exceptions.WalletException;
import io.lbry.browser.model.WalletSync;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.Lbryio;
public class SyncGetTask extends AsyncTask<Void, Void, WalletSync> {
private boolean applySyncChanges;
private boolean applySyncSuccessful;
private Exception error;
private Exception syncApplyError;
private String password;
private SyncTaskHandler handler;
private View progressView;
private String syncHash;
private String syncData;
public SyncGetTask(String password, boolean applySyncChanges, View progressView, SyncTaskHandler handler) {
this.password = password;
this.progressView = progressView;
this.applySyncChanges = applySyncChanges;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected WalletSync doInBackground(Void... params) {
try {
password = Helper.isNullOrEmpty(password) ? "" : password;
JSONObject result = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_WALLET_STATUS);
boolean isLocked = Helper.getJSONBoolean("is_locked", false, result);
boolean unlockSuccessful =
!isLocked || (boolean) Lbry.genericApiCall(Lbry.METHOD_WALLET_UNLOCK, Lbry.buildSingleParam("password", password));
if (!unlockSuccessful) {
throw new WalletException("The wallet could be unlocked with the provided password.");
}
String hash = (String) Lbry.genericApiCall(Lbry.METHOD_SYNC_HASH);
try {
JSONObject response = (JSONObject) Lbryio.parseResponse(
Lbryio.call("sync", "get", Lbryio.buildSingleParam("hash", hash), Helper.METHOD_POST, null));
WalletSync walletSync = new WalletSync(
Helper.getJSONString("hash", null, response),
Helper.getJSONString("data", null, response),
Helper.getJSONBoolean("changed", false, response)
);
if (applySyncChanges && (!hash.equalsIgnoreCase(walletSync.getHash()) || walletSync.isChanged())) {
//Lbry.sync_apply({ password, data: response.data, blocking: true });
try {
Map<String, Object> options = new HashMap<>();
options.put("hash", walletSync.getHash());
options.put("data", walletSync.getData());
options.put("blocking", true);
JSONObject syncApplyResponse = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_SYNC_APPLY, options);
syncHash = Helper.getJSONString("hash", null, syncApplyResponse);
syncData = Helper.getJSONString("data", null, syncApplyResponse);
applySyncSuccessful = true;
} catch (ApiCallException | ClassCastException ex) {
// sync_apply failed
syncApplyError = ex;
}
}
if (Lbryio.isSignedIn() && !Lbryio.userHasSyncedWallet) {
// indicate that the user owns a synced wallet (only if the user is signed in)
Lbryio.userHasSyncedWallet = true;
}
return walletSync;
} catch (LbryioResponseException ex) {
// wallet sync data doesn't exist
return null;
}
} catch (ApiCallException | WalletException | ClassCastException | LbryioRequestException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(WalletSync result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result != null) {
handler.onSyncGetSuccess(result);
} else if (error != null) {
handler.onSyncGetError(error);
} else {
handler.onSyncGetWalletNotFound();
}
if (applySyncChanges) {
if (applySyncSuccessful) {
handler.onSyncApplySuccess(syncHash, syncData);
} else {
handler.onSyncApplyError(syncApplyError);
}
}
}
}
}

View file

@ -0,0 +1,53 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.LbryioRequestException;
import io.lbry.browser.exceptions.LbryioResponseException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class SyncSetTask extends AsyncTask<Void, Void, String> {
private Exception error;
private String oldHash;
private String newHash;
private String data;
private SyncTaskHandler handler;
public SyncSetTask(String oldHash, String newHash, String data, SyncTaskHandler handler) {
this.oldHash = oldHash;
this.newHash = newHash;
this.data = data;
this.handler = handler;
}
protected String doInBackground(Void... params) {
try {
Map<String, String> options = new HashMap<>();
options.put("old_hash", oldHash);
options.put("new_hash", newHash);
options.put("data", data);
JSONObject response = (JSONObject) Lbryio.parseResponse(
Lbryio.call("sync", "set", options, Helper.METHOD_POST, null));
String hash = Helper.getJSONString("hash", null, response);
return hash;
} catch (LbryioRequestException | LbryioResponseException | ClassCastException ex) {
error = ex;
return null;
}
}
protected void onPostExecute(String hash) {
if (handler != null) {
if (!Helper.isNullOrEmpty(hash)) {
handler.onSyncSetSuccess(hash);
} else if (error != null) {
handler.onSyncSetError(error);
}
}
}
}

View file

@ -0,0 +1,13 @@
package io.lbry.browser.tasks.wallet;
import io.lbry.browser.model.WalletSync;
public interface SyncTaskHandler {
void onSyncGetSuccess(WalletSync walletSync);
void onSyncGetWalletNotFound();
void onSyncGetError(Exception error);
void onSyncSetSuccess(String hash);
void onSyncSetError(Exception error);
void onSyncApplySuccess(String hash, String data);
void onSyncApplyError(Exception error);
}

View file

@ -0,0 +1,56 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import android.view.View;
import java.util.List;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.Transaction;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class TransactionListTask extends AsyncTask<Void, Void, List<Transaction>> {
private int page;
private int pageSize;
private View progressView;
private TransactionListHandler handler;
private Exception error;
public TransactionListTask(int page, int pageSize, View progressView, TransactionListHandler handler) {
this.page = page;
this.pageSize = pageSize;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected List<Transaction> doInBackground(Void... params) {
List<Transaction> transactions = null;
try {
transactions = Lbry.transactionList(page, pageSize);
} catch (ApiCallException ex) {
error = ex;
}
return transactions;
}
protected void onPostExecute(List<Transaction> transactions) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (transactions != null) {
handler.onSuccess(transactions, transactions.size() < pageSize);
} else {
handler.onError(error);
}
}
}
public interface TransactionListHandler {
void onSuccess(List<Transaction> transactions, boolean hasReachedEnd);
void onError(Exception error);
}
}

View file

@ -0,0 +1,55 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigDecimal;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class WalletAddressUnusedTask extends AsyncTask<Void, Void, String> {
private WalletAddressUnusedHandler handler;
private Exception error;
public WalletAddressUnusedTask(WalletAddressUnusedHandler handler) {
this.handler = handler;
}
protected void onPreExecute() {
if (handler != null) {
handler.beforeStart();
}
}
protected String doInBackground(Void... params) {
String address = null;
try {
address = (String) Lbry.genericApiCall(Lbry.METHOD_ADDRESS_UNUSED);
} catch (ApiCallException | ClassCastException ex) {
error = ex;
}
return address;
}
protected void onPostExecute(String unusedAddress) {
if (handler != null) {
if (!Helper.isNullOrEmpty(unusedAddress)) {
handler.onSuccess(unusedAddress);
} else {
handler.onError(error);
}
}
}
public interface WalletAddressUnusedHandler {
void beforeStart();
void onSuccess(String newAddress);
void onError(Exception error);
}
}

View file

@ -0,0 +1,58 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import org.json.JSONObject;
import java.math.BigDecimal;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class WalletBalanceTask extends AsyncTask<Void, Void, WalletBalance> {
private WalletBalanceHandler handler;
private Exception error;
public WalletBalanceTask(WalletBalanceHandler handler) {
this.handler = handler;
}
protected WalletBalance doInBackground(Void... params) {
WalletBalance balance = new WalletBalance();
try {
JSONObject json = (JSONObject) Lbry.genericApiCall(Lbry.METHOD_WALLET_BALANCE);
JSONObject reservedSubtotals = Helper.getJSONObject("reserved_subtotals", json);
balance.setAvailable(new BigDecimal(Helper.getJSONString("available", "0", json)));
balance.setReserved(new BigDecimal(Helper.getJSONString("reserved", "0", json)));
balance.setTotal(new BigDecimal(Helper.getJSONString("total", "0", json)));
if (reservedSubtotals != null) {
balance.setClaims(new BigDecimal(Helper.getJSONString("claims", "0", reservedSubtotals)));
balance.setSupports(new BigDecimal(Helper.getJSONString("supports", "0", reservedSubtotals)));
balance.setTips(new BigDecimal(Helper.getJSONString("tips", "0", reservedSubtotals)));
}
} catch (ApiCallException | ClassCastException ex) {
error = ex;
return null;
}
return balance;
}
protected void onPostExecute(WalletBalance walletBalance) {
if (handler != null) {
if (walletBalance != null) {
handler.onSuccess(walletBalance);
} else {
handler.onError(error);
}
}
}
public interface WalletBalanceHandler {
void onSuccess(WalletBalance walletBalance);
void onError(Exception error);
}
}

View file

@ -0,0 +1,60 @@
package io.lbry.browser.tasks.wallet;
import android.os.AsyncTask;
import android.view.View;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.exceptions.ApiCallException;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
public class WalletSendTask extends AsyncTask<Void, Void, Boolean> {
private String recipientAddress;
private String amount;
private View progressView;
private WalletSendHandler handler;
private Exception error;
public WalletSendTask(String recipientAddress, String amount, View progressView, WalletSendHandler handler) {
this.recipientAddress = recipientAddress;
this.amount = amount;
this.progressView = progressView;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected Boolean doInBackground(Void... params) {
try {
Map<String, Object> options = new HashMap<>();
options.put("addresses", Arrays.asList(recipientAddress));
options.put("amount", amount);
Lbry.genericApiCall(Lbry.METHOD_WALLET_SEND, options);
} catch (ApiCallException ex) {
error = ex;
return false;
}
return true;
}
protected void onPostExecute(Boolean result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result) {
handler.onSuccess();
} else {
handler.onError(error);
}
}
}
public interface WalletSendHandler {
void onSuccess();
void onError(Exception error);
}
}

View file

@ -0,0 +1,25 @@
package io.lbry.browser.ui;
import android.content.Context;
import androidx.fragment.app.Fragment;
import java.util.Map;
import io.lbry.browser.MainActivity;
import lombok.Getter;
import lombok.Setter;
public class BaseFragment extends Fragment {
@Getter
@Setter
private Map<String, Object> params;
public void onResume() {
super.onResume();
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).setSelectedMenuItemForFragment(this);
}
}
}

View file

@ -0,0 +1,315 @@
package io.lbry.browser.ui.allcontent;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import io.lbry.browser.FileViewActivity;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.dialog.ContentFromDialogFragment;
import io.lbry.browser.dialog.ContentSortDialogFragment;
import io.lbry.browser.model.Claim;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
// TODO: Similar code to FollowingFragment and Channel page fragment. Probably make common operations (sorting/filtering) into a control
public class AllContentFragment extends BaseFragment {
private boolean singleTagView;
private List<String> tags;
private View layoutFilterContainer;
private View sortLink;
private View contentFromLink;
private View scopeLink;
private TextView titleView;
private TextView sortLinkText;
private TextView contentFromLinkText;
private TextView scopeLinkText;
private RecyclerView contentList;
private int currentSortBy;
private int currentContentFrom;
private int currentScope;
private String contentReleaseTime;
private List<String> contentSortOrder;
private View fromPrefix;
private View forPrefix;
private View contentLoading;
private View bigContentLoading;
private ClaimListAdapter contentListAdapter;
private boolean contentClaimSearchLoading;
private boolean contentHasReachedEnd;
private int currentClaimSearchPage;
private ClaimSearchTask contentClaimSearchTask;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_all_content, container, false);
// All content page is sorted by trending by default, past week if sort is top
currentSortBy = ContentSortDialogFragment.ITEM_SORT_BY_TRENDING;
currentContentFrom = ContentFromDialogFragment.ITEM_FROM_PAST_WEEK;
layoutFilterContainer = root.findViewById(R.id.all_content_filter_container);
titleView = root.findViewById(R.id.all_content_page_title);
sortLink = root.findViewById(R.id.all_content_sort_link);
contentFromLink = root.findViewById(R.id.all_content_time_link);
scopeLink = root.findViewById(R.id.all_content_scope_link);
fromPrefix = root.findViewById(R.id.all_content_from_prefix);
forPrefix = root.findViewById(R.id.all_content_for_prefix);
sortLinkText = root.findViewById(R.id.all_content_sort_link_text);
contentFromLinkText = root.findViewById(R.id.all_content_time_link_text);
scopeLinkText = root.findViewById(R.id.all_content_scope_link_text);
bigContentLoading = root.findViewById(R.id.all_content_main_progress);
contentLoading = root.findViewById(R.id.all_content_load_progress);
contentList = root.findViewById(R.id.all_content_list);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
contentList.setLayoutManager(llm);
contentList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (contentClaimSearchLoading) {
return;
}
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
if (lm != null) {
int visibleItemCount = lm.getChildCount();
int totalItemCount = lm.getItemCount();
int pastVisibleItems = lm.findFirstVisibleItemPosition();
if (pastVisibleItems + visibleItemCount >= totalItemCount) {
if (!contentHasReachedEnd) {
// load more
currentClaimSearchPage++;
fetchClaimSearchContent();
}
}
}
}
});
sortLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentSortDialogFragment dialog = ContentSortDialogFragment.newInstance();
dialog.setCurrentSortByItem(currentSortBy);
dialog.setSortByListener(new ContentSortDialogFragment.SortByListener() {
@Override
public void onSortByItemSelected(int sortBy) {
onSortByChanged(sortBy);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentSortDialogFragment.TAG);
}
}
});
contentFromLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentFromDialogFragment dialog = ContentFromDialogFragment.newInstance();
dialog.setCurrentFromItem(currentContentFrom);
dialog.setContentFromListener(new ContentFromDialogFragment.ContentFromListener() {
@Override
public void onContentFromItemSelected(int contentFromItem) {
onContentFromChanged(contentFromItem);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentFromDialogFragment.TAG);
}
}
});
checkParams(false);
return root;
}
public void setParams(Map<String, Object> params) {
super.setParams(params);
if (getView() != null) {
checkParams(true);
}
}
private void checkParams(boolean reload) {
Map<String, Object> params = getParams();
if (params != null && params.containsKey("singleTag")) {
String tagName = params.get("singleTag").toString();
singleTagView = true;
tags = Arrays.asList(tagName);
titleView.setText(Helper.capitalize(tagName));
} else {
singleTagView = false;
tags = null;
titleView.setText(getString(R.string.all_content));
}
forPrefix.setVisibility(singleTagView ? View.GONE : View.VISIBLE);
scopeLink.setVisibility(singleTagView ? View.GONE : View.VISIBLE);
if (reload) {
fetchClaimSearchContent(true);
}
}
private void onContentFromChanged(int contentFrom) {
currentContentFrom = contentFrom;
// rebuild options and search
updateContentFromLinkText();
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void onSortByChanged(int sortBy) {
currentSortBy = sortBy;
// rebuild options and search
Helper.setViewVisibility(fromPrefix, currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ? View.VISIBLE : View.GONE);
Helper.setViewVisibility(contentFromLink, currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ? View.VISIBLE : View.GONE);
currentContentFrom = currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ?
(currentContentFrom == 0 ? ContentFromDialogFragment.ITEM_FROM_PAST_WEEK : currentContentFrom) : 0;
updateSortByLinkText();
contentSortOrder = Helper.buildContentSortOrder(currentSortBy);
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void updateSortByLinkText() {
int stringResourceId = -1;
switch (currentSortBy) {
case ContentSortDialogFragment.ITEM_SORT_BY_NEW: default: stringResourceId = R.string.new_text; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TOP: stringResourceId = R.string.top; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TRENDING: stringResourceId = R.string.trending; break;
}
Helper.setViewText(sortLinkText, stringResourceId);
}
private void updateContentFromLinkText() {
int stringResourceId = -1;
switch (currentContentFrom) {
case ContentFromDialogFragment.ITEM_FROM_PAST_24_HOURS: stringResourceId = R.string.past_24_hours; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_WEEK: default: stringResourceId = R.string.past_week; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_MONTH: stringResourceId = R.string.past_month; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_YEAR: stringResourceId = R.string.past_year; break;
case ContentFromDialogFragment.ITEM_FROM_ALL_TIME: stringResourceId = R.string.all_time; break;
}
Helper.setViewText(contentFromLinkText, stringResourceId);
}
public void onResume() {
super.onResume();
fetchClaimSearchContent();
}
private Map<String, Object> buildContentOptions() {
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
tags != null ? tags : null,
null, // TODO: Check mature
null,
null,
getContentSortOrder(),
contentReleaseTime,
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
Helper.CONTENT_PAGE_SIZE);
}
private List<String> getContentSortOrder() {
if (contentSortOrder == null) {
return Arrays.asList(Claim.ORDER_BY_TRENDING_GROUP, Claim.ORDER_BY_TRENDING_MIXED);
}
return contentSortOrder;
}
private View getLoadingView() {
return (contentListAdapter == null || contentListAdapter.getItemCount() == 0) ? bigContentLoading : contentLoading;
}
private void fetchClaimSearchContent() {
fetchClaimSearchContent(false);
}
private void fetchClaimSearchContent(boolean reset) {
if (reset && contentListAdapter != null) {
contentListAdapter.clearItems();
currentClaimSearchPage = 1;
}
contentClaimSearchLoading = true;
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
if (contentListAdapter == null) {
contentListAdapter = new ClaimListAdapter(claims, getContext());
contentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override
public void onClaimClicked(Claim claim) {
String claimId = claim.getClaimId();
String url = claim.getPermanentUrl();
if (claim.getName().startsWith("@")) {
// channel claim
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).openChannelClaim(claim);
}
} else {
Intent intent = new Intent(getContext(), FileViewActivity.class);
intent.putExtra("claimId", claimId);
intent.putExtra("url", url);
MainActivity.startingFileViewActivity = true;
startActivity(intent);
}
}
});
} else {
contentListAdapter.addItems(claims);
}
if (contentList != null && contentList.getAdapter() == null) {
contentList.setAdapter(contentListAdapter);
}
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -0,0 +1,71 @@
package io.lbry.browser.ui.channel;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.text.HtmlCompat;
import androidx.fragment.app.Fragment;
import io.lbry.browser.R;
import io.lbry.browser.utils.Helper;
import lombok.Setter;
public class ChannelAboutFragment extends Fragment {
private View layoutWebsite;
private View layoutEmail;
private View layoutInfoArea;
private View layoutNoAboutInfo;
private TextView textWebsite;
private TextView textEmail;
private TextView textDescription;
@Setter
private String website;
@Setter
private String email;
@Setter
private String description;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_channel_about, container, false);
layoutInfoArea = root.findViewById(R.id.channel_about_info_area);
layoutNoAboutInfo = root.findViewById(R.id.channel_about_no_info_container);
layoutWebsite = root.findViewById(R.id.channel_about_website_container);
layoutEmail = root.findViewById(R.id.channel_about_email_container);
textWebsite = root.findViewById(R.id.channel_about_website);
textEmail = root.findViewById(R.id.channel_about_email);
textDescription = root.findViewById(R.id.channel_about_description);
boolean noInfo = (Helper.isNullOrEmpty(website) && Helper.isNullOrEmpty(email) && Helper.isNullOrEmpty(description));
layoutNoAboutInfo.setVisibility(noInfo ? View.VISIBLE : View.GONE);
layoutInfoArea.setVisibility(noInfo ? View.GONE : View.VISIBLE);
layoutWebsite.setVisibility(!Helper.isNullOrEmpty(website) ? View.VISIBLE : View.GONE);
layoutEmail.setVisibility(!Helper.isNullOrEmpty(email) ? View.VISIBLE : View.GONE);
textDescription.setVisibility(!Helper.isNullOrEmpty(description) ? View.VISIBLE : View.GONE);
textWebsite.setLinksClickable(true);
textWebsite.setMovementMethod(LinkMovementMethod.getInstance());
textWebsite.setText(!Helper.isNullOrEmpty(website) ?
HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", website, website), HtmlCompat.FROM_HTML_MODE_LEGACY) : null);
textEmail.setText(email);
textDescription.setText(description);
return root;
}
public void refresh() {
textWebsite.setText(!Helper.isNullOrEmpty(website) ?
HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", website, website), HtmlCompat.FROM_HTML_MODE_LEGACY) : null);
textEmail.setText(email);
textDescription.setText(description);
}
}

View file

@ -0,0 +1,273 @@
package io.lbry.browser.ui.channel;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import io.lbry.browser.FileViewActivity;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.dialog.ContentFromDialogFragment;
import io.lbry.browser.dialog.ContentSortDialogFragment;
import io.lbry.browser.model.Claim;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import lombok.Setter;
public class ChannelContentFragment extends Fragment {
@Setter
private String channelId;
private View sortLink;
private View contentFromLink;
private TextView sortLinkText;
private TextView contentFromLinkText;
private RecyclerView contentList;
private int currentSortBy;
private int currentContentFrom;
private String contentReleaseTime;
private List<String> contentSortOrder;
private View contentLoading;
private View bigContentLoading;
private ClaimListAdapter contentListAdapter;
private boolean contentClaimSearchLoading;
private boolean contentHasReachedEnd;
private int currentClaimSearchPage;
private ClaimSearchTask contentClaimSearchTask;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_channel_content, container, false);
currentSortBy = ContentSortDialogFragment.ITEM_SORT_BY_TRENDING;
currentContentFrom = ContentFromDialogFragment.ITEM_FROM_PAST_WEEK;
sortLink = root.findViewById(R.id.channel_content_sort_link);
contentFromLink = root.findViewById(R.id.channel_content_time_link);
sortLinkText = root.findViewById(R.id.channel_content_sort_link_text);
contentFromLinkText = root.findViewById(R.id.channel_content_time_link_text);
bigContentLoading = root.findViewById(R.id.channel_content_main_progress);
contentLoading = root.findViewById(R.id.channel_content_load_progress);
contentList = root.findViewById(R.id.channel_content_list);
LinearLayoutManager llm = new LinearLayoutManager(getContext());
contentList.setLayoutManager(llm);
contentList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (contentClaimSearchLoading) {
return;
}
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
if (lm != null) {
int visibleItemCount = lm.getChildCount();
int totalItemCount = lm.getItemCount();
int pastVisibleItems = lm.findFirstVisibleItemPosition();
if (pastVisibleItems + visibleItemCount >= totalItemCount) {
if (!contentHasReachedEnd) {
// load more
currentClaimSearchPage++;
fetchClaimSearchContent();
}
}
}
}
});
sortLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentSortDialogFragment dialog = ContentSortDialogFragment.newInstance();
dialog.setCurrentSortByItem(currentSortBy);
dialog.setSortByListener(new ContentSortDialogFragment.SortByListener() {
@Override
public void onSortByItemSelected(int sortBy) {
onSortByChanged(sortBy);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentSortDialogFragment.TAG);
}
}
});
contentFromLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentFromDialogFragment dialog = ContentFromDialogFragment.newInstance();
dialog.setCurrentFromItem(currentContentFrom);
dialog.setContentFromListener(new ContentFromDialogFragment.ContentFromListener() {
@Override
public void onContentFromItemSelected(int contentFromItem) {
onContentFromChanged(contentFromItem);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentFromDialogFragment.TAG);
}
}
});
return root;
}
private void onContentFromChanged(int contentFrom) {
currentContentFrom = contentFrom;
// rebuild options and search
updateContentFromLinkText();
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void onSortByChanged(int sortBy) {
currentSortBy = sortBy;
// rebuild options and search
Helper.setViewVisibility(contentFromLink, currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ? View.VISIBLE : View.GONE);
currentContentFrom = currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ?
(currentContentFrom == 0 ? ContentFromDialogFragment.ITEM_FROM_PAST_WEEK : currentContentFrom) : 0;
updateSortByLinkText();
contentSortOrder = Helper.buildContentSortOrder(currentSortBy);
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void updateSortByLinkText() {
int stringResourceId = -1;
switch (currentSortBy) {
case ContentSortDialogFragment.ITEM_SORT_BY_NEW: default: stringResourceId = R.string.new_text; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TOP: stringResourceId = R.string.top; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TRENDING: stringResourceId = R.string.trending; break;
}
Helper.setViewText(sortLinkText, stringResourceId);
}
private void updateContentFromLinkText() {
int stringResourceId = -1;
switch (currentContentFrom) {
case ContentFromDialogFragment.ITEM_FROM_PAST_24_HOURS: stringResourceId = R.string.past_24_hours; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_WEEK: default: stringResourceId = R.string.past_week; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_MONTH: stringResourceId = R.string.past_month; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_YEAR: stringResourceId = R.string.past_year; break;
case ContentFromDialogFragment.ITEM_FROM_ALL_TIME: stringResourceId = R.string.all_time; break;
}
Helper.setViewText(contentFromLinkText, stringResourceId);
}
public void onResume() {
super.onResume();
fetchClaimSearchContent();
}
public void refresh() {
fetchClaimSearchContent(true);
}
private Map<String, Object> buildContentOptions() {
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
null,
null, // TODO: Check mature
Arrays.asList(channelId),
null,
getContentSortOrder(),
contentReleaseTime,
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
Helper.CONTENT_PAGE_SIZE);
}
private List<String> getContentSortOrder() {
if (contentSortOrder == null) {
return Arrays.asList(Claim.ORDER_BY_RELEASE_TIME);
}
return contentSortOrder;
}
private View getLoadingView() {
return (contentListAdapter == null || contentListAdapter.getItemCount() == 0) ? bigContentLoading : contentLoading;
}
private void fetchClaimSearchContent() {
fetchClaimSearchContent(false);
}
private void fetchClaimSearchContent(boolean reset) {
if (reset && contentListAdapter != null) {
contentListAdapter.clearItems();
currentClaimSearchPage = 1;
}
contentClaimSearchLoading = true;
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
if (contentListAdapter == null) {
contentListAdapter = new ClaimListAdapter(claims, getContext());
contentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override
public void onClaimClicked(Claim claim) {
String claimId = claim.getClaimId();
String url = claim.getPermanentUrl();
if (claim.getName().startsWith("@")) {
// channel claim
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).openChannelClaim(claim);
}
} else {
Intent intent = new Intent(getContext(), FileViewActivity.class);
intent.putExtra("claimId", claimId);
intent.putExtra("url", url);
MainActivity.startingFileViewActivity = true;
startActivity(intent);
}
}
});
} else {
contentListAdapter.addItems(claims);
}
if (contentList != null && contentList.getAdapter() == null) {
contentList.setAdapter(contentListAdapter);
}
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}

View file

@ -0,0 +1,242 @@
package io.lbry.browser.ui.channel;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import java.util.List;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.model.Claim;
import io.lbry.browser.tasks.ResolveTask;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import lombok.SneakyThrows;
public class ChannelFragment extends BaseFragment {
private Claim claim;
private boolean resolving;
private String url;
private View layoutResolving;
private View layoutDisplayArea;
private ImageView imageCover;
private ImageView imageThumbnail;
private View noThumbnailView;
private TextView textAlpha;
private TextView textTitle;
private TextView textFollowerCount;
private TabLayout tabLayout;
private ViewPager2 tabPager;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_channel, container, false);
layoutDisplayArea = root.findViewById(R.id.channel_view_claim_display_area);
layoutResolving = root.findViewById(R.id.channel_view_loading_container);
imageCover = root.findViewById(R.id.channel_view_cover_image);
imageThumbnail = root.findViewById(R.id.channel_view_thumbnail);
noThumbnailView = root.findViewById(R.id.channel_view_no_thumbnail);
textAlpha = root.findViewById(R.id.channel_view_icon_alpha);
textTitle = root.findViewById(R.id.channel_view_title);
textFollowerCount = root.findViewById(R.id.channel_view_follower_count);
tabPager = root.findViewById(R.id.channel_view_pager);
tabLayout = root.findViewById(R.id.channel_view_tabs);
tabPager.setSaveEnabled(false);
return root;
}
public void onResume() {
super.onResume();
checkParams();
}
private void checkParams() {
boolean updateRequired = false;
Map<String, Object> params = getParams();
if (params.containsKey("claim")) {
Claim claim = (Claim) params.get("claim");
if (claim != null && !claim.equals(this.claim)) {
this.claim = claim;
updateRequired = true;
}
}
if (!updateRequired && params.containsKey("url")) {
String newUrl = params.get("url").toString();
if (!newUrl.equalsIgnoreCase(url) || claim == null) {
this.claim = null;
this.url = newUrl;
updateRequired = true;
}
}
if (updateRequired) {
resetFragments();
if (!Helper.isNullOrEmpty(url)) {
resolveUrl();
} else if (claim == null) {
// nothing at this location
renderNothingAtLocation();
}
}
if (claim != null) {
renderClaim();
}
}
private void resolveUrl() {
layoutDisplayArea.setVisibility(View.INVISIBLE);
ResolveTask task = new ResolveTask(url, Lbry.LBRY_TV_CONNECTION_STRING, layoutResolving, new ResolveTask.ResolveResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
if (claims.size() > 0) {
claim = claims.get(0);
renderClaim();
// TODO: Load follower count
} else {
renderNothingAtLocation();
}
}
@Override
public void onError(Exception error) {
renderNothingAtLocation();
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void renderNothingAtLocation() {
}
public void setParams(Map<String, Object> params) {
super.setParams(params);
if (getView() != null) {
checkParams();
}
}
private void renderClaim() {
if (claim == null) {
renderNothingAtLocation();
return;
}
layoutDisplayArea.setVisibility(View.VISIBLE);
String thumbnailUrl = claim.getThumbnailUrl();
String coverUrl = claim.getCoverUrl();
textTitle.setText(Helper.isNullOrEmpty(claim.getTitle()) ? claim.getName() : claim.getTitle());
if (!Helper.isNullOrEmpty(coverUrl)) {
Glide.with(getContext().getApplicationContext()).load(coverUrl).centerCrop().into(imageCover);
}
if (!Helper.isNullOrEmpty(thumbnailUrl)) {
Glide.with(getContext().getApplicationContext()).load(thumbnailUrl).apply(RequestOptions.circleCropTransform()).into(imageThumbnail);
noThumbnailView.setVisibility(View.GONE);
} else {
imageThumbnail.setVisibility(View.GONE);
int bgColor = Helper.generateRandomColorForValue(claim.getClaimId());
Helper.setIconViewBackgroundColor(noThumbnailView, bgColor, false, getContext());
noThumbnailView.setVisibility(View.VISIBLE);
textAlpha.setText(claim.getName().substring(1, 2));
}
tabPager.setAdapter(new ChannelPagerAdapter(claim, (MainActivity) getContext()));
new TabLayoutMediator(tabLayout, tabPager, new TabLayoutMediator.TabConfigurationStrategy() {
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position) {
tab.setText(position == 0 ? R.string.content : R.string.about);
}
}).attach();
}
private void resetFragments() {
try {
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) getContext();
FragmentManager manager = activity.getSupportFragmentManager();
FragmentTransaction tx = manager.beginTransaction();
for (Fragment fragment : manager.getFragments()) {
if (fragment.getClass().equals(ChannelAboutFragment.class) || fragment.getClass().equals(ChannelContentFragment.class)) {
tx.remove(fragment);
}
}
tx.commitAllowingStateLoss();
}
} catch (Exception ex) {
// pass
}
}
private static class ChannelPagerAdapter extends FragmentStateAdapter {
private Claim channelClaim;
public ChannelPagerAdapter(Claim channelClaim, FragmentActivity activity) {
super(activity);
this.channelClaim = channelClaim;
}
@SneakyThrows
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
ChannelContentFragment contentFragment = ChannelContentFragment.class.newInstance();
contentFragment.setChannelId(channelClaim.getClaimId());
return contentFragment;
case 1:
ChannelAboutFragment aboutFragment = ChannelAboutFragment.class.newInstance();
try {
Claim.ChannelMetadata metadata = (Claim.ChannelMetadata) channelClaim.getValue();
aboutFragment.setDescription(metadata.getDescription());
aboutFragment.setEmail(metadata.getEmail());
aboutFragment.setWebsite(metadata.getWebsiteUrl());
} catch (ClassCastException ex) {
// pass
}
return aboutFragment;
}
return null;
}
public long getItemId(int position) {
return String.format("%s-%d", channelClaim.getClaimId(), position).hashCode();
}
@Override
public int getItemCount() {
return 2;
}
}
}

View file

@ -0,0 +1,29 @@
package io.lbry.browser.ui.controls;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.view.Gravity;
import androidx.appcompat.widget.AppCompatTextView;
public class SolidIconView extends AppCompatTextView {
private Context context;
public SolidIconView(Context context) {
super(context);
this.context = context;
init();
}
public SolidIconView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
private void init() {
setGravity(Gravity.CENTER);
setTypeface(Typeface.createFromAsset(context.getAssets(), "font_awesome_5_free_solid.otf"));
}
}

View file

@ -0,0 +1,581 @@
package io.lbry.browser.ui.following;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
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.Arrays;
import java.util.List;
import java.util.Map;
import io.lbry.browser.FileViewActivity;
import io.lbry.browser.MainActivity;
import io.lbry.browser.R;
import io.lbry.browser.adapter.ChannelFilterListAdapter;
import io.lbry.browser.adapter.ClaimListAdapter;
import io.lbry.browser.adapter.SuggestedChannelGridAdapter;
import io.lbry.browser.dialog.ContentFromDialogFragment;
import io.lbry.browser.dialog.ContentSortDialogFragment;
import io.lbry.browser.exceptions.LbryUriException;
import io.lbry.browser.model.Claim;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.ChannelSubscribeTask;
import io.lbry.browser.tasks.ClaimSearchTask;
import io.lbry.browser.tasks.FetchSubscriptionsTask;
import io.lbry.browser.tasks.ResolveTask;
import io.lbry.browser.listener.ChannelItemSelectionListener;
import io.lbry.browser.ui.BaseFragment;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbry;
import io.lbry.browser.utils.LbryUri;
import io.lbry.browser.utils.Lbryio;
public class FollowingFragment extends BaseFragment implements FetchSubscriptionsTask.FetchSubscriptionsHandler, ChannelItemSelectionListener {
private static final int SUGGESTED_PAGE_SIZE = 45;
private static final int MIN_SUGGESTED_SUBSCRIBE_COUNT = 5;
private MaterialButton suggestedDoneButton;
private TextView titleView;
private TextView infoView;
private RecyclerView horizontalChannelList;
private RecyclerView suggestedChannelGrid;
private RecyclerView contentList;
private ProgressBar bigContentLoading;
private ProgressBar contentLoading;
private ProgressBar channelListLoading;
private View layoutSortContainer;
private View sortLink;
private TextView sortLinkText;
private View contentFromLink;
private TextView contentFromLinkText;
private int currentSortBy;
private int currentContentFrom;
private String contentReleaseTime;
private List<String> contentSortOrder;
private boolean contentClaimSearchLoading = false;
private List<Integer> queuedContentPages = new ArrayList<>();
private List<Integer> queuedSuggestedPages = new ArrayList<>();
private int currentSuggestedPage = 0;
private int currentClaimSearchPage;
private boolean suggestedHasReachedEnd;
private boolean contentHasReachedEnd;
private boolean contentPendingFetch = false;
private int numSuggestedSelected;
// adapters
private SuggestedChannelGridAdapter suggestedChannelAdapter;
private ChannelFilterListAdapter channelFilterListAdapter;
private ClaimListAdapter contentListAdapter;
private List<String> channelIds;
private List<String> channelUrls;
private List<Subscription> subscriptionsList;
private List<Claim> suggestedChannels;
private ClaimSearchTask suggestedChannelClaimSearchTask;
private ClaimSearchTask contentClaimSearchTask;
private boolean loadingSuggested;
private boolean loadingContent;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_following, container, false);
// Following page is sorted by new by default, past week if sort is top
currentSortBy = ContentSortDialogFragment.ITEM_SORT_BY_NEW;
currentContentFrom = ContentFromDialogFragment.ITEM_FROM_PAST_WEEK;
titleView = root.findViewById(R.id.following_page_title);
infoView = root.findViewById(R.id.following_page_info);
horizontalChannelList = root.findViewById(R.id.following_channel_list);
layoutSortContainer = root.findViewById(R.id.following_filter_container);
sortLink = root.findViewById(R.id.following_sort_link);
sortLinkText = root.findViewById(R.id.following_sort_link_text);
contentFromLink = root.findViewById(R.id.following_time_link);
contentFromLinkText = root.findViewById(R.id.following_time_link_text);
suggestedChannelGrid = root.findViewById(R.id.following_suggested_grid);
suggestedDoneButton = root.findViewById(R.id.following_suggested_done_button);
contentList = root.findViewById(R.id.following_content_list);
bigContentLoading = root.findViewById(R.id.following_main_progress);
contentLoading = root.findViewById(R.id.following_content_progress);
channelListLoading = root.findViewById(R.id.following_channel_load_progress);
Context context = getContext();
GridLayoutManager glm = new GridLayoutManager(context, 3);
suggestedChannelGrid.setLayoutManager(glm);
LinearLayoutManager cllm = new LinearLayoutManager(context, RecyclerView.HORIZONTAL, false);
horizontalChannelList.setLayoutManager(cllm);
LinearLayoutManager llm = new LinearLayoutManager(context);
contentList.setLayoutManager(llm);
contentList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (contentClaimSearchLoading) {
return;
}
LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager();
if (lm != null) {
int visibleItemCount = lm.getChildCount();
int totalItemCount = lm.getItemCount();
int pastVisibleItems = lm.findFirstVisibleItemPosition();
if (pastVisibleItems + visibleItemCount >= totalItemCount) {
if (!contentHasReachedEnd) {
// load more
currentClaimSearchPage++;
fetchClaimSearchContent();
}
}
}
}
});
suggestedDoneButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int selected = suggestedChannelAdapter == null ? 0 : suggestedChannelAdapter.getSelectedCount();
int remaining = MIN_SUGGESTED_SUBSCRIBE_COUNT - selected;
if (remaining == MIN_SUGGESTED_SUBSCRIBE_COUNT) {
Snackbar.make(getView(), R.string.select_five_subscriptions, Snackbar.LENGTH_LONG).show();
} else {
fetchSubscriptions();
showSubscribedContent();
fetchAndResolveChannelList();
}
}
});
sortLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentSortDialogFragment dialog = ContentSortDialogFragment.newInstance();
dialog.setCurrentSortByItem(currentSortBy);
dialog.setSortByListener(new ContentSortDialogFragment.SortByListener() {
@Override
public void onSortByItemSelected(int sortBy) {
onSortByChanged(sortBy);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentSortDialogFragment.TAG);
}
}
});
contentFromLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ContentFromDialogFragment dialog = ContentFromDialogFragment.newInstance();
dialog.setCurrentFromItem(currentContentFrom);
dialog.setContentFromListener(new ContentFromDialogFragment.ContentFromListener() {
@Override
public void onContentFromItemSelected(int contentFromItem) {
onContentFromChanged(contentFromItem);
}
});
Context context = getContext();
if (context instanceof MainActivity) {
MainActivity activity = (MainActivity) context;
dialog.show(activity.getSupportFragmentManager(), ContentFromDialogFragment.TAG);
}
}
});
return root;
}
private void onContentFromChanged(int contentFrom) {
currentContentFrom = contentFrom;
// rebuild options and search
updateContentFromLinkText();
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void onSortByChanged(int sortBy) {
currentSortBy = sortBy;
// rebuild options and search
Helper.setViewVisibility(contentFromLink, currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ? View.VISIBLE : View.GONE);
currentContentFrom = currentSortBy == ContentSortDialogFragment.ITEM_SORT_BY_TOP ?
(currentContentFrom == 0 ? ContentFromDialogFragment.ITEM_FROM_PAST_WEEK : currentContentFrom) : 0;
updateSortByLinkText();
contentSortOrder = Helper.buildContentSortOrder(currentSortBy);
contentReleaseTime = Helper.buildReleaseTime(currentContentFrom);
fetchClaimSearchContent(true);
}
private void updateSortByLinkText() {
int stringResourceId = -1;
switch (currentSortBy) {
case ContentSortDialogFragment.ITEM_SORT_BY_NEW: default: stringResourceId = R.string.new_text; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TOP: stringResourceId = R.string.top; break;
case ContentSortDialogFragment.ITEM_SORT_BY_TRENDING: stringResourceId = R.string.trending; break;
}
Helper.setViewText(sortLinkText, stringResourceId);
}
private void updateContentFromLinkText() {
int stringResourceId = -1;
switch (currentContentFrom) {
case ContentFromDialogFragment.ITEM_FROM_PAST_24_HOURS: stringResourceId = R.string.past_24_hours; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_WEEK: default: stringResourceId = R.string.past_week; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_MONTH: stringResourceId = R.string.past_month; break;
case ContentFromDialogFragment.ITEM_FROM_PAST_YEAR: stringResourceId = R.string.past_year; break;
case ContentFromDialogFragment.ITEM_FROM_ALL_TIME: stringResourceId = R.string.all_time; break;
}
Helper.setViewText(contentFromLinkText, stringResourceId);
}
public void onResume() {
super.onResume();
// check if subscriptions exist
if (suggestedChannelAdapter != null) {
showSuggestedChannels();
}
if (Lbryio.cacheSubscriptions != null && Lbryio.cacheSubscriptions.size() > 0) {
subscriptionsList = new ArrayList<>(Lbryio.cacheSubscriptions);
buildChannelIdsAndUrls();
if (Lbryio.cacheResolvedSubscriptions.size() > 0) {
updateChannelFilterListAdapter(Lbryio.cacheResolvedSubscriptions);
} else {
fetchAndResolveChannelList();
}
fetchClaimSearchContent();
showSubscribedContent();
} else {
fetchSubscriptions();
}
}
public void loadFollowing() {
// wrapper to just re-fetch subscriptions (upon user sign in, for example)
fetchSubscriptions();
}
private void fetchSubscriptions() {
FetchSubscriptionsTask task = new FetchSubscriptionsTask(getContext(), channelListLoading, this);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private Map<String, Object> buildSuggestedOptions() {
return Lbry.buildClaimSearchOptions(
Claim.TYPE_CHANNEL,
null,
null,
null,
null,
Arrays.asList(Claim.ORDER_BY_EFFECTIVE_AMOUNT),
null,
currentSuggestedPage == 0 ? 1 : currentSuggestedPage,
SUGGESTED_PAGE_SIZE);
}
private Map<String, Object> buildContentOptions() {
return Lbry.buildClaimSearchOptions(
Claim.TYPE_STREAM,
null,
null,
getChannelIds(),
null,
getContentSortOrder(),
contentReleaseTime,
currentClaimSearchPage == 0 ? 1 : currentClaimSearchPage,
Helper.CONTENT_PAGE_SIZE);
}
private List<String> getChannelIds() {
if (channelFilterListAdapter != null) {
Claim selected = channelFilterListAdapter.getSelectedItem();
if (selected != null) {
return Arrays.asList(selected.getClaimId());
}
}
return channelIds;
}
private List<String> getContentSortOrder() {
if (contentSortOrder == null) {
return Arrays.asList(Claim.ORDER_BY_RELEASE_TIME);
}
return contentSortOrder;
}
private void showSuggestedChannels() {
Helper.setViewText(titleView, R.string.find_channels_to_follow);
Helper.setViewVisibility(horizontalChannelList, View.GONE);
Helper.setViewVisibility(contentList, View.GONE);
Helper.setViewVisibility(infoView, View.VISIBLE);
Helper.setViewVisibility(layoutSortContainer, View.GONE);
Helper.setViewVisibility(suggestedChannelGrid, View.VISIBLE);
Helper.setViewVisibility(suggestedDoneButton, View.VISIBLE);
updateSuggestedDoneButtonText();
}
private void showSubscribedContent() {
Helper.setViewText(titleView, R.string.channels_you_follow);
Helper.setViewVisibility(horizontalChannelList, View.VISIBLE);
Helper.setViewVisibility(contentList, View.VISIBLE);
Helper.setViewVisibility(infoView, View.GONE);
Helper.setViewVisibility(layoutSortContainer, View.VISIBLE);
Helper.setViewVisibility(suggestedChannelGrid, View.GONE);
Helper.setViewVisibility(suggestedDoneButton, View.GONE);
}
private void buildChannelIdsAndUrls() {
channelIds = new ArrayList<>();
channelUrls = new ArrayList<>();
if (subscriptionsList != null) {
for (Subscription subscription : subscriptionsList) {
try {
String url = subscription.getUrl();
LbryUri uri = LbryUri.parse(url);
String claimId = uri.getClaimId();
channelIds.add(claimId);
channelUrls.add(url);
} catch (LbryUriException ex) {
// pass
}
}
}
}
private void fetchAndResolveChannelList() {
buildChannelIdsAndUrls();
if (channelIds.size() > 0) {
ResolveTask resolveSubscribedTask = new ResolveTask(channelUrls, Lbry.LBRY_TV_CONNECTION_STRING, channelListLoading, new ResolveTask.ResolveResultHandler() {
@Override
public void onSuccess(List<Claim> claims) {
updateChannelFilterListAdapter(claims);
Lbryio.cacheResolvedSubscriptions = claims;
}
@Override
public void onError(Exception error) {
fetchAndResolveChannelList();
}
});
resolveSubscribedTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
fetchClaimSearchContent();
}
}
private View getLoadingView() {
return (contentListAdapter == null || contentListAdapter.getItemCount() == 0) ? bigContentLoading : contentLoading;
}
private void updateChannelFilterListAdapter(List<Claim> resolvedSubs) {
if (channelFilterListAdapter == null) {
channelFilterListAdapter = new ChannelFilterListAdapter(getContext());
channelFilterListAdapter.setListener(new ChannelItemSelectionListener() {
@Override
public void onChannelItemSelected(Claim claim) {
if (contentClaimSearchTask != null && contentClaimSearchTask.getStatus() != AsyncTask.Status.FINISHED) {
contentClaimSearchTask.cancel(true);
}
if (contentListAdapter != null) {
contentListAdapter.clearItems();
}
currentClaimSearchPage = 1;
contentClaimSearchLoading = false;
fetchClaimSearchContent();
}
@Override
public void onChannelItemDeselected(Claim claim) {
}
@Override
public void onChannelSelectionCleared() {
if (contentClaimSearchTask != null && contentClaimSearchTask.getStatus() != AsyncTask.Status.FINISHED) {
contentClaimSearchTask.cancel(true);
}
if (contentListAdapter != null) {
contentListAdapter.clearItems();
}
currentClaimSearchPage = 1;
contentClaimSearchLoading = false;
fetchClaimSearchContent();
}
});
}
if (horizontalChannelList != null && horizontalChannelList.getAdapter() == null) {
horizontalChannelList.setAdapter(channelFilterListAdapter);
}
channelFilterListAdapter.addClaims(resolvedSubs);
}
private void fetchClaimSearchContent() {
fetchClaimSearchContent(false);
}
private void fetchClaimSearchContent(boolean reset) {
if (reset && contentListAdapter != null) {
contentListAdapter.clearItems();
currentClaimSearchPage = 1;
}
contentClaimSearchLoading = true;
Map<String, Object> claimSearchOptions = buildContentOptions();
contentClaimSearchTask = new ClaimSearchTask(claimSearchOptions, Lbry.LBRY_TV_CONNECTION_STRING, getLoadingView(), new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
if (contentListAdapter == null) {
contentListAdapter = new ClaimListAdapter(claims, getContext());
contentListAdapter.setListener(new ClaimListAdapter.ClaimListItemListener() {
@Override
public void onClaimClicked(Claim claim) {
String claimId = claim.getClaimId();
String url = claim.getPermanentUrl();
if (claim.getName().startsWith("@")) {
// channel claim
Context context = getContext();
if (context instanceof MainActivity) {
((MainActivity) context).openChannelClaim(claim);
}
} else {
Intent intent = new Intent(getContext(), FileViewActivity.class);
intent.putExtra("claimId", claimId);
intent.putExtra("url", url);
MainActivity.startingFileViewActivity = true;
startActivity(intent);
}
}
});
} else {
contentListAdapter.addItems(claims);
}
if (contentList != null && contentList.getAdapter() == null) {
contentList.setAdapter(contentListAdapter);
}
contentHasReachedEnd = hasReachedEnd;
contentClaimSearchLoading = false;
}
@Override
public void onError(Exception error) {
contentClaimSearchLoading = false;
}
});
contentClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void updateSuggestedDoneButtonText() {
int selected = suggestedChannelAdapter == null ? 0 : suggestedChannelAdapter.getSelectedCount();
int remaining = MIN_SUGGESTED_SUBSCRIBE_COUNT - selected;
String buttonText = remaining <= 0 ? getString(R.string.done) : getString(R.string.n_remaining, remaining);
if (suggestedDoneButton != null) {
suggestedDoneButton.setText(buttonText);
}
}
// handler methods
public void onSuccess(List<Subscription> subscriptions) {
if (subscriptions.size() == 0) {
// fresh start
// TODO: Only do this if there are no local subscriptions stored
currentSuggestedPage = 1;
buildSuggestedOptions();
loadingSuggested = true;
loadingContent = false;
suggestedChannelClaimSearchTask = new ClaimSearchTask(
buildSuggestedOptions(),
Lbry.LBRY_TV_CONNECTION_STRING,
suggestedChannelAdapter == null ? bigContentLoading : contentLoading,
new ClaimSearchTask.ClaimSearchResultHandler() {
@Override
public void onSuccess(List<Claim> claims, boolean hasReachedEnd) {
if (suggestedChannelAdapter == null) {
suggestedChannelAdapter = new SuggestedChannelGridAdapter(claims, getContext());
suggestedChannelAdapter.setListener(FollowingFragment.this);
if (suggestedChannelGrid != null) {
suggestedChannelGrid.setAdapter(suggestedChannelAdapter);
}
} else {
suggestedChannelAdapter.addClaims(claims);
}
}
@Override
public void onError(Exception error) {
}
});
suggestedChannelClaimSearchTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
showSuggestedChannels();
} else {
Lbryio.cacheSubscriptions = subscriptions;
subscriptionsList = new ArrayList<>(subscriptions);
showSubscribedContent();
fetchAndResolveChannelList();
}
}
public void onError(Exception exception) {
}
public void onChannelItemSelected(Claim claim) {
// subscribe
Subscription subscription = new Subscription();
subscription.setChannelName(claim.getName());
subscription.setUrl(claim.getPermanentUrl());
String channelClaimId = claim.getClaimId();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, false, null);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
}
public void onChannelItemDeselected(Claim claim) {
// unsubscribe
Subscription subscription = new Subscription();
subscription.setChannelName(claim.getName());
subscription.setUrl(claim.getPermanentUrl());
String channelClaimId = claim.getClaimId();
ChannelSubscribeTask task = new ChannelSubscribeTask(getContext(), channelClaimId, subscription, true, null);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
updateSuggestedDoneButtonText();
}
public void onChannelSelectionCleared() {
}
}

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