diff --git a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java index db70edcc1..dfc7bc3c5 100644 --- a/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java +++ b/sdk/src/androidTest/java/ly/count/android/sdk/ModuleConfigurationTests.java @@ -1089,6 +1089,10 @@ public void doWork(String requestData, String customEndpoint, ConnectionProcesso } }; } + + @Override public ImmediateRequestI CreatePreflightRequestMaker() { + return null; + } }; } @@ -1162,6 +1166,10 @@ private int[] setupTest_allFeatures(JSONObject serverConfig) { } }; } + + @Override public ImmediateRequestI CreatePreflightRequestMaker() { + return null; + } }; countlyConfig.metricProviderOverride = new MockedMetricProvider(); Countly.sharedInstance().init(countlyConfig); diff --git a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java index 49dd31eac..7ec1c2960 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java +++ b/sdk/src/main/java/ly/count/android/sdk/ConnectionProcessor.java @@ -247,6 +247,63 @@ private enum RequestResult { return conn; } + synchronized public @NonNull URLConnection urlConnectionForPreflightRequest(@NonNull String preflightData) throws IOException { + long approxSize = preflightData.length(); + URL url = new URL(preflightData); + + long tOpen = pcc != null ? UtilsTime.getNanoTime() : 0; + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + if (conn instanceof HttpsURLConnection && (Countly.publicKeyPinCertificates != null || Countly.certificatePinCertificates != null)) { + ((HttpsURLConnection) conn).setSSLSocketFactory(sslContext_.getSocketFactory()); + } + + if (pcc != null) { + pcc.TrackCounterTimeNs("ConnectionProcessorUrlConnectionForPreflightRequest_01_OpenURLConnection", UtilsTime.getNanoTime() - tOpen); + tOpen = UtilsTime.getNanoTime(); + } + + conn.setRequestMethod("HEAD"); + conn.setConnectTimeout(CONNECT_TIMEOUT_IN_MILLISECONDS); + conn.setReadTimeout(READ_TIMEOUT_IN_MILLISECONDS); + conn.setUseCaches(false); + conn.setDoInput(true); + conn.setDoOutput(false); + conn.setInstanceFollowRedirects(true); + + if (requestHeaderCustomValues_ != null) { + L.v("[ConnectionProcessor] Adding [" + requestHeaderCustomValues_.size() + "] custom header fields"); + for (Map.Entry e : requestHeaderCustomValues_.entrySet()) { + if (e.getKey() != null && e.getValue() != null && !e.getKey().isEmpty()) { + conn.addRequestProperty(e.getKey(), e.getValue()); + } + } + } + + if (pcc != null) { + pcc.TrackCounterTimeNs("ConnectionProcessorUrlConnectionForPreflightRequest_02_ConfigureConnection", UtilsTime.getNanoTime() - tOpen); + tOpen = UtilsTime.getNanoTime(); + } + + try { + for (int i = 0; ; i++) { + String key = conn.getHeaderFieldKey(i); + if (key == null) break; + String value = conn.getHeaderField(i); + approxSize += key.getBytes(StandardCharsets.US_ASCII).length + value.getBytes(StandardCharsets.US_ASCII).length + 2L; + } + } catch (Exception e) { + L.e("[ConnectionProcessor] urlConnectionForPreflightRequest, exception while calculating header field size: " + e); + } + + if (pcc != null) { + pcc.TrackCounterTimeNs("ConnectionProcessorUrlConnectionForPreflightRequest_03_HeaderFieldSize", UtilsTime.getNanoTime() - tOpen); + } + + L.v("[ConnectionProcessor] urlConnectionForPreflightRequest, Approx data size: [" + approxSize + " B]"); + return conn; + } + /** * Return the size of the text multipart entry * diff --git a/sdk/src/main/java/ly/count/android/sdk/Countly.java b/sdk/src/main/java/ly/count/android/sdk/Countly.java index 09bb8ca5a..f974a1491 100644 --- a/sdk/src/main/java/ly/count/android/sdk/Countly.java +++ b/sdk/src/main/java/ly/count/android/sdk/Countly.java @@ -493,6 +493,10 @@ public synchronized Countly init(CountlyConfig config) { @Override public ImmediateRequestI CreateImmediateRequestMaker() { return (new ImmediateRequestMaker()); } + + @Override public ImmediateRequestI CreatePreflightRequestMaker() { + return (new PreflightRequestMaker()); + } }; } diff --git a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java index 0d4a48746..d38f58a5a 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java +++ b/sdk/src/main/java/ly/count/android/sdk/ImmediateRequestGenerator.java @@ -2,4 +2,6 @@ interface ImmediateRequestGenerator { ImmediateRequestI CreateImmediateRequestMaker(); + + ImmediateRequestI CreatePreflightRequestMaker(); } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java index 008f4794e..47bc6df8f 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -76,28 +76,37 @@ void fetchContentsInternal(@NonNull String[] categories) { try { if (validateResponse(checkResponse)) { L.d("[ModuleContent] fetchContentsInternal, got new content data, showing it"); - Map placementCoordinates = parseContent(checkResponse, displayMetrics); - if (placementCoordinates.isEmpty()) { - L.d("[ModuleContent] fetchContentsInternal, placement coordinates are empty, skipping"); - return; - } - - Intent intent = new Intent(_cly.context_, TransparentActivity.class); - intent.putExtra(TransparentActivity.CONFIGURATION_LANDSCAPE, placementCoordinates.get(Configuration.ORIENTATION_LANDSCAPE)); - intent.putExtra(TransparentActivity.CONFIGURATION_PORTRAIT, placementCoordinates.get(Configuration.ORIENTATION_PORTRAIT)); - intent.putExtra(TransparentActivity.ORIENTATION, _cly.context_.getResources().getConfiguration().orientation); - - Long id = System.currentTimeMillis(); - intent.putExtra(TransparentActivity.ID_CALLBACK, id); - if (globalContentCallback != null) { - TransparentActivity.contentCallbacks.put(id, globalContentCallback); - } - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - _cly.context_.startActivity(intent); - - shouldFetchContents = false; // disable fetching contents until the next time, this will disable the timer fetching - isCurrentlyInContentZone = true; + + iRGenerator.CreatePreflightRequestMaker().doWork(checkResponse.optString("html"), null, cp, false, true, preflightResponse -> { + if (preflightResponse == null) { + L.d("[ModuleContent] fetchContentsInternal, preflight check failed, skipping showing content"); + return; + } + + Map placementCoordinates = parseContent(checkResponse, displayMetrics); + if (placementCoordinates.isEmpty()) { + L.d("[ModuleContent] fetchContentsInternal, placement coordinates are empty, skipping"); + return; + } + + L.d("[ModuleContent] fetchContentsInternal, preflight check succeeded"); + Intent intent = new Intent(_cly.context_, TransparentActivity.class); + intent.putExtra(TransparentActivity.CONFIGURATION_LANDSCAPE, placementCoordinates.get(Configuration.ORIENTATION_LANDSCAPE)); + intent.putExtra(TransparentActivity.CONFIGURATION_PORTRAIT, placementCoordinates.get(Configuration.ORIENTATION_PORTRAIT)); + intent.putExtra(TransparentActivity.ORIENTATION, _cly.context_.getResources().getConfiguration().orientation); + + Long id = System.currentTimeMillis(); + intent.putExtra(TransparentActivity.ID_CALLBACK, id); + if (globalContentCallback != null) { + TransparentActivity.contentCallbacks.put(id, globalContentCallback); + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + _cly.context_.startActivity(intent); + + shouldFetchContents = false; // disable fetching contents until the next time, this will disable the timer fetching + isCurrentlyInContentZone = true; + }, L); } else { L.w("[ModuleContent] fetchContentsInternal, response is not valid, skipping"); } diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java index cec962def..228596859 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java @@ -268,35 +268,45 @@ void presentFeedbackWidgetInternal(@Nullable final CountlyFeedbackWidget widgetI widgetListUrl.append(customObjectToSendWithTheWidget); String preparedWidgetUrl = widgetListUrl.toString(); - L.d("[ModuleFeedback] Using following url for widget:[" + preparedWidgetUrl + "]"); - if (!Utils.isNullOrEmpty(widgetInfo.widgetVersion)) { - L.d("[ModuleFeedback] Will use transparent activity for displaying the widget"); - showFeedbackWidget_newActivity(context, preparedWidgetUrl, widgetInfo, devCallback); - } else { - L.d("[ModuleFeedback] Will use dialog for displaying the widget"); - //enable for chrome debugging - // WebView.setWebContentsDebuggingEnabled(true); - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { - public void run() { - L.d("[ModuleFeedback] Calling on main thread"); - try { - showFeedbackWidget(context, widgetInfo, closeButtonText, devCallback, preparedWidgetUrl); + iRGenerator.CreatePreflightRequestMaker().doWork(preparedWidgetUrl, null, requestQueueProvider.createConnectionProcessor(), false, true, preflightResponse -> { + if (preflightResponse == null) { + L.e("[ModuleFeedback] Failed to do preflight check for the widget url"); + if (devCallback != null) { + devCallback.onFinished("Failed to do preflight check for the widget url"); + } + return; + } - if (devCallback != null) { - devCallback.onFinished(null); - } - } catch (Exception ex) { - L.e("[ModuleFeedback] Failed at displaying feedback widget dialog, [" + ex.toString() + "]"); - if (devCallback != null) { - devCallback.onFinished("Failed at displaying feedback widget dialog, [" + ex.toString() + "]"); + if (!Utils.isNullOrEmpty(widgetInfo.widgetVersion)) { + L.d("[ModuleFeedback] Will use transparent activity for displaying the widget"); + showFeedbackWidget_newActivity(context, preparedWidgetUrl, widgetInfo, devCallback); + } else { + L.d("[ModuleFeedback] Will use dialog for displaying the widget"); + //enable for chrome debugging + // WebView.setWebContentsDebuggingEnabled(true); + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + L.d("[ModuleFeedback] Calling on main thread"); + + try { + showFeedbackWidget(context, widgetInfo, closeButtonText, devCallback, preparedWidgetUrl); + + if (devCallback != null) { + devCallback.onFinished(null); + } + } catch (Exception ex) { + L.e("[ModuleFeedback] Failed at displaying feedback widget dialog, [" + ex.toString() + "]"); + if (devCallback != null) { + devCallback.onFinished("Failed at displaying feedback widget dialog, [" + ex.toString() + "]"); + } } } - } - }); - } + }); + } + }, L); } private void showFeedbackWidget(Context context, CountlyFeedbackWidget widgetInfo, String closeButtonText, FeedbackCallback devCallback, String url) { diff --git a/sdk/src/main/java/ly/count/android/sdk/PreflightRequestMaker.java b/sdk/src/main/java/ly/count/android/sdk/PreflightRequestMaker.java new file mode 100644 index 000000000..aa4f65828 --- /dev/null +++ b/sdk/src/main/java/ly/count/android/sdk/PreflightRequestMaker.java @@ -0,0 +1,78 @@ +package ly.count.android.sdk; + +import android.os.AsyncTask; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.io.IOException; +import java.net.HttpURLConnection; +import org.json.JSONObject; + +class PreflightRequestMaker extends AsyncTask implements ImmediateRequestI { + + ImmediateRequestMaker.InternalImmediateRequestCallback callback; + ModuleLog L; + + @Override + public void doWork(@NonNull String requestData, @Nullable String customEndpoint, @NonNull ConnectionProcessor cp, boolean requestShouldBeDelayed, boolean networkingIsEnabled, @NonNull ImmediateRequestMaker.InternalImmediateRequestCallback callback, @NonNull ModuleLog log) { + assert Utils.isNotNullOrEmpty(requestData); + assert cp != null; + assert log != null; + assert callback != null; + + this.execute(requestData, customEndpoint, cp, requestShouldBeDelayed, networkingIsEnabled, callback, log); + } + + /** + * params fields: + * 0 - requestData + * 1 - custom endpoint + * 2 - connection processor + * 3 - requestShouldBeDelayed + * 4 - networkingIsEnabled + * 5 - callback + * 6 - log module + */ + protected Boolean doInBackground(Object... params) { + final String urlRequest = (String) params[0]; + final ConnectionProcessor cp = (ConnectionProcessor) params[2]; + callback = (ImmediateRequestMaker.InternalImmediateRequestCallback) params[5]; + L = (ModuleLog) params[6]; + + L.v("[PreflightRequestMaker] doPreflightRequest, Starting preflight request"); + + HttpURLConnection connection = null; + + try { + //getting connection ready + try { + connection = (HttpURLConnection) cp.urlConnectionForPreflightRequest(urlRequest); + } catch (IOException e) { + L.e("[PreflightRequestMaker] doPreflightRequest, IOException while preparing preflight request :[" + e + "]"); + return null; + } + + int responseCode = connection.getResponseCode(); + + L.v("[PreflightRequestMaker] doPreflightRequest, Preflight request finished, response code: " + responseCode); + return responseCode >= 200 && responseCode < 400; + } catch (Exception e) { + L.e("[PreflightRequestMaker] doPreflightRequest, Received exception while making a immediate server request", e); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + L.v("[PreflightRequestMaker] doPreflightRequest, Finished request"); + return false; + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + L.v("[PreflightRequestMaker] onPostExecute"); + + if (callback != null) { + callback.callback(result ? new JSONObject() : null); + } + } +}