From 83d10dd6396a9f48cbec7bf96e04bb57ccb22be0 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 02:31:07 -0700 Subject: [PATCH 01/10] Add Kotlin-based LanternHttpClient --- .../lantern/model/LanternHttpClient.java | 494 ------------------ .../kotlin/io/lantern/model/SessionModel.kt | 9 +- .../org/getlantern/lantern/LanternApp.kt | 2 +- .../org/getlantern/lantern/MainActivity.kt | 4 +- .../lantern/service/LanternService.kt | 2 +- .../lantern/util/LanternHttpClient.kt | 152 ++++++ 6 files changed, 160 insertions(+), 503 deletions(-) delete mode 100644 android/app/src/main/java/org/getlantern/lantern/model/LanternHttpClient.java create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt diff --git a/android/app/src/main/java/org/getlantern/lantern/model/LanternHttpClient.java b/android/app/src/main/java/org/getlantern/lantern/model/LanternHttpClient.java deleted file mode 100644 index ae36661fe..000000000 --- a/android/app/src/main/java/org/getlantern/lantern/model/LanternHttpClient.java +++ /dev/null @@ -1,494 +0,0 @@ -package org.getlantern.lantern.model; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.reflect.TypeToken; - -import org.getlantern.lantern.LanternApp; -import org.getlantern.lantern.util.Json; -import org.getlantern.mobilesdk.Logger; -import org.getlantern.mobilesdk.util.HttpClient; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import okhttp3.CacheControl; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.FormBody; -import okhttp3.Headers; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okio.Buffer; - -/** - * An OkHttp-based HTTP client. - */ -public class LanternHttpClient extends HttpClient { - private static final String TAG = LanternHttpClient.class.getName(); - - // the standard user headers sent with most Pro requests - private static final String DEVICE_ID_HEADER = "X-Lantern-Device-Id"; - private static final String USER_ID_HEADER = "X-Lantern-User-Id"; - private static final String PRO_TOKEN_HEADER = "X-Lantern-Pro-Token"; - private static final String APP_VERSION_HEADER = "X-Lantern-Version"; - private static final String PLATFORM_HEADER = "X-Lantern-Platform"; - - private static final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - - /** - * Creates a new HTTP client - */ - public LanternHttpClient() { - super(); - } - - /** - * Creates a new HTTP client - * - * @param httpClient The HTTP client to use. - */ - public LanternHttpClient(final OkHttpClient httpClient) { - super(httpClient); - } - - public static HttpUrl createProUrl(final String uri) { - return createProUrl(uri, null); - } - - /** - * Constructs a url for a request to the pro server - * - * @param uri the requested resource - * @param params any query params to include with the url - * @return a URL - */ - public static HttpUrl createProUrl(final String uri, final Map params) { - final String url = String.format("http://localhost/pro%s", uri); - HttpUrl.Builder builder = HttpUrl.parse(url).newBuilder(); - if (params != null) { - for (Map.Entry param : params.entrySet()) { - builder.addQueryParameter(param.getKey(), param.getValue()); - } - } - return builder.build(); - } - - /** - * The HTTP headers expected with Pro requests for a user - */ - private Map userHeaders() { - final Map headers = new HashMap(); - headers.put(DEVICE_ID_HEADER, LanternApp.getSession().getDeviceID()); - headers.put(PRO_TOKEN_HEADER, LanternApp.getSession().getToken()); - headers.put(USER_ID_HEADER, String.valueOf(LanternApp.getSession().getUserID())); - headers.put(PLATFORM_HEADER, "android"); - headers.put(APP_VERSION_HEADER, Utils.appVersion(LanternApp.getAppContext())); - headers.putAll(LanternApp.getSession().getInternalHeaders()); - return headers; - } - - public void request(@NonNull final String method, @NonNull final HttpUrl url, - final HttpCallback cb) { - request(method, url, null, null, cb); - } - - public void request(@NonNull final String method, @NonNull final HttpUrl url, - final ProCallback cb) { - proRequest(method, url, null, null, cb); - } - - public void request(@NonNull final String method, @NonNull final HttpUrl url, - final boolean addProHeaders, - RequestBody body, final HttpCallback cb) { - if (addProHeaders) { - request(method, url, userHeaders(), body, cb); - } else { - request(method, url, null, body, cb); - } - } - - public void request(@NonNull final String method, @NonNull final HttpUrl url, - RequestBody body, final ProCallback cb) { - proRequest(method, url, userHeaders(), body, cb); - } - - /** - * GET request. - * - * @param url request URL - * @param cb for notifying the caller of an HTTP response or failure - */ - public void get(@NonNull final HttpUrl url, final ProCallback cb) { - proRequest("GET", url, userHeaders(), null, cb); - } - - /** - * POST request. - * - * @param url request URL - * @param body the data enclosed with the HTTP message - * @param cb the callback responded with an HTTP response or failure - */ - public void post(@NonNull final HttpUrl url, - final RequestBody body, @NonNull final ProCallback cb) { - proRequest("POST", url, userHeaders(), body, cb); - } - - private void processPlans(List fetched, final PlansCallback cb, InAppBilling inAppBilling) { - Map plans = new HashMap(); - Logger.debug(TAG, "Pro plans: " + fetched); - for (ProPlan plan : fetched) { - if (plan != null) { - plan.formatCost(); - Logger.debug(TAG, "New plan is " + plan); - plans.put(plan.getId(), plan); - } - } - if (inAppBilling != null) { - // this means we're in the play store, use the configured plans from there but with the - // renewal bonus from the server side plans - Map regularPlans = new HashMap<>(); - for (Map.Entry entry : plans.entrySet()) { - // Plans from the pro server have a version suffix, like '1y-usd-9' but plans from - // the Play Store don't, like '1y-usd'. So we normalize by dropping the version - // suffix. - regularPlans.put(entry.getKey().substring(0, entry.getKey().lastIndexOf("-")), entry.getValue()); - } - plans = inAppBilling.getPlans(); - for (Map.Entry entry : plans.entrySet()) { - ProPlan regularPlan = regularPlans.get(entry.getKey()); - if (regularPlan != null) { - entry.getValue().updateRenewalBonusExpected(regularPlan.getRenewalBonusExpected()); - } - } - } - cb.onSuccess(plans); - } - - private void processPlansV1(final JsonObject result, final PlansCallback cb, InAppBilling inAppBilling) { - String stripePubKey = result.get("providers").getAsJsonObject().get("stripe").getAsJsonObject().get("pubKey").getAsString(); - LanternApp.getSession().setStripePubKey(stripePubKey); - Type listType = new TypeToken>() { - }.getType(); - Logger.debug(TAG, "Plans: " + result.get("plans")); - final List fetched = Json.gson.fromJson(result.get("plans"), listType); - processPlans(fetched, cb, inAppBilling); - } - - private void processPlansV3(final JsonObject result, final PlansV3Callback cb, InAppBilling inAppBilling) { - Type mapType = new TypeToken>>() { - }.getType(); - Map> response = Json.gson.fromJson(result.get("providers"), mapType); - List providers = response.get("android"); - Type listType = new TypeToken>() { - }.getType(); - final List fetched = Json.gson.fromJson(result.get("plans"), listType); - Logger.debug(TAG, "Payment providers: " + providers); - Map plans = new HashMap(); - for (ProPlan plan : fetched) { - if (plan != null) { - plan.formatCost(); - plans.put(plan.getId(), plan); - } - } - cb.onSuccess(plans, providers); - } - - public void sendLinkRequest(final ProCallback cb) { - final HttpUrl url = createProUrl("/user-link-request"); - final RequestBody formBody = new FormBody.Builder() - .add("email", LanternApp.getSession().email()) - .add("deviceName", LanternApp.getSession().deviceName()) - .build(); - - post(url, formBody, - new LanternHttpClient.ProCallback() { - @Override - public void onFailure(final Throwable throwable, final ProError error) { - if (cb != null) { - cb.onFailure(throwable, error); - } - } - - @Override - public void onSuccess(final Response response, final JsonObject result) { - if (result.get("error") != null) { - onFailure(null, new ProError(result)); - } else if (cb != null) { - cb.onSuccess(response, result); - } - } - }); - } - - /** - * Returns all user data, including payments, referrals, and all available - * fields. - * - * @param cb for notifying the caller of an HTTP response or failure - */ - public void userData(final ProUserCallback cb) { - final Map params = new HashMap(); - params.put("locale", LanternApp.getSession().getLanguage()); - final HttpUrl url = createProUrl("/user-data", params); - get(url, new ProCallback() { - @Override - public void onFailure(final Throwable throwable, final ProError error) { - Logger.error(TAG, "Unable to fetch user data", throwable); - if (cb != null) { - cb.onFailure(throwable, error); - } - } - - @Override - public void onSuccess(final Response response, final JsonObject result) { - try { - Logger.debug(TAG, "JSON response" + result.toString()); - final ProUser user = Json.gson.fromJson(result, ProUser.class); - if (user != null) { - Logger.debug(TAG, "User ID is " + user.getUserId()); - LanternApp.getSession().storeUserData(user); - } - if (cb != null) { - cb.onSuccess(response, user); - } - } catch (Exception e) { - Logger.error(TAG, "Unable to fetch user data: " + e.getMessage(), e); - } - } - }); - } - - public void plans(final PlansCallback cb, InAppBilling inAppBilling) { - final Map params = new HashMap(); - params.put("locale", LanternApp.getSession().getLanguage()); - params.put("countrycode", LanternApp.getSession().getCountryCode()); - final HttpUrl url = createProUrl("/plans", params); - final Map plans = new HashMap(); - get(url, new ProCallback() { - @Override - public void onFailure(final Throwable throwable, final ProError error) { - Logger.error(TAG, "Unable to fetch plans", throwable); - cb.onFailure(throwable, error); - } - - @Override - public void onSuccess(final Response response, final JsonObject result) { - try { - Logger.debug(TAG, "JSON response for " + url + ":" + result.toString()); - processPlansV1(result, cb, inAppBilling); - } catch (Exception e) { - Logger.error(TAG, "Unable to fetch plans: " + e.getMessage(), e); - } - } - }); - } - - public void plansV3(final PlansV3Callback cb, InAppBilling inAppBilling) { - final Map params = new HashMap(); - params.put("locale", LanternApp.getSession().getLanguage()); - params.put("countrycode", LanternApp.getSession().getCountryCode()); - final HttpUrl url = createProUrl("/plans-v3", params); - final Map plans = new HashMap(); - get(url, new ProCallback() { - @Override - public void onFailure(final Throwable throwable, final ProError error) { - Logger.error(TAG, "Unable to fetch plans", throwable); - cb.onFailure(throwable, error); - } - - @Override - public void onSuccess(final Response response, final JsonObject result) { - try { - Logger.debug(TAG, "JSON response for " + url + ":" + result.toString()); - processPlansV3(result, cb, inAppBilling); - } catch (Exception e) { - Logger.error(TAG, "Unable to fetch plans: " + e.getMessage(), e); - } - } - }); - } - - - public void plansV3(final PlansCallback cb, InAppBilling inAppBilling) { - plansV3(cb, inAppBilling); - } - - public void getPlans(final PlansCallback cb, InAppBilling inAppBilling) { - plans(cb, inAppBilling); - } - - /** - * Convert a JsonObject json into a RequestBody that transmits content - * - * @param json the JsonObject to be converted - */ - public static RequestBody createJsonBody(final JsonObject json) { - return RequestBody.create(JSON, json.toString()); - } - - public void request(@NonNull final String method, @NonNull final HttpUrl url, - final Map headers, - RequestBody body, final HttpCallback cb) { - Request.Builder builder = new Request.Builder() - .cacheControl(CacheControl.FORCE_NETWORK); - if (headers != null) { - builder = builder.headers(Headers.of(headers)); - } - builder = builder.url(url); - - if (method != null && method.equals("POST")) { - if (body == null) { - body = RequestBody.create(null, new byte[0]); - } - builder = builder.post(body); - } - final Request request = builder.build(); - httpClient.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - if (cb != null) - cb.onFailure(e); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - if (!response.isSuccessful()) { - Logger.error(TAG, "Request to " + url + " failed"); - Logger.error(TAG, "Response: " + response); - final ResponseBody body = response.body(); - if (body != null) { - Logger.error(TAG, "Body: " + body.string()); - } - cb.onFailure(null); - return; - } - cb.onSuccess(response); - } - }); - } - - /** - * Creates a new HTTP request to be enqueued for later execution - * - * @param method the HTTP method - * @param url the URL target of this request - * @param headers the HTTP header fields to add to the request - * @param body the body of a POST request - * @param cb to notify the caller of an HTTP response or failure - */ - private void proRequest(@NonNull final String method, @NonNull final HttpUrl url, - final Map headers, - RequestBody body, final ProCallback cb) { - Request.Builder builder = new Request.Builder() - .cacheControl(CacheControl.FORCE_NETWORK); - if (headers != null) { - builder = builder.headers(Headers.of(headers)); - } - builder = builder.url(url); - - if (method != null && method.equals("POST")) { - if (body == null) { - body = RequestBody.create(null, new byte[0]); - } - builder = builder.post(body); - } - final Request request = builder.build(); - httpClient.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - if (e != null) { - final ProError error = new ProError("", e.getMessage()); - cb.onFailure(e, error); - } - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - if (!response.isSuccessful()) { - Logger.error(TAG, "Request to " + url + " failed"); - Logger.error(TAG, "Response: " + response); - final ResponseBody body = response.body(); - if (body != null) { - Logger.error(TAG, "Body: " + body.string()); - } - final ProError error = new ProError("", "Unexpected response code from server"); - cb.onFailure(null, error); - return; - } - final String responseData = response.body().string(); - JsonObject result; - if (responseData == null) { - Logger.error(TAG, String.format("Invalid response body for %s request", url)); - return; - } - try { - result = (new JsonParser()).parse(responseData).getAsJsonObject(); - } catch (Throwable t) { - Logger.debug(TAG, "Not a JSON response"); - final ResponseBody body = ResponseBody.create(null, responseData); - cb.onSuccess(response.newBuilder().body(body).build(), null); - return; - } - if (result.get("error") != null) { - final String error = result.get("error").getAsString(); - Logger.error(TAG, "Error making request to " + url + ":" + result + " error:" + error); - cb.onFailure(null, new ProError(result)); - } else if (cb != null) { - cb.onSuccess(response, result); - } - } - }); - } - - public interface ProCallback { - public void onFailure(@Nullable Throwable throwable, @Nullable final ProError error); - - public void onSuccess(Response response, JsonObject result); - } - - public interface ProUserCallback { - public void onFailure(@Nullable Throwable throwable, @Nullable final ProError error); - - public void onSuccess(Response response, final ProUser userData); - } - - public interface HttpCallback { - public void onFailure(@Nullable Throwable throwable); - - public void onSuccess(Response response); - } - - public interface PlansCallback { - public void onFailure(@Nullable Throwable throwable, @Nullable final ProError error); - - public void onSuccess(Map plans); - } - - public interface PlansV3Callback { - public void onFailure(@Nullable Throwable throwable, @Nullable final ProError error); - - public void onSuccess(Map plans, List methods); - } - - public interface YuansferCallback { - public void onFailure(@Nullable Throwable throwable, @Nullable final ProError error); - - public void onSuccess(String paymentInfo); - } -} diff --git a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt index 9ef39f399..06b083d44 100644 --- a/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt +++ b/android/app/src/main/kotlin/io/lantern/model/SessionModel.kt @@ -502,9 +502,8 @@ class SessionModel( } } - override fun onSuccess(response: Response, result: JsonObject) { - Logger.debug(TAG, "Response: $result") - if (result["token"] != null && result["userID"] != null) { + override fun onSuccess(response: Response?, result: JsonObject?) { + if (result != null && result["token"] != null && result["userID"] != null) { Logger.debug(TAG, "Successfully validated recovery code") // update token and user ID with those returned by the pro server // update token and user ID with those returned by the pro server @@ -540,7 +539,7 @@ class SessionModel( activity.showErrorDialog(activity.resources.getString(R.string.invalid_verification_code)) } - override fun onSuccess(response: Response, result: JsonObject) { + override fun onSuccess(response: Response?, result: JsonObject?) { lanternClient.userData(object : ProUserCallback { override fun onSuccess(response: Response, userData: ProUser) { Logger.debug(TAG, "Successfully updated userData") @@ -608,7 +607,7 @@ class SessionModel( activity.showErrorDialog(activity.resources.getString(R.string.unable_remove_device)) } - override fun onSuccess(response: Response, result: JsonObject) { + override fun onSuccess(response: Response?, result: JsonObject?) { Logger.debug(TAG, "Successfully removed device") val isLogout = deviceId == LanternApp.getSession().deviceID diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt index e36531259..72344df97 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/LanternApp.kt @@ -94,7 +94,7 @@ open class LanternApp : Application() { @JvmStatic fun getPlans(cb: LanternHttpClient.PlansCallback) { - lanternHttpClient.getPlans( + lanternHttpClient.plans( cb, if (session.isPlayVersion && !session.isRussianUser) inAppBilling else null ) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt index 385269a01..54ddc1798 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -303,7 +303,7 @@ class MainActivity : Logger.error(TAG, "Unable to fetch user data: $error", throwable) } - override fun onSuccess(response: Response, user: ProUser?) { + override fun onSuccess(response: Response, user: ProUser) { val devices = user?.getDevices() val deviceID = LanternApp.getSession().deviceID() // if the payment test mode is enabled @@ -323,7 +323,7 @@ class MainActivity : } private fun updatePlans() { - lanternClient.getPlans(object : PlansCallback { + lanternClient.plans(object : PlansCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { Logger.error(TAG, "Unable to fetch user plans: $error", throwable) } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt index ac39595ae..e81eb106c 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt @@ -155,7 +155,7 @@ open class LanternService : Service(), Runnable { service.createUser(attempts) } - override fun onSuccess(response: Response, result: JsonObject) { + override fun onSuccess(response: Response?, result: JsonObject?) { val user: ProUser? = Json.gson.fromJson(result, ProUser::class.java) if (user == null) { Logger.error(TAG, "Unable to parse user from JSON") diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt new file mode 100644 index 000000000..c3a8a6d31 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -0,0 +1,152 @@ +package org.getlantern.lantern.model + +import androidx.annotation.NonNull +import androidx.annotation.Nullable + +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.reflect.TypeToken + +import org.getlantern.lantern.LanternApp +import org.getlantern.lantern.util.Json +import org.getlantern.mobilesdk.Logger +import org.getlantern.mobilesdk.util.HttpClient + +import okhttp3.CacheControl +import okhttp3.Call +import okhttp3.Callback +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.ResponseBody +import okio.Buffer + +open class LanternHttpClient : HttpClient() { + companion object { + private const val DEVICE_ID_HEADER = "X-Lantern-Device-Id" + private const val USER_ID_HEADER = "X-Lantern-User-Id" + private const val PRO_TOKEN_HEADER = "X-Lantern-Pro-Token" + private const val APP_VERSION_HEADER = "X-Lantern-Version" + private const val PLATFORM_HEADER = "X-Lantern-Platform" + + private var JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() + + fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { + val url = "http://localhost/pro${uri}" + var builder = HttpUrl.parse(url)?.newBuilder() + for ((key, value) in params) { + builder?.addQueryParameter(key, value) + } + return builder?.build() + } + + fun createJsonBody(json: JsonObject): RequestBody { + return RequestBody.create(JSON, json.toString()) + } + } + + private fun userHeaders(): MutableMap { + val headers = mutableMapOf() + headers.put(DEVICE_ID_HEADER, LanternApp.getSession().deviceID) + headers.put(PRO_TOKEN_HEADER, LanternApp.getSession().token) + headers.put(USER_ID_HEADER, LanternApp.getSession().userID.toString()) + headers.put(PLATFORM_HEADER, "android") + headers.put(APP_VERSION_HEADER, Utils.appVersion(LanternApp.getAppContext())) + headers.putAll(LanternApp.getSession().getInternalHeaders()) + return headers + } + + fun get(url: HttpUrl, cb: ProCallback) { + proRequest("GET", url, userHeaders(), null, cb) + } + + fun post(url: HttpUrl, body: RequestBody, cb: ProCallback) { + proRequest("POST", url, userHeaders(), body, cb) + } + + fun userData(cb: ProUserCallback) { + val params = mapOf("locale" to LanternApp.getSession().language) + val url = createProUrl("/user-data", params) + get(url, object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + + } + + override fun onSuccess(response: Response?, user: ProUser?) { + + } + }) + } + + fun sendLinkRequest(cb: ProCallback?) { + val url = createProUrl("/user-link-request") + val formBody = FormBody.Builder() + .add("email", LanternApp.getSession().email()) + .add("deviceName", LanternApp.getSession().deviceName()) + .build() + post(url, formBody, object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + if (cb != null) cb.onFailure(throwable, error) + } + + override fun onSuccess(response: Response?, result: JsonObject?) { + + } + }) + } + + fun plans(cb: PlansCallback, inAppBilling: InAppBilling?) { + val params = mapOf("locale" to LanternApp.getSession().getLanguage(), + "countrycode" to LanternApp.getSession().getCountryCode()) + val url = createProUrl("/plans", params) + val plans = mapOf() + get(url, object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + if (cb != null) cb.onFailure(throwable, error) + } + + override fun onSuccess(response: Response?, result: JsonObject?) { + + } + + }) + } + + fun plansV3(cb: PlansV3Callback, inAppBilling: InAppBilling?) { + + } + + private fun proRequest(method: String, url: HttpUrl, headers: MutableMap, + body: RequestBody?, cb: ProCallback) { + var builder = Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) + if (headers != null) { + builder = builder.headers(Headers.of(headers)) + } + } + + fun interface ProCallback { + fun onFailure(throwable: Throwable?, error: ProError?) + abstract fun onSuccess(response: Response?, result: JsonObject?) + } + + fun interface ProUserCallback { + fun onFailure(throwable: Throwable?, error: ProError?) + fun onSuccess(response: Response, userData: ProUser) + } + + fun interface PlansCallback { + fun onFailure(throwable: Throwable?, error: ProError?) + fun onSuccess(plans: Map) + } + + fun interface PlansV3Callback { + fun onFailure(throwable: Throwable?, error: ProError?) + fun onSuccess(plans: Map, methods: List) + } +} From 11dbf145952e4bd6f56aa78a5d1099e9d667b75c Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 22:00:12 -0700 Subject: [PATCH 02/10] Kotlin-based LanternHttpClient --- .../lantern/util/LanternHttpClient.kt | 24 ++++++++++--------- .../getlantern/lantern/util/PaymentsUtil.kt | 2 +- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index c3a8a6d31..ee913d5c1 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -17,7 +17,9 @@ import okhttp3.Call import okhttp3.Callback import okhttp3.FormBody import okhttp3.Headers +import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient @@ -39,11 +41,11 @@ open class LanternHttpClient : HttpClient() { fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { val url = "http://localhost/pro${uri}" - var builder = HttpUrl.parse(url)?.newBuilder() + var builder = url.toHttpUrl().newBuilder() for ((key, value) in params) { - builder?.addQueryParameter(key, value) + builder.addQueryParameter(key, value) } - return builder?.build() + return builder.build() } fun createJsonBody(json: JsonObject): RequestBody { @@ -78,7 +80,7 @@ open class LanternHttpClient : HttpClient() { } - override fun onSuccess(response: Response?, user: ProUser?) { + override fun onSuccess(response: Response?, result: JsonObject?) { } }) @@ -102,7 +104,7 @@ open class LanternHttpClient : HttpClient() { } fun plans(cb: PlansCallback, inAppBilling: InAppBilling?) { - val params = mapOf("locale" to LanternApp.getSession().getLanguage(), + val params = mapOf("locale" to LanternApp.getSession().language, "countrycode" to LanternApp.getSession().getCountryCode()) val url = createProUrl("/plans", params) val plans = mapOf() @@ -122,30 +124,30 @@ open class LanternHttpClient : HttpClient() { } - private fun proRequest(method: String, url: HttpUrl, headers: MutableMap, + private fun proRequest(method: String, url: HttpUrl, headers: Map, body: RequestBody?, cb: ProCallback) { var builder = Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) if (headers != null) { - builder = builder.headers(Headers.of(headers)) + builder = builder.headers(headers.toHeaders()) } } - fun interface ProCallback { + interface ProCallback { fun onFailure(throwable: Throwable?, error: ProError?) abstract fun onSuccess(response: Response?, result: JsonObject?) } - fun interface ProUserCallback { + interface ProUserCallback { fun onFailure(throwable: Throwable?, error: ProError?) fun onSuccess(response: Response, userData: ProUser) } - fun interface PlansCallback { + interface PlansCallback { fun onFailure(throwable: Throwable?, error: ProError?) fun onSuccess(plans: Map) } - fun interface PlansV3Callback { + interface PlansV3Callback { fun onFailure(throwable: Throwable?, error: ProError?) fun onSuccess(plans: Map, methods: List) } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt index 6e2125d3f..0aaaa2a76 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/PaymentsUtil.kt @@ -211,7 +211,7 @@ class PaymentsUtil(private val activity: Activity) { } } - override fun onSuccess(response: Response, result: JsonObject) { + override fun onSuccess(response: Response?, result: JsonObject?) { Logger.debug( TAG, "Successfully redeemed referral code: $refCode", From b3521e616c3a98aba2d9c5b516d5bb425cb5991d Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 22:15:29 -0700 Subject: [PATCH 03/10] Kotlin-based LanternHttpClient --- .../lantern/util/LanternHttpClient.kt | 82 +++++++++++++------ 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index ee913d5c1..5e935615b 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -3,6 +3,7 @@ package org.getlantern.lantern.model import androidx.annotation.NonNull import androidx.annotation.Nullable +import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken @@ -30,28 +31,6 @@ import okhttp3.ResponseBody import okio.Buffer open class LanternHttpClient : HttpClient() { - companion object { - private const val DEVICE_ID_HEADER = "X-Lantern-Device-Id" - private const val USER_ID_HEADER = "X-Lantern-User-Id" - private const val PRO_TOKEN_HEADER = "X-Lantern-Pro-Token" - private const val APP_VERSION_HEADER = "X-Lantern-Version" - private const val PLATFORM_HEADER = "X-Lantern-Platform" - - private var JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() - - fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { - val url = "http://localhost/pro${uri}" - var builder = url.toHttpUrl().newBuilder() - for ((key, value) in params) { - builder.addQueryParameter(key, value) - } - return builder.build() - } - - fun createJsonBody(json: JsonObject): RequestBody { - return RequestBody.create(JSON, json.toString()) - } - } private fun userHeaders(): MutableMap { val headers = mutableMapOf() @@ -77,11 +56,11 @@ open class LanternHttpClient : HttpClient() { val url = createProUrl("/user-data", params) get(url, object : ProCallback { override fun onFailure(throwable: Throwable?, error: ProError?) { - + cb.onFailure(throwable, error) } override fun onSuccess(response: Response?, result: JsonObject?) { - + Logger.debug(TAG, "JSON response" + result.toString()) } }) } @@ -98,11 +77,17 @@ open class LanternHttpClient : HttpClient() { } override fun onSuccess(response: Response?, result: JsonObject?) { - + result?.get("error")?.let { + onFailure(null, ProError(result)) + } } }) } + inline fun parseData(row :String): T{ + return Gson().fromJson(row, object: TypeToken(){}.type) + } + fun plans(cb: PlansCallback, inAppBilling: InAppBilling?) { val params = mapOf("locale" to LanternApp.getSession().language, "countrycode" to LanternApp.getSession().getCountryCode()) @@ -114,6 +99,7 @@ open class LanternHttpClient : HttpClient() { } override fun onSuccess(response: Response?, result: JsonObject?) { + //val mapType = TypeToken>() {}.type } @@ -121,7 +107,29 @@ open class LanternHttpClient : HttpClient() { } fun plansV3(cb: PlansV3Callback, inAppBilling: InAppBilling?) { + val params = mapOf("locale" to LanternApp.getSession().language, + "countrycode" to LanternApp.getSession().getCountryCode()) + val httpUrl = createProUrl("/plans-v3", params) + val plans = mutableMapOf() + get(httpUrl, object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + Logger.error(TAG, "Unable to fetch plans", throwable) + if (cb != null) cb.onFailure(throwable, error) + } + override fun onSuccess(response: Response?, result: JsonObject?) { + val response = parseData>>(result?.get("providers").toString()) + val providers = response?.get("android") + val fetched = parseData>(result?.get("plans").toString()) + for (plan in fetched) { + plan?.let { + plan.formatCost() + plans.put(plan.id, plan) + } + } + if (providers != null) cb.onSuccess(plans, providers) + } + }) } private fun proRequest(method: String, url: HttpUrl, headers: Map, @@ -151,4 +159,28 @@ open class LanternHttpClient : HttpClient() { fun onFailure(throwable: Throwable?, error: ProError?) fun onSuccess(plans: Map, methods: List) } + + companion object { + private const val DEVICE_ID_HEADER = "X-Lantern-Device-Id" + private const val USER_ID_HEADER = "X-Lantern-User-Id" + private const val PRO_TOKEN_HEADER = "X-Lantern-Pro-Token" + private const val APP_VERSION_HEADER = "X-Lantern-Version" + private const val PLATFORM_HEADER = "X-Lantern-Platform" + private val TAG = LanternHttpClient::class.java.name + + private var JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() + + fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { + val url = "http://localhost/pro${uri}" + var builder = url.toHttpUrl().newBuilder() + for ((key, value) in params) { + builder.addQueryParameter(key, value) + } + return builder.build() + } + + fun createJsonBody(json: JsonObject): RequestBody { + return RequestBody.create(JSON, json.toString()) + } + } } From 8b8abc2666c66c9fa62864546b1b695705608400 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 23:39:05 -0700 Subject: [PATCH 04/10] Updates to LanternHttpClient --- .../lantern/util/LanternHttpClient.kt | 219 ++++++++++++------ 1 file changed, 147 insertions(+), 72 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index 5e935615b..9f3a5e583 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -1,35 +1,27 @@ package org.getlantern.lantern.model -import androidx.annotation.NonNull -import androidx.annotation.Nullable - import com.google.gson.Gson import com.google.gson.JsonObject import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken - -import org.getlantern.lantern.LanternApp -import org.getlantern.lantern.util.Json -import org.getlantern.mobilesdk.Logger -import org.getlantern.mobilesdk.util.HttpClient - import okhttp3.CacheControl import okhttp3.Call import okhttp3.Callback import okhttp3.FormBody -import okhttp3.Headers import okhttp3.Headers.Companion.toHeaders import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response -import okhttp3.ResponseBody -import okio.Buffer +import org.getlantern.lantern.LanternApp +import org.getlantern.mobilesdk.Logger +import org.getlantern.mobilesdk.util.HttpClient +import java.io.IOException +// An OkHttp-Based HTTP client for communicating with the Pro server open class LanternHttpClient : HttpClient() { private fun userHeaders(): MutableMap { @@ -51,18 +43,30 @@ open class LanternHttpClient : HttpClient() { proRequest("POST", url, userHeaders(), body, cb) } + inline fun parseData(row: String): T { + return Gson().fromJson(row, object : TypeToken() {}.type) + } + fun userData(cb: ProUserCallback) { val params = mapOf("locale" to LanternApp.getSession().language) val url = createProUrl("/user-data", params) - get(url, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { - cb.onFailure(throwable, error) - } + get( + url, + object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + cb.onFailure(throwable, error) + } - override fun onSuccess(response: Response?, result: JsonObject?) { - Logger.debug(TAG, "JSON response" + result.toString()) - } - }) + override fun onSuccess(response: Response?, result: JsonObject?) { + Logger.debug(TAG, "JSON response" + result.toString()) + result?.let { + val user = parseData(result.asString) + Logger.debug(TAG, "User ID is ${user.userId}") + LanternApp.getSession().storeUserData(user) + } + } + }, + ) } fun sendLinkRequest(cb: ProCallback?) { @@ -71,75 +75,146 @@ open class LanternHttpClient : HttpClient() { .add("email", LanternApp.getSession().email()) .add("deviceName", LanternApp.getSession().deviceName()) .build() - post(url, formBody, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { - if (cb != null) cb.onFailure(throwable, error) - } + post( + url, + formBody, + object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + if (cb != null) cb.onFailure(throwable, error) + } - override fun onSuccess(response: Response?, result: JsonObject?) { - result?.get("error")?.let { - onFailure(null, ProError(result)) + override fun onSuccess(response: Response?, result: JsonObject?) { + result?.get("error")?.let { + onFailure(null, ProError(result)) + } } - } - }) + }, + ) } - inline fun parseData(row :String): T{ - return Gson().fromJson(row, object: TypeToken(){}.type) + private fun plansMap(fetched: List): Map { + val plans = mutableMapOf() + for (plan in fetched) { + plan.formatCost() + plans.put(plan.id, plan) + } + return plans } fun plans(cb: PlansCallback, inAppBilling: InAppBilling?) { - val params = mapOf("locale" to LanternApp.getSession().language, - "countrycode" to LanternApp.getSession().getCountryCode()) + val params = mapOf( + "locale" to LanternApp.getSession().language, + "countrycode" to LanternApp.getSession().getCountryCode(), + ) val url = createProUrl("/plans", params) - val plans = mapOf() - get(url, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { - if (cb != null) cb.onFailure(throwable, error) - } + get( + url, + object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + cb.onFailure(throwable, error) + } - override fun onSuccess(response: Response?, result: JsonObject?) { - //val mapType = TypeToken>() {}.type + override fun onSuccess(response: Response?, result: JsonObject?) { + // val mapType = TypeToken>() {}.type + val stripePubKey = result?.get("providers")?.asJsonObject + ?.get("stripe")?.asJsonObject?.get("pubKey")?.asString + LanternApp.getSession().setStripePubKey(stripePubKey) + val fetched = parseData>(result?.get("plans").toString()) + Logger.debug(TAG, "Pro plans: $fetched") + var plans = plansMap(fetched) + if (inAppBilling != null) { + // this means we're in the play store, use the configured plans from there but + // with the renewal bonus from the server side plans + val regularPlans = mutableMapOf() + plans.forEach { (key, value) -> + regularPlans.put(key.substring(0, key.lastIndexOf("-")), value) + } + plans = inAppBilling.plans + plans.forEach { (key, value) -> + val regularPlan = regularPlans.get(key) + if (regularPlan != null) { + value.updateRenewalBonusExpected(regularPlan.renewalBonusExpected) + } + } + } + cb.onSuccess(plans) + } + }, + ) + } - } + fun plansV3(cb: PlansV3Callback, inAppBilling: InAppBilling?) { + val params = mapOf( + "locale" to LanternApp.getSession().language, + "countrycode" to LanternApp.getSession().getCountryCode(), + ) + get( + createProUrl("/plans-v3", params), + object : ProCallback { + override fun onFailure(throwable: Throwable?, error: ProError?) { + Logger.error(TAG, "Unable to fetch plans", throwable) + cb.onFailure(throwable, error) + } - }) + override fun onSuccess(response: Response?, result: JsonObject?) { + val methods = parseData>>( + result?.get("providers").toString(), + ) + val providers = methods.get("android") + val fetched = parseData>(result?.get("plans").toString()) + val plans = plansMap(fetched) + if (providers != null) cb.onSuccess(plans, providers) + } + }, + ) } - fun plansV3(cb: PlansV3Callback, inAppBilling: InAppBilling?) { - val params = mapOf("locale" to LanternApp.getSession().language, - "countrycode" to LanternApp.getSession().getCountryCode()) - val httpUrl = createProUrl("/plans-v3", params) - val plans = mutableMapOf() - get(httpUrl, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { - Logger.error(TAG, "Unable to fetch plans", throwable) - if (cb != null) cb.onFailure(throwable, error) + private fun proRequest( + method: String, + url: HttpUrl, + headers: Map, + body: RequestBody?, + cb: ProCallback, + ) { + var builder = Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) + .headers(headers.toHeaders()) + .url(url) + if (method == "POST") { + var requestBody = if (body != null) body else RequestBody.create(null, ByteArray(0)) + builder = builder.post(requestBody) + } + + val request = builder.build() + httpClient.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + cb.onFailure(e, ProError("", e.message ?: "")) } - override fun onSuccess(response: Response?, result: JsonObject?) { - val response = parseData>>(result?.get("providers").toString()) - val providers = response?.get("android") - val fetched = parseData>(result?.get("plans").toString()) - for (plan in fetched) { - plan?.let { - plan.formatCost() - plans.put(plan.id, plan) + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + val responseData = response.body.toString() + val result = JsonParser().parse(responseData).asJsonObject + if (result.get("error") != null) { + val error = result.get("error").asString + Logger.error(TAG, "Error making request to $url: $result error: $error") + cb.onFailure(null, ProError(result)) + return } + cb.onSuccess(response, result) + return } - if (providers != null) cb.onSuccess(plans, providers) + Logger.error(TAG, "Request to $url failed") + Logger.error(TAG, "Response: $response") + val responseBody = response.body + if (responseBody != null) { + Logger.error(TAG, "Body: $responseBody") + } + val error = ProError("", "Unexpected response code from server") + cb.onFailure(null, error) } }) } - private fun proRequest(method: String, url: HttpUrl, headers: Map, - body: RequestBody?, cb: ProCallback) { - var builder = Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) - if (headers != null) { - builder = builder.headers(headers.toHeaders()) - } - } - interface ProCallback { fun onFailure(throwable: Throwable?, error: ProError?) abstract fun onSuccess(response: Response?, result: JsonObject?) @@ -152,12 +227,12 @@ open class LanternHttpClient : HttpClient() { interface PlansCallback { fun onFailure(throwable: Throwable?, error: ProError?) - fun onSuccess(plans: Map) + fun onSuccess(plans: Map) } interface PlansV3Callback { fun onFailure(throwable: Throwable?, error: ProError?) - fun onSuccess(plans: Map, methods: List) + fun onSuccess(plans: Map, methods: List) } companion object { @@ -171,7 +246,7 @@ open class LanternHttpClient : HttpClient() { private var JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { - val url = "http://localhost/pro${uri}" + val url = "http://localhost/pro$uri" var builder = url.toHttpUrl().newBuilder() for ((key, value) in params) { builder.addQueryParameter(key, value) From 57b344edcf9cd469975f277f4194351dc44c8bfe Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 23:47:32 -0700 Subject: [PATCH 05/10] Add tests --- .../notification/NotificationHelperTest.kt | 62 ++++++++ .../lantern/model/InAppBillingTest.kt | 149 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt create mode 100644 android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt diff --git a/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt b/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt new file mode 100644 index 000000000..318f40df5 --- /dev/null +++ b/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt @@ -0,0 +1,62 @@ +package org.getlantern.lantern.notification + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Test + +class NotificationHelperTest { + + @MockK + lateinit var notificationHelper: NotificationHelper + + var receiverSlot = slot() + + val appContext : Context = mockk(relaxed = true) { + every { + getSystemService(Context.NOTIFICATION_SERVICE) + } returns mockk() + } + + @Before + fun setUp() { + MockKAnnotations.init(this, relaxUnitFun = true) + val receiverIntent = mockk() + every { appContext.registerReceiver(capture(receiverSlot), any()) } returns receiverIntent + notificationHelper = NotificationHelper(appContext, receiverSlot.captured) + } + + @After + fun tearDown() { + verify(exactly = 1) { appContext.unregisterReceiver(receiverSlot.captured) } + } + + @Test + fun `Disconnect broadcast resolves to pending intent`() { + val receiverIntent = mockk() + every { appContext.registerReceiver(capture(receiverSlot), any()) } returns receiverIntent + + notificationHelper = NotificationHelper(appContext, receiverSlot.captured) + val intent = Intent("org.getlantern.lantern.intent.VPN_DISCONNECTED") + val pendingIntent = + PendingIntent.getBroadcast(appContext, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + every { notificationHelper.disconnectBroadcast() } returns pendingIntent + } + + @Test + fun `VPN connected notification`() { + notificationHelper.vpnConnectedNotification() + } +} \ No newline at end of file diff --git a/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt b/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt new file mode 100644 index 000000000..9cfc65fff --- /dev/null +++ b/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt @@ -0,0 +1,149 @@ +package org.getlantern.lantern.model + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import com.android.billingclient.api.AcknowledgePurchaseParams +import com.android.billingclient.api.AcknowledgePurchaseResponseListener +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.ConsumeResponseListener +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.PurchaseHistoryRecord +import com.android.billingclient.api.PurchaseHistoryResponseListener +import com.android.billingclient.api.PurchasesResponseListener +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.SkuDetails +import com.android.billingclient.api.SkuDetailsParams +import com.android.billingclient.api.SkuDetailsResponseListener +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.runs +import io.mockk.slot +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class InAppBillingTest { + + @MockK + lateinit var builder: BillingClient.Builder + + @MockK + lateinit var billingClient: BillingClient + + @MockK + lateinit var availability: GoogleApiAvailability + + private lateinit var inAppBilling: InAppBilling + + @Before + fun setUp() { + val context = mockk(relaxed = true) + MockKAnnotations.init(this, relaxUnitFun = true) + every { builder.setListener(any()) } returns builder + every { builder.build() } returns billingClient + inAppBilling = InAppBilling(context, builder, availability) + } + + private fun playServicesAvailable() = + every { availability.isGooglePlayServicesAvailable(any()) } returns ConnectionResult.SUCCESS + + @Test + fun `initConnection new connection succeeds`() { + every { billingClient.isReady } returns false + val listener = slot() + every { billingClient.startConnection(capture(listener)) } answers { + listener.captured.onBillingSetupFinished( + BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK) + .build() + ) + } + playServicesAvailable() + inAppBilling.initConnection() + } + + @Test + fun `initConnection new connection fails`() { + every { billingClient.isReady } returns false + val listener = slot() + every { billingClient.startConnection(capture(listener)) } answers { + listener.captured.onBillingSetupFinished( + BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.ERROR) + .build() + ) + } + playServicesAvailable() + + inAppBilling.initConnection() + } + + @Test + fun `endConnection resolves`() { + playServicesAvailable() + + inAppBilling.initConnection() + inAppBilling.endConnection() + + verify { billingClient.endConnection() } + } + + @Test + fun `ensureConnection should attempt to reconnect, if not in ready state`() { + playServicesAvailable() + var callbackCalled = false + every { billingClient.isReady } returns true + val listener = slot() + every { billingClient.startConnection(capture(listener)) } answers { + listener.captured.onBillingSetupFinished( + BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK) + .build() + ) + } + inAppBilling.billingClient = billingClient + inAppBilling.ensureConnected { + callbackCalled = true + } + assertTrue("Callback should be called", callbackCalled) + } + + @Test + fun `handlePendingPurchases successfully handles pending purchases`() { + playServicesAvailable() + every { billingClient.isReady } returns true + val listener = slot() + every { billingClient.queryPurchasesAsync(any(), capture(listener)) } answers { + listener.captured.onQueryPurchasesResponse( + BillingResult.newBuilder().build(), + listOf( + mockk { + every { purchaseState } returns 2 + every { purchaseToken } returns "token" + }, + ) + ) + } + val consumeListener = slot() + every { billingClient.consumeAsync(any(), capture(consumeListener)) } answers { + consumeListener.captured.onConsumeResponse( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.ITEM_NOT_OWNED).build(), + "" + ) + } + inAppBilling.billingClient = billingClient + inAppBilling.initConnection() + verify { inAppBilling.handlePendingPurchases() } + } + +} \ No newline at end of file From 45cf2befb5b0535dac2da89f5a10c6402495651d Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 23:47:37 -0700 Subject: [PATCH 06/10] Add tests --- android/app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 083bda981..4f4e104bb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -439,6 +439,7 @@ dependencies { androidTestImplementation 'com.squareup.okhttp3:okhttp:4.9.2' testImplementation 'junit:junit:4.13.2' + testImplementation "io.mockk:mockk:1.13.5" implementation "com.github.YarikSOffice:lingver:1.3.0" // from https://github.com/getlantern/opus_android From fd95536babfe15d5cf583bad7a7f03c3d0bc1f7f Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 23:54:46 -0700 Subject: [PATCH 07/10] Add LanternHttpClientTest --- .../lantern/util/LanternHttpClientTest.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt diff --git a/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt b/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt new file mode 100644 index 000000000..2c8beecd8 --- /dev/null +++ b/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt @@ -0,0 +1,57 @@ +package org.getlantern.lantern.util + +import android.content.Context +import android.content.Intent +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Before +import org.junit.Test + +class LanternHttpClientTest { + @Test + fun `from raw http - normal`() { + val statusOK = "HTTP/1.1 200 OK\r\n" + + "Content-Type: application/json;charset=utf-8\r\n" + + "X-Lantern-Device-Id: abc\r\n" + + "X-Lantern-User-Id: 12345\r\n" + + "\r\n" + + "Some Content" + + statusOK.toVauInnerHttpResponse(mockk()).let { + assertEquals(Protocol.HTTP_1_1, it.protocol) + assertEquals(200, it.code) + assertEquals("abc", it.headers["X-Lantern-Device-Id"]) + assertEquals("12345", it.headers["X-Lantern-User-Id"]) + assertEquals("application/json;charset=utf-8", it.body!!.contentType().toString()) + assertEquals("Some Content", it.body!!.string()) + } + + val notFound = "HTTP/1.1 400 NOT FOUND\r\n" + + "Content-Type: application/json;charset=utf-8\r\n" + + "X-Header: abc\r\n" + + "Y-Header: 12345\r\n" + + "\r\n" + + "Some Content" + + notFound.toVauInnerHttpResponse(mockk()).let { + assertEquals(Protocol.HTTP_1_1, it.protocol) + assertEquals(400, it.code) + assertEquals("abc", it.headers["X-Lantern-Device-Id"]) + assertEquals("12345", it.headers["X-Lantern-User-Id"]) + assertEquals("application/json;charset=utf-8", it.body!!.contentType().toString()) + assertEquals("Some Content", it.body!!.string()) + } + } +} \ No newline at end of file From be3821035a39003d8b8b0302ba1854f457bea332 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 28 Aug 2023 23:56:21 -0700 Subject: [PATCH 08/10] formatting --- .../getlantern/lantern/notification/NotificationHelperTest.kt | 2 +- .../org/getlantern/lantern/util/LanternHttpClientTest.kt | 2 +- .../kotlin/org/getlantern/lantern/model/InAppBillingTest.kt | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt b/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt index 318f40df5..3e3e25c0b 100644 --- a/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt +++ b/android/app/src/test/kotlin/org/getlantern/lantern/notification/NotificationHelperTest.kt @@ -59,4 +59,4 @@ class NotificationHelperTest { fun `VPN connected notification`() { notificationHelper.vpnConnectedNotification() } -} \ No newline at end of file +} diff --git a/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt b/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt index 2c8beecd8..3df494da6 100644 --- a/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt +++ b/android/app/src/test/kotlin/org/getlantern/lantern/util/LanternHttpClientTest.kt @@ -54,4 +54,4 @@ class LanternHttpClientTest { assertEquals("Some Content", it.body!!.string()) } } -} \ No newline at end of file +} diff --git a/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt b/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt index 9cfc65fff..6157b18ae 100644 --- a/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt +++ b/android/app/src/testPlay/kotlin/org/getlantern/lantern/model/InAppBillingTest.kt @@ -145,5 +145,4 @@ class InAppBillingTest { inAppBilling.initConnection() verify { inAppBilling.handlePendingPurchases() } } - -} \ No newline at end of file +} From 325cf36785a343583a00ac3434c8c0b7164e9037 Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 23 Oct 2023 13:13:22 -0700 Subject: [PATCH 09/10] Add ProUser data class --- .../getlantern/lantern/model/ProError.java | 45 ----- .../org/getlantern/lantern/model/ProUser.java | 164 ------------------ .../org/getlantern/lantern/MainActivity.kt | 6 +- .../lantern/model/LanternSessionManager.kt | 4 +- .../org/getlantern/lantern/model/ProError.kt | 17 ++ .../org/getlantern/lantern/model/ProUser.kt | 49 ++++++ .../lantern/service/LanternService.kt | 7 +- .../lantern/util/LanternHttpClient.kt | 33 ++-- 8 files changed, 94 insertions(+), 231 deletions(-) delete mode 100644 android/app/src/main/java/org/getlantern/lantern/model/ProError.java delete mode 100644 android/app/src/main/java/org/getlantern/lantern/model/ProUser.java create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/model/ProError.kt create mode 100644 android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt diff --git a/android/app/src/main/java/org/getlantern/lantern/model/ProError.java b/android/app/src/main/java/org/getlantern/lantern/model/ProError.java deleted file mode 100644 index 438ce1a38..000000000 --- a/android/app/src/main/java/org/getlantern/lantern/model/ProError.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.getlantern.lantern.model; - -import androidx.annotation.NonNull; - -import com.google.gson.JsonObject; - -public class ProError { - - private String id; - private String message; - private JsonObject details; - - public ProError(@NonNull final String id, @NonNull final String message) { - this.id = id; - this.message = message; - } - - public ProError(@NonNull final JsonObject result) { - if (result.get("errorId") != null) { - this.id = result.get("errorId").getAsString(); - } - if (result.get("error") != null) { - this.message = result.get("error").getAsString(); - } - if (result.get("details") != null) { - this.details = result.get("details").getAsJsonObject(); - } - } - - public String getMessage() { - return message; - } - - public String getId() { - return id; - } - - public JsonObject getDetails() { - return details; - } - - public String toString() { - return String.format("Error; id=%s message=%s", id, message); - } -} diff --git a/android/app/src/main/java/org/getlantern/lantern/model/ProUser.java b/android/app/src/main/java/org/getlantern/lantern/model/ProUser.java deleted file mode 100644 index 0c6fc74f2..000000000 --- a/android/app/src/main/java/org/getlantern/lantern/model/ProUser.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.getlantern.lantern.model; - -import com.google.gson.annotations.SerializedName; - -import org.joda.time.Days; -import org.joda.time.Months; -import org.joda.time.LocalDateTime; - -import java.util.List; - -import org.getlantern.mobilesdk.Logger; - -public class ProUser { - - private static final String TAG = ProUser.class.getName(); - - public ProUser() { - - } - - @SerializedName("userId") - private Long userId; - - @SerializedName("token") - private String token; - - @SerializedName("referral") - private String referral; - - @SerializedName("email") - private String email; - - @SerializedName("userStatus") - private String userStatus; - - @SerializedName("code") - private String code; - - @SerializedName("subscription") - private String subscription; - - @SerializedName("expiration") - private Long expiration; - - @SerializedName("devices") - private List devices; - - - @SerializedName("userLevel") - public String userLevel; - - public void setUserId(final Long userId) { - this.userId = userId; - } - - public Long getUserId() { - return userId; - } - - public void setToken(final String token) { - this.token = token; - } - - public String getToken() { - return token; - } - - public String getUserLevel() { - return this.userLevel; - } - - public void setUserLevel(final String level) { this.userLevel = level; } - - public void setReferral(final String referral) { - this.referral = referral; - } - - public String getReferral() { - return referral; - } - - public void setEmail(final String email) { - this.email = email; - } - - public String getEmail() { - return email; - } - - public void setUserStatus(final String userStatus) { - this.userStatus = userStatus; - } - - public String getUserStatus() { - return userStatus; - } - - public Boolean isProUser() { - return userStatus != null && userStatus.equals("active"); - } - - public void setCode(final String code) { - this.code = code; - } - - public String getCode() { - return code; - } - - public void setExpiration(final Long expiration) { - this.expiration = expiration; - } - - public Long getExpiration() { - return expiration; - } - - public LocalDateTime getExpirationDate() { - final Long expiration = getExpiration(); - if (expiration == null) { - return null; - } - return new LocalDateTime(expiration * 1000); - } - - public Integer monthsLeft() { - final LocalDateTime expDate = getExpirationDate(); - if (expDate != null) { - final int months = Months.monthsBetween(LocalDateTime.now(), expDate).getMonths(); - return months; - } - return null; - } - - public Integer daysLeft() { - final LocalDateTime expDate = getExpirationDate(); - if (expiration != null) { - final int days = Days.daysBetween(LocalDateTime.now(), expDate).getDays(); - Logger.debug(TAG, "Number of days until Pro account expires " + days); - return days; - } - return null; - } - - public Boolean isActive() { - return userStatus != null && userStatus.equals("active"); - } - - public Boolean isExpired() { - return userStatus != null && userStatus.equals("expired"); - } - - public List getDevices() { - return devices; - } - - public String toString() { - return String.format("User ID %d status %s expiration %d Pro user %b", userId, userStatus, expiration, isProUser()); - } - - public String newUserDetails() { - return String.format("User ID %d referral", userId, referral); - } -} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt index a7c771675..44275691b 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/MainActivity.kt @@ -195,6 +195,10 @@ class MainActivity : override fun onDestroy() { super.onDestroy() + if (accountInitDialog != null) { + accountInitDialog.dismiss() + } + vpnModel.destroy() sessionModel.destroy() replicaModel.destroy() @@ -314,7 +318,7 @@ class MainActivity : } override fun onSuccess(response: Response, user: ProUser) { - val devices = user?.getDevices() + val devices = user?.devices val deviceID = LanternApp.getSession().deviceID() // if the payment test mode is enabled // then do nothing To avoid restarting app while debugging diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt index fd6c278f5..0a28b2d3b 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/LanternSessionManager.kt @@ -293,8 +293,8 @@ class LanternSessionManager(application: Application) : SessionManager(applicati if (user.isProUser) { EventBus.getDefault().post(UserStatus(user.isActive, user.monthsLeft().toLong())) - prefs.edit().putInt(PRO_MONTHS_LEFT, user.monthsLeft()) - .putInt(PRO_DAYS_LEFT, user.daysLeft()) + prefs.edit().putInt(PRO_MONTHS_LEFT, user.monthsLeft() ?: 0) + .putInt(PRO_DAYS_LEFT, user.daysLeft() ?: 0) .apply() } } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProError.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProError.kt new file mode 100644 index 000000000..31a63a8e1 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProError.kt @@ -0,0 +1,17 @@ +package org.getlantern.lantern.model + +import com.google.gson.JsonObject + +data class ProError( + val id: String, + val message: String, + val details: JsonObject? = null +) { + constructor(result: JsonObject) : this( + result.get("errorId").asString, + result.get("error").asString, + result.get("details").asJsonObject, + ) { + + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt new file mode 100644 index 000000000..b80932963 --- /dev/null +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt @@ -0,0 +1,49 @@ +package org.getlantern.lantern.model + +import com.google.gson.JsonObject +import org.joda.time.Days +import org.joda.time.Months +import org.joda.time.LocalDateTime + +data class ProUser( + val userId: Long, + val token: String, + val referral: String, + val email: String, + val userStatus: String, + val code: String, + val subscription: String, + val expiration: Long, + val devices: List, + val userLevel: String +) { + + private fun isUserStatus(status: String) = userStatus == status + + private fun expirationDate() = if (expiration == null) null else LocalDateTime(expiration * 1000) + + fun monthsLeft(): Int { + val expDate = expirationDate() + if (expDate == null) return 0 + return Months.monthsBetween(LocalDateTime.now(), expDate).getMonths() + } + + fun daysLeft(): Int { + val expDate = expirationDate() + if (expDate == null) return 0 + return Days.daysBetween(LocalDateTime.now(), expDate).getDays() + } + + fun newUserDetails(): String { + return "User ID $userId referral $referral" + } + + val isProUser: Boolean + get() = isUserStatus("active") + + val isActive: Boolean + get() = isProUser + + val isExpired: Boolean + get() = isUserStatus("expired") +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt index e81eb106c..a55cd84b1 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/service/LanternService.kt @@ -55,7 +55,8 @@ open class LanternService : Service(), Runnable { private val started: AtomicBoolean = AtomicBoolean() private lateinit var autoUpdater: AutoUpdater - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) return START_NOT_STICKY autoUpdater = AutoUpdater(this) val autoBooted = intent.getBooleanExtra(AUTO_BOOTED, false) Logger.d(TAG, "Called onStartCommand, autoBooted?: $autoBooted") @@ -163,8 +164,8 @@ open class LanternService : Service(), Runnable { } service.createUserHandler.removeCallbacks(service.createUserRunnable) Logger.debug(TAG, "Created new Lantern user: ${user.newUserDetails()}") - LanternApp.getSession().setUserIdAndToken(user.getUserId(), user.getToken()) - val referral = user.getReferral() + LanternApp.getSession().setUserIdAndToken(user.userId, user.token) + val referral = user.referral if (!referral.isEmpty()) { LanternApp.getSession().setCode(referral) } diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index 9f3a5e583..3b48afa2f 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -19,6 +19,7 @@ import okhttp3.Response import org.getlantern.lantern.LanternApp import org.getlantern.mobilesdk.Logger import org.getlantern.mobilesdk.util.HttpClient +import org.json.JSONObject import java.io.IOException // An OkHttp-Based HTTP client for communicating with the Pro server @@ -60,7 +61,7 @@ open class LanternHttpClient : HttpClient() { override fun onSuccess(response: Response?, result: JsonObject?) { Logger.debug(TAG, "JSON response" + result.toString()) result?.let { - val user = parseData(result.asString) + val user = parseData(result.toString()) Logger.debug(TAG, "User ID is ${user.userId}") LanternApp.getSession().storeUserData(user) } @@ -191,26 +192,26 @@ open class LanternHttpClient : HttpClient() { } override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - val responseData = response.body.toString() + response.use { + if (!response.isSuccessful) { + val error = ProError("", "Unexpected response code from server $response") + cb.onFailure(null, error) + return + } + val responseData = response.body!!.string() + Logger.d(TAG, "Response body " + responseData) val result = JsonParser().parse(responseData).asJsonObject - if (result.get("error") != null) { - val error = result.get("error").asString - Logger.error(TAG, "Error making request to $url: $result error: $error") - cb.onFailure(null, ProError(result)) + if (result == null) { + return + } else if (result.get("error") != null) { + var error = result.get("error").asString + error = "Error making request to $url: $result error: $error" + Logger.error(TAG, error) + cb.onFailure(null, ProError("", error)) return } cb.onSuccess(response, result) - return - } - Logger.error(TAG, "Request to $url failed") - Logger.error(TAG, "Response: $response") - val responseBody = response.body - if (responseBody != null) { - Logger.error(TAG, "Body: $responseBody") } - val error = ProError("", "Unexpected response code from server") - cb.onFailure(null, error) } }) } From 7cb59c03810d028f61a84ec9ba7526fec81188bc Mon Sep 17 00:00:00 2001 From: atavism Date: Mon, 23 Oct 2023 13:15:18 -0700 Subject: [PATCH 10/10] Formatting --- .../org/getlantern/lantern/model/ProUser.kt | 60 +++-- .../lantern/util/LanternHttpClient.kt | 211 ++++++++++++------ 2 files changed, 173 insertions(+), 98 deletions(-) diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt index b80932963..1c3e72cce 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/model/ProUser.kt @@ -1,49 +1,47 @@ package org.getlantern.lantern.model -import com.google.gson.JsonObject import org.joda.time.Days -import org.joda.time.Months import org.joda.time.LocalDateTime +import org.joda.time.Months data class ProUser( - val userId: Long, - val token: String, - val referral: String, - val email: String, - val userStatus: String, - val code: String, - val subscription: String, - val expiration: Long, - val devices: List, - val userLevel: String + val userId: Long, + val token: String, + val referral: String, + val email: String, + val userStatus: String, + val code: String, + val subscription: String, + val expiration: Long, + val devices: List, + val userLevel: String, ) { + private fun isUserStatus(status: String) = userStatus == status - private fun isUserStatus(status: String) = userStatus == status - - private fun expirationDate() = if (expiration == null) null else LocalDateTime(expiration * 1000) + private fun expirationDate() = if (expiration == null) null else LocalDateTime(expiration * 1000) - fun monthsLeft(): Int { - val expDate = expirationDate() - if (expDate == null) return 0 - return Months.monthsBetween(LocalDateTime.now(), expDate).getMonths() - } + fun monthsLeft(): Int { + val expDate = expirationDate() + if (expDate == null) return 0 + return Months.monthsBetween(LocalDateTime.now(), expDate).getMonths() + } - fun daysLeft(): Int { - val expDate = expirationDate() - if (expDate == null) return 0 - return Days.daysBetween(LocalDateTime.now(), expDate).getDays() - } + fun daysLeft(): Int { + val expDate = expirationDate() + if (expDate == null) return 0 + return Days.daysBetween(LocalDateTime.now(), expDate).getDays() + } - fun newUserDetails(): String { - return "User ID $userId referral $referral" - } + fun newUserDetails(): String { + return "User ID $userId referral $referral" + } val isProUser: Boolean get() = isUserStatus("active") val isActive: Boolean - get() = isProUser + get() = isProUser val isExpired: Boolean - get() = isUserStatus("expired") -} \ No newline at end of file + get() = isUserStatus("expired") +} diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt index 3b48afa2f..5356c8602 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/util/LanternHttpClient.kt @@ -19,12 +19,10 @@ import okhttp3.Response import org.getlantern.lantern.LanternApp import org.getlantern.mobilesdk.Logger import org.getlantern.mobilesdk.util.HttpClient -import org.json.JSONObject import java.io.IOException // An OkHttp-Based HTTP client for communicating with the Pro server open class LanternHttpClient : HttpClient() { - private fun userHeaders(): MutableMap { val headers = mutableMapOf() headers.put(DEVICE_ID_HEADER, LanternApp.getSession().deviceID) @@ -36,11 +34,18 @@ open class LanternHttpClient : HttpClient() { return headers } - fun get(url: HttpUrl, cb: ProCallback) { + fun get( + url: HttpUrl, + cb: ProCallback, + ) { proRequest("GET", url, userHeaders(), null, cb) } - fun post(url: HttpUrl, body: RequestBody, cb: ProCallback) { + fun post( + url: HttpUrl, + body: RequestBody, + cb: ProCallback, + ) { proRequest("POST", url, userHeaders(), body, cb) } @@ -54,11 +59,17 @@ open class LanternHttpClient : HttpClient() { get( url, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { + override fun onFailure( + throwable: Throwable?, + error: ProError?, + ) { cb.onFailure(throwable, error) } - override fun onSuccess(response: Response?, result: JsonObject?) { + override fun onSuccess( + response: Response?, + result: JsonObject?, + ) { Logger.debug(TAG, "JSON response" + result.toString()) result?.let { val user = parseData(result.toString()) @@ -72,19 +83,26 @@ open class LanternHttpClient : HttpClient() { fun sendLinkRequest(cb: ProCallback?) { val url = createProUrl("/user-link-request") - val formBody = FormBody.Builder() - .add("email", LanternApp.getSession().email()) - .add("deviceName", LanternApp.getSession().deviceName()) - .build() + val formBody = + FormBody.Builder() + .add("email", LanternApp.getSession().email()) + .add("deviceName", LanternApp.getSession().deviceName()) + .build() post( url, formBody, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { + override fun onFailure( + throwable: Throwable?, + error: ProError?, + ) { if (cb != null) cb.onFailure(throwable, error) } - override fun onSuccess(response: Response?, result: JsonObject?) { + override fun onSuccess( + response: Response?, + result: JsonObject?, + ) { result?.get("error")?.let { onFailure(null, ProError(result)) } @@ -102,23 +120,34 @@ open class LanternHttpClient : HttpClient() { return plans } - fun plans(cb: PlansCallback, inAppBilling: InAppBilling?) { - val params = mapOf( - "locale" to LanternApp.getSession().language, - "countrycode" to LanternApp.getSession().getCountryCode(), - ) + fun plans( + cb: PlansCallback, + inAppBilling: InAppBilling?, + ) { + val params = + mapOf( + "locale" to LanternApp.getSession().language, + "countrycode" to LanternApp.getSession().getCountryCode(), + ) val url = createProUrl("/plans", params) get( url, object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { + override fun onFailure( + throwable: Throwable?, + error: ProError?, + ) { cb.onFailure(throwable, error) } - override fun onSuccess(response: Response?, result: JsonObject?) { + override fun onSuccess( + response: Response?, + result: JsonObject?, + ) { // val mapType = TypeToken>() {}.type - val stripePubKey = result?.get("providers")?.asJsonObject - ?.get("stripe")?.asJsonObject?.get("pubKey")?.asString + val stripePubKey = + result?.get("providers")?.asJsonObject + ?.get("stripe")?.asJsonObject?.get("pubKey")?.asString LanternApp.getSession().setStripePubKey(stripePubKey) val fetched = parseData>(result?.get("plans").toString()) Logger.debug(TAG, "Pro plans: $fetched") @@ -144,23 +173,34 @@ open class LanternHttpClient : HttpClient() { ) } - fun plansV3(cb: PlansV3Callback, inAppBilling: InAppBilling?) { - val params = mapOf( - "locale" to LanternApp.getSession().language, - "countrycode" to LanternApp.getSession().getCountryCode(), - ) + fun plansV3( + cb: PlansV3Callback, + inAppBilling: InAppBilling?, + ) { + val params = + mapOf( + "locale" to LanternApp.getSession().language, + "countrycode" to LanternApp.getSession().getCountryCode(), + ) get( createProUrl("/plans-v3", params), object : ProCallback { - override fun onFailure(throwable: Throwable?, error: ProError?) { + override fun onFailure( + throwable: Throwable?, + error: ProError?, + ) { Logger.error(TAG, "Unable to fetch plans", throwable) cb.onFailure(throwable, error) } - override fun onSuccess(response: Response?, result: JsonObject?) { - val methods = parseData>>( - result?.get("providers").toString(), - ) + override fun onSuccess( + response: Response?, + result: JsonObject?, + ) { + val methods = + parseData>>( + result?.get("providers").toString(), + ) val providers = methods.get("android") val fetched = parseData>(result?.get("plans").toString()) val plans = plansMap(fetched) @@ -177,63 +217,97 @@ open class LanternHttpClient : HttpClient() { body: RequestBody?, cb: ProCallback, ) { - var builder = Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) - .headers(headers.toHeaders()) - .url(url) + var builder = + Request.Builder().cacheControl(CacheControl.FORCE_NETWORK) + .headers(headers.toHeaders()) + .url(url) if (method == "POST") { var requestBody = if (body != null) body else RequestBody.create(null, ByteArray(0)) builder = builder.post(requestBody) } val request = builder.build() - httpClient.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - cb.onFailure(e, ProError("", e.message ?: "")) - } + httpClient.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + cb.onFailure(e, ProError("", e.message ?: "")) + } - override fun onResponse(call: Call, response: Response) { - response.use { - if (!response.isSuccessful) { - val error = ProError("", "Unexpected response code from server $response") - cb.onFailure(null, error) - return - } - val responseData = response.body!!.string() - Logger.d(TAG, "Response body " + responseData) - val result = JsonParser().parse(responseData).asJsonObject - if (result == null) { - return - } else if (result.get("error") != null) { - var error = result.get("error").asString - error = "Error making request to $url: $result error: $error" - Logger.error(TAG, error) - cb.onFailure(null, ProError("", error)) - return + override fun onResponse( + call: Call, + response: Response, + ) { + response.use { + if (!response.isSuccessful) { + val error = ProError("", "Unexpected response code from server $response") + cb.onFailure(null, error) + return + } + val responseData = response.body!!.string() + Logger.d(TAG, "Response body " + responseData) + val result = JsonParser().parse(responseData).asJsonObject + if (result == null) { + return + } else if (result.get("error") != null) { + var error = result.get("error").asString + error = "Error making request to $url: $result error: $error" + Logger.error(TAG, error) + cb.onFailure(null, ProError("", error)) + return + } + cb.onSuccess(response, result) } - cb.onSuccess(response, result) } - } - }) + }, + ) } interface ProCallback { - fun onFailure(throwable: Throwable?, error: ProError?) - abstract fun onSuccess(response: Response?, result: JsonObject?) + fun onFailure( + throwable: Throwable?, + error: ProError?, + ) + + abstract fun onSuccess( + response: Response?, + result: JsonObject?, + ) } interface ProUserCallback { - fun onFailure(throwable: Throwable?, error: ProError?) - fun onSuccess(response: Response, userData: ProUser) + fun onFailure( + throwable: Throwable?, + error: ProError?, + ) + + fun onSuccess( + response: Response, + userData: ProUser, + ) } interface PlansCallback { - fun onFailure(throwable: Throwable?, error: ProError?) + fun onFailure( + throwable: Throwable?, + error: ProError?, + ) + fun onSuccess(plans: Map) } interface PlansV3Callback { - fun onFailure(throwable: Throwable?, error: ProError?) - fun onSuccess(plans: Map, methods: List) + fun onFailure( + throwable: Throwable?, + error: ProError?, + ) + + fun onSuccess( + plans: Map, + methods: List, + ) } companion object { @@ -246,7 +320,10 @@ open class LanternHttpClient : HttpClient() { private var JSON: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull() - fun createProUrl(uri: String, params: Map = mutableMapOf()): HttpUrl { + fun createProUrl( + uri: String, + params: Map = mutableMapOf(), + ): HttpUrl { val url = "http://localhost/pro$uri" var builder = url.toHttpUrl().newBuilder() for ((key, value) in params) {