Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 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.

## 25.4.0
* ! Minor breaking change ! Removed Secure.ANDROID_ID usage in device id generation. The SDK now exclusively uses random UUIDs for device id generation.
* ! Minor breaking change ! Server Configuration is now enabled by default. Changes made on SDK Manager > SDK Configuration on your server will affect SDK behavior directly.
Expand Down
10 changes: 7 additions & 3 deletions sdk/src/main/java/ly/count/android/sdk/ModuleContent.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}

Expand Down
173 changes: 117 additions & 56 deletions sdk/src/main/java/ly/count/android/sdk/ModuleFeedback.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@
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;
Expand All @@ -22,11 +27,12 @@ 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";
Expand Down Expand Up @@ -123,6 +129,7 @@ static List<CountlyFeedbackWidget> parseFeedbackList(JSONObject requestResponse)
String valId = jObj.optString("_id", "");
String valType = jObj.optString("type", "");
String valName = jObj.optString("name", "");
String widgetVersion = jObj.optString("wv", "");
List<String> valTagsArr = new ArrayList<String>();

JSONArray jTagArr = jObj.optJSONArray("tg");
Expand Down Expand Up @@ -161,6 +168,7 @@ static List<CountlyFeedbackWidget> 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) {
Expand Down Expand Up @@ -247,90 +255,127 @@ void presentFeedbackWidgetInternal(@Nullable final CountlyFeedbackWidget widgetI
JSONObject customObjectToSendWithTheWidget = new JSONObject();
try {
customObjectToSendWithTheWidget.put("tc", 1);
customObjectToSendWithTheWidget.put("rw", 1);
customObjectToSendWithTheWidget.put("xb", 1);
} catch (JSONException e) {
throw new RuntimeException(e);
}
widgetListUrl.append("&custom=");
widgetListUrl.append(customObjectToSendWithTheWidget.toString());
widgetListUrl.append(customObjectToSendWithTheWidget);

final String preparedWidgetUrl = widgetListUrl.toString();
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");

try {

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)) {
showFeedbackWidget_newActivity(context, preparedWidgetUrl, widgetInfo, devCallback);
} else {
//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";
}
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();
}

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<Integer, TransparentActivityConfig> 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<String, Object> 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, @NonNull String appVersion) {
void reportFeedbackWidgetCancelButton(@NonNull CountlyFeedbackWidget widgetInfo) {
L.d("[reportFeedbackWidgetCancelButton] Cancel button event");
if (consentProvider.getConsent(Countly.CountlyFeatureNames.feedback)) {
final Map<String, Object> 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;
Expand Down Expand Up @@ -681,6 +726,7 @@ public void getAvailableFeedbackWidgets(@Nullable RetrieveFeedbackWidgets callba
* @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) {
Expand All @@ -690,6 +736,21 @@ public void presentFeedbackWidget(@Nullable CountlyFeedbackWidget widgetInfo, @N
}
}

/**
* Present a chosen feedback widget in an alert dialog
*
* @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 in an alert dialog");

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)
Expand Down Expand Up @@ -820,4 +881,4 @@ public void presentRating(@NonNull Context context, @NonNull String nameIDorTag,
}
}
}
}
}
9 changes: 9 additions & 0 deletions sdk/src/main/java/ly/count/android/sdk/ModuleRatings.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading
Loading