diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cb21db1b..2d212e874 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ ## XX.XX.XX +* The feedback widgets now have transparent backgrounds for a cleaner look. + +* Deprecated "presentFeedbackWidget(widgetInfo, context, closeButtonText, devCallback)", replaced with "presentFeedbackWidget(widgetInfo, context, devCallback)" in the feedbacks. * Added a config method to disable server config in the initialization "disableSDKBehaviorSettings()". ## 25.4.0 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 a5b581ade..008f4794e 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleContent.java @@ -86,6 +86,13 @@ void fetchContentsInternal(@NonNull String[] categories) { 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); @@ -213,9 +220,6 @@ private TransparentActivityConfig extractOrientationPlacements(@NonNull JSONObje TransparentActivityConfig config = new TransparentActivityConfig((int) Math.ceil(x * density), (int) Math.ceil(y * density), (int) Math.ceil(w * density), (int) Math.ceil(h * density)); config.url = content; - // TODO, passing callback with an intent is impossible, need to find a way to pass it - // Currently, the callback is set as a static variable in TransparentActivity - TransparentActivity.globalContentCallback = globalContentCallback; return config; } 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 5531c4a0e..cec962def 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java @@ -3,30 +3,37 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Handler; import android.os.Looper; +import android.util.DisplayMetrics; import android.webkit.WebSettings; -import android.webkit.WebView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +// import android.webkit.WebView; public class ModuleFeedback extends ModuleBase { public enum FeedbackWidgetType {survey, nps, rating} - public static class CountlyFeedbackWidget { + public static class CountlyFeedbackWidget implements Serializable { public String widgetId; public FeedbackWidgetType type; public String name; public String[] tags; + public String widgetVersion; } final static String NPS_EVENT_KEY = "[CLY]_nps"; @@ -123,6 +130,7 @@ static List parseFeedbackList(JSONObject requestResponse) String valId = jObj.optString("_id", ""); String valType = jObj.optString("type", ""); String valName = jObj.optString("name", ""); + String widgetVersion = jObj.isNull("wv") ? null : jObj.optString("wv", null); List valTagsArr = new ArrayList(); JSONArray jTagArr = jObj.optJSONArray("tg"); @@ -161,6 +169,7 @@ static List parseFeedbackList(JSONObject requestResponse) se.widgetId = valId; se.name = valName; se.tags = valTagsArr.toArray(new String[0]); + se.widgetVersion = widgetVersion; parsedRes.add(se); } catch (Exception ex) { @@ -247,67 +256,61 @@ void presentFeedbackWidgetInternal(@Nullable final CountlyFeedbackWidget widgetI JSONObject customObjectToSendWithTheWidget = new JSONObject(); try { customObjectToSendWithTheWidget.put("tc", 1); + // these are used only in case of a widget with a version + if (!Utils.isNullOrEmpty(widgetInfo.widgetVersion)) { + customObjectToSendWithTheWidget.put("rw", 1); + customObjectToSendWithTheWidget.put("xb", 1); + } } catch (JSONException e) { throw new RuntimeException(e); } widgetListUrl.append("&custom="); - widgetListUrl.append(customObjectToSendWithTheWidget.toString()); - - final String preparedWidgetUrl = widgetListUrl.toString(); - - L.d("[ModuleFeedback] Using following url for widget:[" + widgetListUrl + "]"); - - //enable for chrome debugging - //WebView.setWebContentsDebuggingEnabled(true); - - final boolean useAlertDialog = true; - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { - public void run() { - L.d("[ModuleFeedback] Calling on main thread"); + widgetListUrl.append(customObjectToSendWithTheWidget); - try { + String preparedWidgetUrl = widgetListUrl.toString(); - ModuleRatings.RatingDialogWebView webView = new ModuleRatings.RatingDialogWebView(context); - webView.getSettings().setJavaScriptEnabled(true); - webView.clearCache(true); - webView.clearHistory(); - webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); - webView.setWebViewClient(new ModuleRatings.FeedbackDialogWebViewClient()); - webView.loadUrl(preparedWidgetUrl); - webView.requestFocus(); - - AlertDialog.Builder builder = prepareAlertDialog(context, webView, closeButtonText, widgetInfo, devCallback); - - if (useAlertDialog) { - // use alert dialog to host the webView - L.d("[ModuleFeedback] Creating standalone Alert dialog"); - builder.show(); - } else { - // use dialog fragment to host the webView - L.d("[ModuleFeedback] Creating Alert dialog in dialogFragment"); + 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"); - //CountlyDialogFragment newFragment = CountlyDialogFragment.newInstance(builder); - //newFragment.show(fragmentManager, "CountlyFragmentDialog"); - } + 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() + "]"); + 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() + "]"); + } } } - } - }); + }); + } } - AlertDialog.Builder prepareAlertDialog(@NonNull final Context context, @NonNull WebView webView, @Nullable String closeButtonText, @NonNull final CountlyFeedbackWidget widgetInfo, @Nullable final FeedbackCallback devCallback) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setView(webView); - builder.setCancelable(false); + private void showFeedbackWidget(Context context, CountlyFeedbackWidget widgetInfo, String closeButtonText, FeedbackCallback devCallback, String url) { + ModuleRatings.RatingDialogWebView webView = new ModuleRatings.RatingDialogWebView(context); + webView.getSettings().setJavaScriptEnabled(true); + webView.clearCache(true); + webView.clearHistory(); + webView.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); + ModuleRatings.FeedbackDialogWebViewClient webViewClient = new ModuleRatings.FeedbackDialogWebViewClient(); + webView.setWebViewClient(webViewClient); + webView.loadUrl(url); + webView.requestFocus(); + + AlertDialog.Builder builder = new AlertDialog.Builder(context).setView(webView).setCancelable(false); String usedCloseButtonText = closeButtonText; if (closeButtonText == null || closeButtonText.isEmpty()) { usedCloseButtonText = "Close"; @@ -315,22 +318,70 @@ AlertDialog.Builder prepareAlertDialog(@NonNull final Context context, @NonNull builder.setNeutralButton(usedCloseButtonText, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { L.d("[ModuleFeedback] Cancel button clicked for the feedback widget"); - reportFeedbackWidgetCancelButton(widgetInfo, deviceInfo.mp.getAppVersion(context)); + reportFeedbackWidgetCancelButton(widgetInfo); if (devCallback != null) { devCallback.onClosed(); } } }); - return builder; + builder.show(); } - void reportFeedbackWidgetCancelButton(@NonNull CountlyFeedbackWidget widgetInfo, @NonNull String appVersion) { + private void showFeedbackWidget_newActivity(@NonNull Context context, String url, CountlyFeedbackWidget widgetInfo, FeedbackCallback devCallback) { + DisplayMetrics displayMetrics = deviceInfo.mp.getDisplayMetrics(context); + Resources resources = context.getResources(); + int currentOrientation = resources.getConfiguration().orientation; + boolean portrait = currentOrientation == Configuration.ORIENTATION_PORTRAIT; + + int width = displayMetrics.widthPixels; + int height = displayMetrics.heightPixels; + + // this calculation needs improvement for status bar and navigation bar + int portraitWidth = portrait ? width : height; + int portraitHeight = portrait ? height : width; + int landscapeWidth = portrait ? height : width; + int landscapeHeight = portrait ? width : height; + + Map placementCoordinates = new ConcurrentHashMap<>(); + TransparentActivityConfig pConfig = new TransparentActivityConfig(0, 0, portraitWidth, portraitHeight); + TransparentActivityConfig lConfig = new TransparentActivityConfig(0, 0, landscapeWidth, landscapeHeight); + pConfig.url = url; + lConfig.url = url; + placementCoordinates.put(Configuration.ORIENTATION_PORTRAIT, pConfig); + placementCoordinates.put(Configuration.ORIENTATION_LANDSCAPE, lConfig); + + Intent intent = new Intent(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, context.getResources().getConfiguration().orientation); + intent.putExtra(TransparentActivity.WIDGET_INFO, widgetInfo); + + Long id = System.currentTimeMillis(); + intent.putExtra(TransparentActivity.ID_CALLBACK, id); + if (devCallback != null) { + ContentCallback feedbackCallback = new ContentCallback() { + @Override public void onContentCallback(ContentStatus contentStatus, Map contentData) { + if (contentStatus.equals(ContentStatus.CLOSED)) { + devCallback.onClosed(); + } else { + devCallback.onFinished(null); + } + } + }; + TransparentActivity.contentCallbacks.put(id, feedbackCallback); + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + _cly.context_.startActivity(intent); + } + + void reportFeedbackWidgetCancelButton(@NonNull CountlyFeedbackWidget widgetInfo) { L.d("[reportFeedbackWidgetCancelButton] Cancel button event"); if (consentProvider.getConsent(Countly.CountlyFeatureNames.feedback)) { final Map segm = new HashMap<>(); segm.put("platform", "android"); - segm.put("app_version", appVersion); + segm.put("app_version", cachedAppVersion); segm.put("widget_id", "" + widgetInfo.widgetId); segm.put("closed", "1"); final String key; @@ -675,21 +726,37 @@ public void getAvailableFeedbackWidgets(@Nullable RetrieveFeedbackWidgets callba } /** - * Present a chosen feedback widget in an alert dialog + * Present a chosen feedback widget * * @param widgetInfo * @param context * @param closeButtonText if this is null, no "close" button will be shown * @param devCallback + * @deprecated use {@link #presentFeedbackWidget(CountlyFeedbackWidget, Context, FeedbackCallback)} instead */ public void presentFeedbackWidget(@Nullable CountlyFeedbackWidget widgetInfo, @Nullable Context context, @Nullable String closeButtonText, @Nullable FeedbackCallback devCallback) { synchronized (_cly) { - L.i("[Feedback] Trying to present feedback widget in an alert dialog"); + L.i("[Feedback] Trying to present feedback widget"); presentFeedbackWidgetInternal(widgetInfo, context, closeButtonText, devCallback); } } + /** + * Present a chosen feedback widget + * + * @param widgetInfo the widget to present + * @param context the context to use for displaying the feedback widget + * @param devCallback callback to be called when the feedback widget is closed + */ + public void presentFeedbackWidget(@Nullable CountlyFeedbackWidget widgetInfo, @Nullable Context context, @Nullable FeedbackCallback devCallback) { + synchronized (_cly) { + L.i("[Feedback] Trying to present feedback widget"); + + presentFeedbackWidgetInternal(widgetInfo, context, null, devCallback); + } + } + /** * Download data for a specific widget so that it can be displayed with a custom UI * When requesting this data, it will count as a shown widget (will increment that "shown" count in the dashboard) @@ -820,4 +887,4 @@ public void presentRating(@NonNull Context context, @NonNull String nameIDorTag, } } } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java index 6af16f3a1..cc9ea10cc 100644 --- a/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java +++ b/sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java @@ -566,10 +566,19 @@ public boolean onCheckIsTextEditor() { } static class FeedbackDialogWebViewClient extends WebViewClient { + + WebViewUrlListener listener; + @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { String url = request.getUrl().toString(); + if (listener != null) { + if (listener.onUrl(url, view)) { + return true; + } + } + // Filter out outgoing calls if (url.endsWith("cly_x_int=1")) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); diff --git a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java index e3995d0a6..64e653b32 100644 --- a/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java +++ b/sdk/src/main/java/ly/count/android/sdk/TransparentActivity.java @@ -30,13 +30,15 @@ public class TransparentActivity extends Activity { static final String CONFIGURATION_LANDSCAPE = "Landscape"; static final String CONFIGURATION_PORTRAIT = "Portrait"; static final String ORIENTATION = "orientation"; - private static final String URL_START = "https://countly_action_event"; + static final String WIDGET_INFO = "widget_info"; + static final String ID_CALLBACK = "id_callback"; int currentOrientation = 0; + long ID = -1; TransparentActivityConfig configLandscape = null; TransparentActivityConfig configPortrait = null; WebView webView; RelativeLayout relativeLayout; - static ContentCallback globalContentCallback; + static Map contentCallbacks = new ConcurrentHashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { @@ -50,7 +52,8 @@ protected void onCreate(Bundle savedInstanceState) { // Get extras Intent intent = getIntent(); - currentOrientation = (int) intent.getSerializableExtra(ORIENTATION); + ID = intent.getLongExtra(ID_CALLBACK, -1); + currentOrientation = intent.getIntExtra(ORIENTATION, 0); configLandscape = (TransparentActivityConfig) intent.getSerializableExtra(CONFIGURATION_LANDSCAPE); configPortrait = (TransparentActivityConfig) intent.getSerializableExtra(CONFIGURATION_PORTRAIT); Log.v(Countly.TAG, "[TransparentActivity] onCreate, orientation: " + currentOrientation); @@ -66,15 +69,6 @@ protected void onCreate(Bundle savedInstanceState) { config = setupConfig(config); - WebViewUrlListener listener = new WebViewUrlListener() { - @Override public boolean onUrl(String url, WebView webView) { - if (url.startsWith(URL_START)) { - return contentUrlAction(url, webView); - } - return false; - } - }; - // Configure window layout parameters WindowManager.LayoutParams params = new WindowManager.LayoutParams(); params.gravity = Gravity.TOP | Gravity.LEFT; // try out START @@ -196,6 +190,27 @@ private void resizeContentInternal() { } } + private boolean widgetUrlAction(String url, WebView view) { + Map query = splitQuery(url); + Object widgetCommand = query.get("?cly_widget_command"); + + if (widgetCommand == null || !widgetCommand.equals("1")) { + Log.w(Countly.TAG, "[TransparentActivity] widgetUrlAction, event:[" + widgetCommand + "] this is not a countly widget command url"); + return false; + } + + if (query.containsKey("close") && Objects.equals(query.get("close"), "1")) { + if (Countly.sharedInstance().isInitialized()) { + close(query); + + ModuleFeedback.CountlyFeedbackWidget widgetInfo = (ModuleFeedback.CountlyFeedbackWidget) getIntent().getSerializableExtra(WIDGET_INFO); + Countly.sharedInstance().moduleFeedback.reportFeedbackWidgetCancelButton(widgetInfo); + } + } + + return true; + } + private boolean contentUrlAction(String url, WebView view) { Log.d(Countly.TAG, "[TransparentActivity] contentUrlAction, url: [" + url + "]"); Map query = splitQuery(url); @@ -230,14 +245,11 @@ private boolean contentUrlAction(String url, WebView view) { } if (query.containsKey("close") && Objects.equals(query.get("close"), "1")) { - if (globalContentCallback != null) { // TODO: verify this later - globalContentCallback.onContentCallback(ContentStatus.CLOSED, query); - } + close(query); if (Countly.sharedInstance().isInitialized()) { Countly.sharedInstance().moduleContent.notifyAfterContentIsClosed(); } - finish(); } return true; @@ -342,7 +354,7 @@ private void eventAction(Map query) { private Map splitQuery(String url) { Map query_pairs = new ConcurrentHashMap<>(); - String[] pairs = url.split("https://countly_action_event/?"); + String[] pairs = url.split(Utils.COMM_URL + "/?"); if (pairs.length != 2) { return query_pairs; } @@ -369,6 +381,14 @@ private Map splitQuery(String url) { return query_pairs; } + private void close(Map contentData) { + if (contentCallbacks.get(ID) != null) { + contentCallbacks.get(ID).onContentCallback(ContentStatus.CLOSED, contentData); + contentCallbacks.remove(ID); + } + super.finish(); + } + @SuppressLint("SetJavaScriptEnabled") private WebView createWebView(TransparentActivityConfig config) { WebView webView = new CountlyWebView(this); @@ -386,9 +406,20 @@ private WebView createWebView(TransparentActivityConfig config) { CountlyWebViewClient client = new CountlyWebViewClient(); client.registerWebViewUrlListener(new WebViewUrlListener() { @Override public boolean onUrl(String url, WebView webView) { - if (url.startsWith(URL_START)) { - return contentUrlAction(url, webView); + if (url.startsWith(Utils.COMM_URL)) { + if (url.contains("cly_x_action_event")) { + return contentUrlAction(url, webView); + } else if (url.contains("cly_widget_command")) { + return widgetUrlAction(url, webView); + } } + + if (url.endsWith("cly_x_int=1")) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(intent); + return true; + } + return false; } }); @@ -397,4 +428,4 @@ private WebView createWebView(TransparentActivityConfig config) { webView.loadUrl(config.url); return webView; } -} +} \ No newline at end of file diff --git a/sdk/src/main/java/ly/count/android/sdk/Utils.java b/sdk/src/main/java/ly/count/android/sdk/Utils.java index 20ca4e089..506ed2c63 100644 --- a/sdk/src/main/java/ly/count/android/sdk/Utils.java +++ b/sdk/src/main/java/ly/count/android/sdk/Utils.java @@ -24,6 +24,10 @@ import static android.content.Context.UI_MODE_SERVICE; public class Utils { + /** + * This is a communication url between web views and the SDK + */ + protected static final String COMM_URL = "https://countly_action_event"; private static final ExecutorService bg = Executors.newSingleThreadExecutor(); public static Future runInBackground(Runnable runnable) {