mirror of
https://github.com/LBRYFoundation/lbry-android.git
synced 2025-08-23 17:47:28 +00:00
545 lines
21 KiB
Java
545 lines
21 KiB
Java
package io.lbry.browser.utils;
|
|
|
|
import android.util.Log;
|
|
|
|
import org.bitcoinj.core.Base58;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONException;
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.IOException;
|
|
import java.security.KeyStore;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.Iterator;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Random;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import io.lbry.browser.exceptions.ApiCallException;
|
|
import io.lbry.browser.exceptions.LbryRequestException;
|
|
import io.lbry.browser.exceptions.LbryResponseException;
|
|
import io.lbry.browser.model.Claim;
|
|
import io.lbry.browser.model.ClaimCacheKey;
|
|
import io.lbry.browser.model.ClaimSearchCacheValue;
|
|
import io.lbry.browser.model.LbryFile;
|
|
import io.lbry.browser.model.Tag;
|
|
import io.lbry.browser.model.Transaction;
|
|
import io.lbry.browser.model.WalletBalance;
|
|
import io.lbry.lbrysdk.Utils;
|
|
import kotlin.Pair;
|
|
import okhttp3.CacheControl;
|
|
import okhttp3.Headers;
|
|
import okhttp3.OkHttpClient;
|
|
import okhttp3.Request;
|
|
import okhttp3.RequestBody;
|
|
import okhttp3.Response;
|
|
|
|
public final class Lbry {
|
|
private static final Object lock = new Object();
|
|
public static LinkedHashMap<ClaimCacheKey, Claim> claimCache = new LinkedHashMap<>();
|
|
public static LinkedHashMap<Map<String, Object>, ClaimSearchCacheValue> claimSearchCache = new LinkedHashMap<>();
|
|
public static WalletBalance walletBalance = new WalletBalance();
|
|
public static List<Tag> knownTags = new ArrayList<>();
|
|
public static List<Tag> followedTags = new ArrayList<>();
|
|
public static List<Claim> ownClaims = new ArrayList<>();
|
|
public static List<Claim> ownChannels = new ArrayList<>(); // Make this a subset of ownClaims?
|
|
public static List<String> abandonedClaimIds = new ArrayList<>();
|
|
|
|
public static final int TTL_CLAIM_SEARCH_VALUE = 120000; // 2-minute TTL for cache
|
|
public static final String SDK_CONNECTION_STRING = "http://127.0.0.1:5279";
|
|
public static final String LBRY_TV_CONNECTION_STRING = "https://api.lbry.tv/api/v1/proxy";
|
|
public static final String TAG = "Lbry";
|
|
|
|
// Values to obtain from LBRY SDK status
|
|
public static boolean IS_STATUS_PARSED = false; // Check if the status has been parsed at least once
|
|
public static final String PLATFORM = String.format("Android %s (API %d)", Utils.getAndroidRelease(), Utils.getAndroidSdk());
|
|
public static final String OS = "android";
|
|
public static String INSTALLATION_ID = null;
|
|
public static String NODE_ID = null;
|
|
public static String DAEMON_VERSION = null;
|
|
|
|
// JSON RPC API Call methods
|
|
public static final String METHOD_RESOLVE = "resolve";
|
|
public static final String METHOD_CLAIM_SEARCH = "claim_search";
|
|
public static final String METHOD_FILE_LIST = "file_list";
|
|
public static final String METHOD_FILE_DELETE = "file_delete";
|
|
public static final String METHOD_GET = "get";
|
|
public static final String METHOD_PUBLISH = "publish";
|
|
|
|
public static final String METHOD_WALLET_BALANCE = "wallet_balance";
|
|
public static final String METHOD_WALLET_ENCRYPT = "wallet_encrypt";
|
|
public static final String METHOD_WALLET_DECRYPT = "wallet_decrypt";
|
|
public static final String METHOD_VERSION = "version";
|
|
|
|
public static final String METHOD_WALLET_LIST = "wallet_list";
|
|
public static final String METHOD_WALLET_SEND = "wallet_send";
|
|
public static final String METHOD_WALLET_STATUS = "wallet_status";
|
|
public static final String METHOD_WALLET_UNLOCK = "wallet_unlock";
|
|
public static final String METHOD_ADDRESS_IS_MINE = "address_is_mine";
|
|
public static final String METHOD_ADDRESS_UNUSED = "address_unused";
|
|
public static final String METHOD_ADDRESS_LIST = "address_list";
|
|
public static final String METHOD_TRANSACTION_LIST = "transaction_list";
|
|
public static final String METHOD_UTXO_RELEASE = "utxo_release";
|
|
public static final String METHOD_SUPPORT_CREATE = "support_create";
|
|
public static final String METHOD_SUPPORT_ABANDON = "support_abandon";
|
|
public static final String METHOD_SYNC_HASH = "sync_hash";
|
|
public static final String METHOD_SYNC_APPLY = "sync_apply";
|
|
public static final String METHOD_PREFERENCE_GET = "preference_get";
|
|
public static final String METHOD_PREFERENCE_SET = "preference_set";
|
|
|
|
public static final String METHOD_COMMENT_CREATE = "comment_create";
|
|
|
|
public static final String METHOD_TXO_LIST = "txo_list";
|
|
public static final String METHOD_TXO_SPEND = "txo_spend";
|
|
|
|
public static final String METHOD_CHANNEL_ABANDON = "channel_abandon";
|
|
public static final String METHOD_CHANNEL_CREATE = "channel_create";
|
|
public static final String METHOD_CHANNEL_UPDATE = "channel_update";
|
|
|
|
|
|
public static final String METHOD_CLAIM_LIST = "claim_list";
|
|
public static final String METHOD_PURCHASE_LIST = "purchase_list";
|
|
public static final String METHOD_STREAM_ABANDON = "stream_abandon";
|
|
public static final String METHOD_STREAM_REPOST = "stream_repost";
|
|
|
|
public static final String METHOD_COMMENT_LIST = "comment_list";
|
|
|
|
public static KeyStore KEYSTORE;
|
|
public static boolean SDK_READY = false;
|
|
|
|
public static void startupInit() {
|
|
abandonedClaimIds = new ArrayList<>();
|
|
ownChannels = new ArrayList<>();
|
|
ownClaims = new ArrayList<>();
|
|
knownTags = new ArrayList<>();
|
|
followedTags = new ArrayList<>();
|
|
}
|
|
|
|
public static void parseStatus(String response) {
|
|
try {
|
|
JSONObject json = parseSdkResponse(response);
|
|
INSTALLATION_ID = json.getString("installation_id");
|
|
if (json.has("lbry_id")) {
|
|
// if DHT is not enabled, lbry_id won't be set
|
|
NODE_ID = json.getString("lbry_id");
|
|
}
|
|
IS_STATUS_PARSED = true;
|
|
} catch (JSONException | LbryResponseException ex) {
|
|
// pass
|
|
Log.e(TAG, "Could not parse status response.", ex);
|
|
}
|
|
}
|
|
|
|
public static Response apiCall(String method) throws LbryRequestException {
|
|
return apiCall(method, null);
|
|
}
|
|
|
|
public static Response apiCall(String method, Map<String, Object> params) throws LbryRequestException {
|
|
return apiCall(method, params, SDK_CONNECTION_STRING);
|
|
}
|
|
|
|
public static Response apiCall(String method, Map<String, Object> params, String connectionString) throws LbryRequestException {
|
|
long counter = new Double(System.currentTimeMillis() / 1000.0).longValue();
|
|
JSONObject requestParams = buildJsonParams(params);
|
|
JSONObject requestBody = new JSONObject();
|
|
try {
|
|
requestBody.put("jsonrpc", "2.0");
|
|
requestBody.put("method", method);
|
|
requestBody.put("params", requestParams);
|
|
requestBody.put("counter", counter);
|
|
} catch (JSONException ex) {
|
|
throw new LbryRequestException("Could not build the JSON request body.", ex);
|
|
}
|
|
|
|
RequestBody body = RequestBody.create(requestBody.toString(), Helper.JSON_MEDIA_TYPE);
|
|
Request request = new Request.Builder().url(connectionString).post(body).build();
|
|
OkHttpClient client = new OkHttpClient.Builder().
|
|
writeTimeout(300, TimeUnit.SECONDS).
|
|
readTimeout(300, TimeUnit.SECONDS).
|
|
build();
|
|
|
|
try {
|
|
return client.newCall(request).execute();
|
|
} catch (IOException ex) {
|
|
throw new LbryRequestException(String.format("\"%s\" method to %s failed", method, connectionString), ex);
|
|
}
|
|
}
|
|
|
|
public static JSONObject buildJsonParams(Map<String, Object> params) {
|
|
JSONObject jsonParams = new JSONObject();
|
|
if (params != null) {
|
|
try {
|
|
for (Map.Entry<String, Object> param : params.entrySet()) {
|
|
Object value = param.getValue();
|
|
if (value instanceof List) {
|
|
value = Helper.jsonArrayFromList((List) value);
|
|
}
|
|
if (value instanceof Double) {
|
|
jsonParams.put(param.getKey(), (double) value);
|
|
} else {
|
|
jsonParams.put(param.getKey(), value == null ? JSONObject.NULL : value);
|
|
}
|
|
}
|
|
} catch (JSONException ex) {
|
|
// pass
|
|
}
|
|
}
|
|
|
|
return jsonParams;
|
|
}
|
|
|
|
public static Object parseResponse(Response response) throws LbryResponseException {
|
|
String responseString = null;
|
|
try {
|
|
responseString = response.body().string();
|
|
JSONObject json = new JSONObject(responseString);
|
|
if (response.code() >= 200 && response.code() < 300) {
|
|
if (json.has("result")) {
|
|
if (json.isNull("result")) {
|
|
return null;
|
|
}
|
|
return json.get("result");
|
|
} else {
|
|
processErrorJson(json);
|
|
}
|
|
}
|
|
|
|
processErrorJson(json);
|
|
} catch (JSONException | IOException ex) {
|
|
throw new LbryResponseException(String.format("Could not parse response: %s", responseString), ex);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void processErrorJson(JSONObject json) throws JSONException, LbryResponseException {
|
|
if (json.has("error")) {
|
|
String errorMessage = null;
|
|
Object jsonError = json.get("error");
|
|
if (jsonError instanceof String) {
|
|
errorMessage = jsonError.toString();
|
|
} else {
|
|
errorMessage = ((JSONObject) jsonError).getString("message");
|
|
}
|
|
throw new LbryResponseException(!Helper.isNullOrEmpty(errorMessage) ? errorMessage : json.getString("error"));
|
|
} else {
|
|
throw new LbryResponseException("Protocol error with unknown response signature.");
|
|
}
|
|
}
|
|
|
|
public static JSONObject parseSdkResponse(String responseString) throws LbryResponseException {
|
|
try {
|
|
JSONObject json = new JSONObject(responseString);
|
|
if (json.has("error")) {
|
|
String errorMessage = null;
|
|
Object jsonError = json.get("error");
|
|
if (jsonError instanceof String) {
|
|
errorMessage = jsonError.toString();
|
|
} else {
|
|
errorMessage = ((JSONObject) jsonError).getString("message");
|
|
}
|
|
throw new LbryResponseException(json.getString("error"));
|
|
}
|
|
|
|
return json;
|
|
} catch (JSONException ex) {
|
|
throw new LbryResponseException(String.format("Could not parse response: %s", responseString), ex);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API Calls
|
|
*/
|
|
public static Claim resolve(String url, String connectionString) throws ApiCallException {
|
|
List<Claim> results = resolve(Arrays.asList(url), connectionString);
|
|
return results.size() > 0 ? results.get(0) : null;
|
|
}
|
|
public static List<Claim> resolve(List<String> urls, String connectionString) throws ApiCallException {
|
|
List<Claim> claims = new ArrayList<>();
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("urls", urls);
|
|
try {
|
|
JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_RESOLVE, params, connectionString));
|
|
Iterator<String> keys = result.keys();
|
|
if (keys != null) {
|
|
while (keys.hasNext()) {
|
|
Claim claim = Claim.fromJSONObject(result.getJSONObject(keys.next()));
|
|
claims.add(claim);
|
|
|
|
addClaimToCache(claim);
|
|
}
|
|
}
|
|
} catch (LbryRequestException | LbryResponseException | JSONException ex) {
|
|
throw new ApiCallException("Could not execute resolve call", ex);
|
|
}
|
|
|
|
return claims;
|
|
}
|
|
public static List<Transaction> transactionList(int page, int pageSize) throws ApiCallException {
|
|
List<Transaction> transactions = new ArrayList<>();
|
|
Map<String, Object> params = new HashMap<>();
|
|
if (page > 0) {
|
|
params.put("page", page);
|
|
}
|
|
if (pageSize > 0) {
|
|
params.put("page_size", pageSize);
|
|
}
|
|
try {
|
|
JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_TRANSACTION_LIST, params, SDK_CONNECTION_STRING));
|
|
JSONArray items = result.getJSONArray("items");
|
|
for (int i = 0; i < items.length(); i++) {
|
|
Transaction tx = Transaction.fromJSONObject(items.getJSONObject(i));
|
|
transactions.add(tx);
|
|
}
|
|
} catch (LbryRequestException | LbryResponseException | JSONException ex) {
|
|
throw new ApiCallException("Could not execute transaction_list call", ex);
|
|
}
|
|
|
|
return transactions;
|
|
}
|
|
|
|
public static LbryFile get(boolean saveFile) throws ApiCallException {
|
|
LbryFile file = null;
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put("save_file", saveFile);
|
|
try {
|
|
JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_GET, params));
|
|
file = LbryFile.fromJSONObject(result);
|
|
|
|
if (file != null) {
|
|
String fileClaimId = file.getClaimId();
|
|
if (!Helper.isNullOrEmpty(fileClaimId)) {
|
|
ClaimCacheKey key = new ClaimCacheKey();
|
|
key.setClaimId(fileClaimId);
|
|
if (claimCache.containsKey(key)) {
|
|
claimCache.get(key).setFile(file);
|
|
}
|
|
}
|
|
}
|
|
} catch (LbryRequestException | LbryResponseException ex) {
|
|
throw new ApiCallException("Could not execute resolve call", ex);
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
public static List<LbryFile> fileList(String claimId, boolean downloads, int page, int pageSize) throws ApiCallException {
|
|
List<LbryFile> files = new ArrayList<>();
|
|
Map<String, Object> params = new HashMap<>();
|
|
if (!Helper.isNullOrEmpty(claimId)) {
|
|
params.put("claim_id", claimId);
|
|
}
|
|
if (downloads) {
|
|
params.put("download_path", null);
|
|
params.put("comparison", "ne");
|
|
}
|
|
if (page > 0) {
|
|
params.put("page", page);
|
|
}
|
|
if (pageSize > 0) {
|
|
params.put("page_size", pageSize);
|
|
}
|
|
try {
|
|
JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_FILE_LIST, params));
|
|
JSONArray items = result.getJSONArray("items");
|
|
for (int i = 0; i < items.length(); i++) {
|
|
JSONObject fileObject = items.getJSONObject(i);
|
|
LbryFile file = LbryFile.fromJSONObject(fileObject);
|
|
files.add(file);
|
|
|
|
String fileClaimId = file.getClaimId();
|
|
if (!Helper.isNullOrEmpty(fileClaimId)) {
|
|
ClaimCacheKey key = new ClaimCacheKey();
|
|
key.setClaimId(fileClaimId);
|
|
if (claimCache.containsKey(key)) {
|
|
claimCache.get(key).setFile(file);
|
|
}
|
|
}
|
|
}
|
|
} catch (LbryRequestException | LbryResponseException | JSONException ex) {
|
|
throw new ApiCallException("Could not execute resolve call", ex);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
private static final String[] listParamTypes = new String[] {
|
|
"any_tags", "channel_ids", "order_by", "not_tags", "not_channel_ids", "urls"
|
|
};
|
|
|
|
public static Map<String, Object> buildClaimSearchOptions(
|
|
String claimType,
|
|
List<String> anyTags,
|
|
List<String> notTags,
|
|
List<String> channelIds,
|
|
List<String> notChannelIds,
|
|
List<String> orderBy,
|
|
String releaseTime,
|
|
int page,
|
|
int pageSize) {
|
|
return buildClaimSearchOptions(
|
|
Collections.singletonList(claimType),
|
|
anyTags,
|
|
notTags,
|
|
channelIds,
|
|
notChannelIds,
|
|
orderBy,
|
|
releaseTime,
|
|
page,
|
|
pageSize);
|
|
}
|
|
|
|
public static Map<String, Object> buildClaimSearchOptions(
|
|
List<String> claimType,
|
|
List<String> anyTags,
|
|
List<String> notTags,
|
|
List<String> channelIds,
|
|
List<String> notChannelIds,
|
|
List<String> orderBy,
|
|
String releaseTime,
|
|
int page,
|
|
int pageSize) {
|
|
Map<String, Object> options = new HashMap<>();
|
|
if (claimType != null && claimType.size() > 0) {
|
|
options.put("claim_type", claimType);
|
|
}
|
|
options.put("no_totals", true);
|
|
options.put("page", page);
|
|
options.put("page_size", pageSize);
|
|
if (!Helper.isNullOrEmpty(releaseTime)) {
|
|
options.put("release_time", releaseTime);
|
|
}
|
|
|
|
addClaimSearchListOption("any_tags", anyTags, options);
|
|
addClaimSearchListOption("not_tags", notTags, options);
|
|
addClaimSearchListOption("channel_ids", channelIds, options);
|
|
addClaimSearchListOption("not_channel_ids", notChannelIds, options);
|
|
addClaimSearchListOption("order_by", orderBy, options);
|
|
|
|
return options;
|
|
}
|
|
|
|
private static void addClaimSearchListOption(String key, List<String> list, Map<String, Object> options) {
|
|
if (list != null && list.size() > 0) {
|
|
options.put(key, list);
|
|
}
|
|
}
|
|
|
|
public static List<Claim> claimSearch(Map<String, Object> options, String connectionString) throws ApiCallException {
|
|
if (claimSearchCache.containsKey(options)) {
|
|
ClaimSearchCacheValue value = claimSearchCache.get(options);
|
|
if (!value.isExpired(TTL_CLAIM_SEARCH_VALUE)) {
|
|
return claimSearchCache.get(options).getClaims();
|
|
}
|
|
}
|
|
|
|
List<Claim> claims = new ArrayList<>();
|
|
try {
|
|
JSONObject result = (JSONObject) parseResponse(apiCall(METHOD_CLAIM_SEARCH, options, connectionString));
|
|
JSONArray items = result.getJSONArray("items");
|
|
if (items != null) {
|
|
for (int i = 0; i < items.length(); i++) {
|
|
Claim claim = Claim.fromJSONObject(items.getJSONObject(i));
|
|
claims.add(claim);
|
|
|
|
addClaimToCache(claim);
|
|
}
|
|
}
|
|
|
|
claimSearchCache.put(options, new ClaimSearchCacheValue(claims, System.currentTimeMillis()));
|
|
} catch (LbryRequestException | LbryResponseException | JSONException ex) {
|
|
throw new ApiCallException("Could not execute resolve call", ex);
|
|
}
|
|
|
|
return claims;
|
|
}
|
|
|
|
public static Map<String, Object> buildSingleParam(String key, Object value) {
|
|
Map<String, Object> params = new HashMap<>();
|
|
params.put(key, value);
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Call to return a generic JSONObject which can be further parsed as required
|
|
* @param method
|
|
* @param params
|
|
* @return
|
|
*/
|
|
public static Object genericApiCall(String method, Map<String, Object> params) throws ApiCallException {
|
|
Object response = null;
|
|
try {
|
|
response = parseResponse(apiCall(method, params));
|
|
} catch (LbryRequestException | LbryResponseException ex) {
|
|
throw new ApiCallException(String.format("Could not execute %s call: %s", method, ex.getMessage()), ex);
|
|
}
|
|
return response;
|
|
}
|
|
public static Object genericApiCall(String method) throws ApiCallException {
|
|
return genericApiCall(method, null);
|
|
}
|
|
public static void addFollowedTag(Tag tag) {
|
|
synchronized (lock) {
|
|
if (!followedTags.contains(tag)) {
|
|
followedTags.add(tag);
|
|
}
|
|
}
|
|
}
|
|
public static void removeFollowedTag(Tag tag) {
|
|
synchronized (lock) {
|
|
followedTags.remove(tag);
|
|
}
|
|
}
|
|
public static void addKnownTag(Tag tag) {
|
|
synchronized (lock) {
|
|
if (!knownTags.contains(tag)) {
|
|
knownTags.add(tag);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void addClaimToCache(Claim claim) {
|
|
ClaimCacheKey fullKey = ClaimCacheKey.fromClaim(claim);
|
|
ClaimCacheKey shortUrlKey = ClaimCacheKey.fromClaimShortUrl(claim);
|
|
ClaimCacheKey permanentUrlKey = ClaimCacheKey.fromClaimPermanentUrl(claim);
|
|
claimCache.put(fullKey, claim);
|
|
claimCache.put(permanentUrlKey, claim);
|
|
if (!Helper.isNullOrEmpty(shortUrlKey.getUrl())) {
|
|
claimCache.put(shortUrlKey, claim);
|
|
}
|
|
}
|
|
|
|
public static void unsetFilesForCachedClaims(List<String> claimIds) {
|
|
for (String claimId : claimIds) {
|
|
ClaimCacheKey key = new ClaimCacheKey();
|
|
key.setClaimId(claimId);
|
|
if (claimCache.containsKey(key)) {
|
|
claimCache.get(key).setFile(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static String generateId() {
|
|
return generateId(64);
|
|
}
|
|
public static String generateId(int numBytes) {
|
|
byte[] arr = new byte[numBytes];
|
|
new Random().nextBytes(arr);
|
|
try {
|
|
MessageDigest md = MessageDigest.getInstance("SHA-384");
|
|
byte[] hash = md.digest(arr);
|
|
return Base58.encode(hash);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
// pass
|
|
return null;
|
|
}
|
|
}
|
|
}
|