Java tutorial
package com.platform; import android.app.Activity; import android.content.Context; import android.content.pm.ApplicationInfo; import android.net.Uri; import android.util.Log; import com.breadwallet.BuildConfig; import com.breadwallet.R; import com.breadwallet.presenter.activities.MainActivity; import com.breadwallet.tools.crypto.Base58; import com.breadwallet.tools.manager.SharedPreferencesManager; import com.breadwallet.tools.crypto.CryptoHelper; import com.breadwallet.tools.security.KeyStoreManager; import com.breadwallet.tools.util.Utils; import com.breadwallet.wallet.BRWalletManager; import com.jniwrappers.BRBase58; import com.jniwrappers.BRKey; import com.platform.kvstore.RemoteKVStore; import com.platform.kvstore.ReplicatedKVStore; import org.apache.commons.compress.archivers.ArchiveException; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import io.sigpipe.jbsdiff.InvalidHeaderException; import io.sigpipe.jbsdiff.ui.FileUI; import okhttp3.Interceptor; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okio.Buffer; import okio.BufferedSink; import static android.R.attr.key; import static android.R.attr.path; import static com.breadwallet.R.string.request; import static com.breadwallet.R.string.rescan; import static com.breadwallet.tools.util.BRCompressor.gZipExtract; /** * BreadWallet * <p/> * Created by Mihail Gutan on <mihail@breadwallet.com> 9/29/16. * Copyright (c) 2016 breadwallet LLC * <p/> * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * <p/> * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * <p/> * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ public class APIClient { public static final String TAG = APIClient.class.getName(); // proto is the transport protocol to use for talking to the API (either http or https) private static final String PROTO = "https"; // host is the server(s) on which the API is hosted private static final String HOST = "api.breadwallet.com"; // convenience getter for the API endpoint public static final String BASE_URL = PROTO + "://" + HOST; //feePerKb url private static final String FEE_PER_KB_URL = "/v1/fee-per-kb"; //token private static final String TOKEN = "/token"; //me private static final String ME = "/me"; //singleton instance private static APIClient ourInstance; private static final String GET = "GET"; private static final String POST = "POST"; public static final String BUNDLES = "bundles"; // public static final String BREAD_BUY = "bread-buy-staging"; public static String BREAD_BUY = "bread-buy-staging"; public static String bundlesFileName = String.format("/%s", BUNDLES); public static String bundleFileName = String.format("/%s/%s.tar", BUNDLES, BREAD_BUY); public static String extractedFolder = String.format("%s-extracted", BREAD_BUY); public static HTTPServer server; private Activity ctx; public enum FeatureFlags { BUY_BITCOIN("buy-bitcoin"), EARLY_ACCESS("early-access"); private final String text; /** * @param text */ private FeatureFlags(final String text) { this.text = text; } /* (non-Javadoc) * @see java.lang.Enum#toString() */ @Override public String toString() { return text; } } public static synchronized APIClient getInstance(Activity context) { if (ourInstance == null) ourInstance = new APIClient(context); return ourInstance; } private APIClient(Activity context) { ctx = context; if (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) { BREAD_BUY = "bread-buy-staging"; } } private APIClient() { } //returns the fee per kb or 0 if something went wrong public long feePerKb() { try { String strUtl = BASE_URL + FEE_PER_KB_URL; Request request = new Request.Builder().url(strUtl).get().build(); String body = null; try { Response response = sendRequest(request, false, 0); body = response.body().string(); } catch (IOException e) { e.printStackTrace(); } JSONObject object = null; object = new JSONObject(body); return (long) object.getInt("fee_per_kb"); } catch (JSONException e) { e.printStackTrace(); } return 0; } public Response buyBitcoinMe() { if (ctx == null) ctx = MainActivity.app; if (ctx == null) return null; String strUtl = BASE_URL + ME; Request request = new Request.Builder().url(strUtl).get().build(); String response = null; Response res = null; try { res = sendRequest(request, true, 0); response = res.body().string(); if (response.isEmpty()) { res = sendRequest(request, true, 0); response = res.body().string(); } } catch (IOException e) { e.printStackTrace(); } if (response == null) throw new NullPointerException(); return res; } public String getToken() { if (ctx == null) ctx = MainActivity.app; if (ctx == null) return null; try { String strUtl = BASE_URL + TOKEN; JSONObject requestMessageJSON = new JSONObject(); String base58PubKey = BRWalletManager.getAuthPublicKeyForAPI(KeyStoreManager.getAuthKey(ctx)); requestMessageJSON.put("pubKey", base58PubKey); requestMessageJSON.put("deviceID", SharedPreferencesManager.getDeviceId(ctx)); final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); RequestBody requestBody = RequestBody.create(JSON, requestMessageJSON.toString()); Request request = new Request.Builder().url(strUtl).header("Content-Type", "application/json") .header("Accept", "application/json").post(requestBody).build(); String strResponse = null; Response response; try { response = sendRequest(request, false, 0); if (response != null) strResponse = response.body().string(); } catch (IOException e) { e.printStackTrace(); } if (Utils.isNullOrEmpty(strResponse)) { Log.e(TAG, "getToken: retrieving token failed"); return null; } JSONObject obj = null; obj = new JSONObject(strResponse); String token = obj.getString("token"); KeyStoreManager.putToken(token.getBytes(), ctx); return token; } catch (JSONException e) { e.printStackTrace(); } return null; } private String createRequest(String reqMethod, String base58Body, String contentType, String dateHeader, String url) { return (reqMethod == null ? "" : reqMethod) + "\n" + (base58Body == null ? "" : base58Body) + "\n" + (contentType == null ? "" : contentType) + "\n" + (dateHeader == null ? "" : dateHeader) + "\n" + (url == null ? "" : url); } public String signRequest(String request) { byte[] doubleSha256 = CryptoHelper.doubleSha256(request.getBytes(StandardCharsets.UTF_8)); BRKey key = new BRKey(KeyStoreManager.getAuthKey(ctx)); byte[] signedBytes = key.compactSign(doubleSha256); return Base58.encode(signedBytes); } public Response sendRequest(Request locRequest, boolean needsAuth, int retryCount) { if (retryCount > 1) throw new RuntimeException("sendRequest: Warning retryCount is: " + retryCount); boolean isTestVersion = BREAD_BUY.equalsIgnoreCase("bread-buy-staging"); boolean isTestNet = BuildConfig.BITCOIN_TESTNET; Request request = locRequest.newBuilder().header("X-Testflight", isTestVersion ? "1" : "0") .header("X-Bitcoin-Testnet", isTestNet ? "1" : "0").build(); if (needsAuth) { Request.Builder modifiedRequest = request.newBuilder(); String base58Body = ""; RequestBody body = request.body(); try { if (body != null && body.contentLength() != 0) { BufferedSink sink = new Buffer(); try { body.writeTo(sink); } catch (IOException e) { e.printStackTrace(); } String bodyString = sink.buffer().readUtf8(); base58Body = CryptoHelper.base58ofSha256(bodyString.getBytes()); } } catch (IOException e) { e.printStackTrace(); } SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); String httpDate = sdf.format(new Date()); request = modifiedRequest.header("Date", httpDate.substring(0, httpDate.length() - 6)).build(); String queryString = request.url().encodedQuery(); String requestString = createRequest(request.method(), base58Body, request.header("Content-Type"), request.header("Date"), request.url().encodedPath() + ((queryString != null && !queryString.isEmpty()) ? ("?" + queryString) : "")); String signedRequest = signRequest(requestString); String token = new String(KeyStoreManager.getToken(ctx)); if (token.isEmpty()) token = getToken(); if (token == null || token.isEmpty()) { Log.e(TAG, "sendRequest: failed to retrieve token"); return null; } String authValue = "bread " + token + ":" + signedRequest; // Log.e(TAG, "sendRequest: authValue: " + authValue); modifiedRequest = request.newBuilder(); request = modifiedRequest.header("Authorization", authValue).build(); } Response response = null; byte[] data = new byte[0]; try { OkHttpClient client = new OkHttpClient.Builder().followRedirects(false) /*.addInterceptor(new LoggingInterceptor())*/.build(); // Log.e(TAG, "sendRequest: before executing the request: " + request.headers().toString()); response = client.newCall(request).execute(); try { data = response.body().bytes(); } catch (IOException e) { e.printStackTrace(); } if (!response.isSuccessful()) Log.e(TAG, "sendRequest: " + String.format(Locale.getDefault(), "url (%s), code (%d), mess (%s), body (%s)", request.url(), response.code(), response.message(), new String(data))); if (response.isRedirect()) { String newLocation = request.url().scheme() + "://" + request.url().host() + response.header("location"); Uri newUri = Uri.parse(newLocation); if (newUri == null) { Log.e(TAG, "sendRequest: redirect uri is null"); } else if (!newUri.getHost().equalsIgnoreCase(HOST) || !newUri.getScheme().equalsIgnoreCase(PROTO)) { Log.e(TAG, "sendRequest: WARNING: redirect is NOT safe: " + newLocation); } else { Log.w(TAG, "redirecting: " + request.url() + " >>> " + newLocation); return sendRequest(new Request.Builder().url(newLocation).get().build(), needsAuth, 0); } return new Response.Builder().code(500).request(request) .body(ResponseBody.create(null, new byte[0])).protocol(Protocol.HTTP_1_1).build(); } } catch (IOException e) { e.printStackTrace(); return new Response.Builder().code(599).request(request).body(ResponseBody.create(null, new byte[0])) .protocol(Protocol.HTTP_1_1).build(); } if (response.header("content-encoding") != null && response.header("content-encoding").equalsIgnoreCase("gzip")) { Log.d(TAG, "sendRequest: the content is gzip, unzipping"); byte[] decompressed = gZipExtract(data); ResponseBody postReqBody = ResponseBody.create(null, decompressed); return response.newBuilder().body(postReqBody).build(); } ResponseBody postReqBody = ResponseBody.create(null, data); if (needsAuth && isBreadChallenge(response)) { Log.e(TAG, "sendRequest: got authentication challenge from API - will attempt to get token"); getToken(); if (retryCount < 1) { sendRequest(request, true, retryCount + 1); } } return response.newBuilder().body(postReqBody).build(); } public void updateBundle() { File bundleFile = new File(ctx.getFilesDir().getAbsolutePath() + bundleFileName); if (bundleFile.exists()) { Log.d(TAG, "updateBundle: exists"); byte[] bFile = new byte[0]; try { bFile = IOUtils.toByteArray(new FileInputStream(bundleFile)); } catch (IOException e) { e.printStackTrace(); } String latestVersion = getLatestVersion(); String currentTarVersion = null; byte[] hash = CryptoHelper.sha256(bFile); currentTarVersion = Utils.bytesToHex(hash); Log.e(TAG, "updateBundle: version of the current tar: " + currentTarVersion); if (latestVersion != null) { if (latestVersion.equals(currentTarVersion)) { Log.d(TAG, "updateBundle: have the latest version"); tryExtractTar(bundleFile); } else { Log.d(TAG, "updateBundle: don't have the most recent version, download diff"); downloadDiff(bundleFile, currentTarVersion); tryExtractTar(bundleFile); } } else { Log.d(TAG, "updateBundle: latestVersion is null"); } } else { Log.d(TAG, "updateBundle: bundle doesn't exist, downloading new copy"); long startTime = System.currentTimeMillis(); Request request = new Request.Builder() .url(String.format("%s/assets/bundles/%s/download", BASE_URL, BREAD_BUY)).get().build(); Response response = null; response = sendRequest(request, false, 0); Log.d(TAG, "updateBundle: Downloaded, took: " + (System.currentTimeMillis() - startTime)); writeBundleToFile(response, bundleFile); tryExtractTar(bundleFile); } } public String getLatestVersion() { String latestVersion = null; String response = null; try { response = sendRequest( new Request.Builder().get() .url(String.format("%s/assets/bundles/%s/versions", BASE_URL, BREAD_BUY)).build(), false, 0).body().string(); } catch (IOException e) { e.printStackTrace(); } String respBody; respBody = response; try { JSONObject versionsJson = new JSONObject(respBody); JSONArray jsonArray = versionsJson.getJSONArray("versions"); if (jsonArray.length() == 0) return null; latestVersion = (String) jsonArray.get(jsonArray.length() - 1); } catch (JSONException e) { e.printStackTrace(); } return latestVersion; } public void downloadDiff(File bundleFile, String currentTarVersion) { Request diffRequest = new Request.Builder() .url(String.format("%s/assets/bundles/%s/diff/%s", BASE_URL, BREAD_BUY, currentTarVersion)).get() .build(); Response diffResponse = sendRequest(diffRequest, false, 0); File patchFile = null; File tempFile = null; byte[] patchBytes = null; try { patchFile = new File(String.format("/%s/%s.diff", BUNDLES, "patch")); patchBytes = diffResponse.body().bytes(); FileUtils.writeByteArrayToFile(patchFile, patchBytes); String compression = System.getProperty("jbsdiff.compressor", "tar"); compression = compression.toLowerCase(); tempFile = new File(String.format("/%s/%s.tar", BUNDLES, "temp")); FileUI.diff(bundleFile, tempFile, patchFile, compression); byte[] updatedBundleBytes = IOUtils.toByteArray(new FileInputStream(tempFile)); FileUtils.writeByteArrayToFile(bundleFile, updatedBundleBytes); } catch (IOException | InvalidHeaderException | CompressorException | NullPointerException e) { e.printStackTrace(); } finally { if (patchFile != null) patchFile.delete(); if (tempFile != null) tempFile.delete(); } } public byte[] writeBundleToFile(Response response, File bundleFile) { byte[] bodyBytes; FileOutputStream fileOutputStream = null; assert (response != null); try { if (response == null) { Log.e(TAG, "writeBundleToFile: WARNING, response is null"); return null; } bodyBytes = response.body().bytes(); FileUtils.writeByteArrayToFile(bundleFile, bodyBytes); return bodyBytes; } catch (IOException e) { e.printStackTrace(); } finally { try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } public boolean tryExtractTar(File inputFile) { String extractFolderName = MainActivity.app.getFilesDir().getAbsolutePath() + bundlesFileName + "/" + extractedFolder; boolean result = false; TarArchiveInputStream debInputStream = null; try { final InputStream is = new FileInputStream(inputFile); debInputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is); TarArchiveEntry entry = null; while ((entry = (TarArchiveEntry) debInputStream.getNextEntry()) != null) { final String outPutFileName = entry.getName().replace("./", ""); final File outputFile = new File(extractFolderName, outPutFileName); if (!entry.isDirectory()) { FileUtils.writeByteArrayToFile(outputFile, org.apache.commons.compress.utils.IOUtils.toByteArray(debInputStream)); } } result = true; } catch (Exception e) { e.printStackTrace(); } finally { try { if (debInputStream != null) debInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } return result; } public void updateFeatureFlag() { String furl = "/me/features"; Request req = new Request.Builder().url(buildUrl(furl)).get().build(); Response res = sendRequest(req, true, 0); if (res == null) { Log.e(TAG, "updateFeatureFlag: error fetching features"); return; } if (!res.isSuccessful()) { Log.e(TAG, "updateFeatureFlag: request was unsuccessful: " + res.code() + ":" + res.message()); return; } try { String j = res.body().string(); if (j.isEmpty()) { Log.e(TAG, "updateFeatureFlag: JSON empty"); return; } JSONArray arr = new JSONArray(j); for (int i = 0; i < arr.length(); i++) { try { JSONObject obj = arr.getJSONObject(i); String name = obj.getString("name"); String description = obj.getString("description"); boolean selected = obj.getBoolean("selected"); boolean enabled = obj.getBoolean("enabled"); SharedPreferencesManager.putFeatureEnabled(ctx, enabled, name); } catch (Exception e) { Log.e(TAG, "malformed feature at position: " + i + ", whole json: " + j, e); } } } catch (IOException | JSONException e) { Log.e(TAG, "updateFeatureFlag: failed to pull up features"); e.printStackTrace(); } } public boolean isBreadChallenge(Response resp) { String challenge = resp.header("www-authenticate"); return challenge != null && challenge.startsWith("bread"); } public boolean isFeatureEnabled(String feature) { return SharedPreferencesManager.getFeatureEnabled(ctx, feature); } public String buildUrl(String path) { return BASE_URL + path; } private class LoggingInterceptor implements Interceptor { @Override public Response intercept(Interceptor.Chain chain) throws IOException { Request request = chain.request(); long t1 = System.nanoTime(); Log.d(TAG, String.format("Sending request %s on %s%n%s", request.url(), chain.connection(), request.headers())); Response response = chain.proceed(request); long t2 = System.nanoTime(); Log.d(TAG, String.format("Received response for %s in %.1fms%n%s", response.request().url(), (t2 - t1) / 1e6d, response.headers())); return response; } } public void updatePlatform() { if (BuildConfig.DEBUG) { final long startTime = System.currentTimeMillis(); Log.d(TAG, "updatePlatform: updating platform..."); new Thread(new Runnable() { @Override public void run() { APIClient apiClient = APIClient.getInstance(ctx); apiClient.updateBundle(); //bread-buy-staging apiClient.updateFeatureFlag(); apiClient.syncKvStore(); long endTime = System.currentTimeMillis(); Log.e(TAG, "updatePlatform: DONE in " + (endTime - startTime) + "ms"); } }).start(); } } public void syncKvStore() { final APIClient client = this; final long startTime = System.currentTimeMillis(); Log.d(TAG, "syncKvStore: DEBUG, syncing kv store..."); //sync the kv stores RemoteKVStore remoteKVStore = RemoteKVStore.getInstance(client); ReplicatedKVStore kvStore = new ReplicatedKVStore(ctx, remoteKVStore); kvStore.syncAllKeys(); long endTime = System.currentTimeMillis(); Log.d(TAG, "syncKvStore: DONE in " + (endTime - startTime) + "ms"); } }