From 7ee9714bff991675042e7929ae269272f69a4706 Mon Sep 17 00:00:00 2001 From: Mahmoud Elmorabea Date: Tue, 22 Oct 2024 16:22:34 +0300 Subject: [PATCH] chore: Update settings page UX (#455) --- .../java_layout/src/main/AndroidManifest.xml | 4 + .../data/model/CustomerIOSDKConfig.java | 123 +++-- .../java_layout/sdk/CustomerIORepository.java | 46 +- .../ui/dashboard/DashboardActivity.java | 5 + .../java_layout/ui/login/LoginActivity.java | 5 + .../ui/settings/InternalSettingsActivity.java | 166 ++++++ .../ui/settings/SettingsActivity.java | 242 ++++----- .../java_layout/utils/DefaultTextWatcher.java | 25 + .../sample/java_layout/utils/StringUtils.java | 2 +- .../sample/java_layout/utils/ViewUtils.java | 12 + .../res/layout/activity_internal_settings.xml | 147 ++++++ .../src/main/res/layout/activity_settings.xml | 494 +++++++++++------- .../src/main/res/values-night/colors.xml | 143 +++++ .../src/main/res/values-night/themes.xml | 73 ++- .../src/main/res/values/colors.xml | 210 +++++--- .../src/main/res/values/strings.xml | 21 + .../src/main/res/values/styles.xml | 6 + .../src/main/res/values/themes.xml | 72 ++- 18 files changed, 1285 insertions(+), 511 deletions(-) create mode 100644 samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/InternalSettingsActivity.java create mode 100644 samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/DefaultTextWatcher.java create mode 100644 samples/java_layout/src/main/res/layout/activity_internal_settings.xml create mode 100644 samples/java_layout/src/main/res/values-night/colors.xml create mode 100644 samples/java_layout/src/main/res/values/styles.xml diff --git a/samples/java_layout/src/main/AndroidManifest.xml b/samples/java_layout/src/main/AndroidManifest.xml index 55127d4bb..341585076 100644 --- a/samples/java_layout/src/main/AndroidManifest.xml +++ b/samples/java_layout/src/main/AndroidManifest.xml @@ -123,6 +123,10 @@ android:name=".ui.common.SimpleFragmentActivity" android:exported="false" android:label="@string/label_simple_fragment_activity" /> + fromMap(@NonNull Map CustomerIOSDKConfig defaultConfig = getDefaultConfigurations(); String apiHost = bundle.get(Keys.API_HOST); String cdnHost = bundle.get(Keys.CDN_HOST); - Integer flushInterval = StringUtils.parseInteger(bundle.get(Keys.FLUSH_INTERVAL), defaultConfig.flushInterval); - Integer flushAt = StringUtils.parseInteger(bundle.get(Keys.FLUSH_AT), defaultConfig.flushAt); boolean screenTrackingEnabled = StringUtils.parseBoolean(bundle.get(Keys.TRACK_SCREENS), defaultConfig.screenTrackingEnabled); boolean deviceAttributesTrackingEnabled = StringUtils.parseBoolean(bundle.get(Keys.TRACK_DEVICE_ATTRIBUTES), defaultConfig.deviceAttributesTrackingEnabled); - boolean debugModeEnabled = StringUtils.parseBoolean(bundle.get(Keys.DEBUG_MODE), defaultConfig.debugModeEnabled); + CioLogLevel logLevel = CioLogLevel.Companion.getLogLevel(bundle.get(Keys.LOG_LEVEL), CioLogLevel.DEBUG); + Region region = Region.Companion.getRegion(bundle.get(Keys.REGION), Region.US.INSTANCE); + boolean applicationLifecycleTrackingEnabled = StringUtils.parseBoolean(bundle.get(Keys.TRACK_APPLICATION_LIFECYCLE), defaultConfig.applicationLifecycleTrackingEnabled); + boolean testModeEnabled = StringUtils.parseBoolean(bundle.get(Keys.TEST_MODE_ENABLED), defaultConfig.testModeEnabled); + boolean inAppMessagingEnabled = StringUtils.parseBoolean(bundle.get(Keys.IN_APP_MESSAGING_ENABLED), defaultConfig.inAppMessagingEnabled); CustomerIOSDKConfig config = new CustomerIOSDKConfig(cdpApiKey, siteId, apiHost, cdnHost, - flushInterval, - flushAt, screenTrackingEnabled, deviceAttributesTrackingEnabled, - debugModeEnabled); + logLevel, + region, + applicationLifecycleTrackingEnabled, + testModeEnabled, + inAppMessagingEnabled); return Optional.of(config); } @@ -78,11 +87,13 @@ public static Map toMap(@NonNull CustomerIOSDKConfig config) { bundle.put(Keys.SITE_ID, config.siteId); bundle.put(Keys.API_HOST, config.apiHost); bundle.put(Keys.CDN_HOST, config.cdnHost); - bundle.put(Keys.FLUSH_INTERVAL, StringUtils.fromInteger(config.flushInterval)); - bundle.put(Keys.FLUSH_AT, StringUtils.fromInteger(config.flushAt)); bundle.put(Keys.TRACK_SCREENS, StringUtils.fromBoolean(config.screenTrackingEnabled)); bundle.put(Keys.TRACK_DEVICE_ATTRIBUTES, StringUtils.fromBoolean(config.deviceAttributesTrackingEnabled)); - bundle.put(Keys.DEBUG_MODE, StringUtils.fromBoolean(config.debugModeEnabled)); + bundle.put(Keys.LOG_LEVEL, config.logLevel.name()); + bundle.put(Keys.REGION, config.getRegion().getCode()); + bundle.put(Keys.TRACK_APPLICATION_LIFECYCLE, StringUtils.fromBoolean(config.applicationLifecycleTrackingEnabled)); + bundle.put(Keys.TEST_MODE_ENABLED, StringUtils.fromBoolean(config.testModeEnabled)); + bundle.put(Keys.IN_APP_MESSAGING_ENABLED, StringUtils.fromBoolean(config.inAppMessagingEnabled)); return bundle; } @@ -94,35 +105,38 @@ public static Map toMap(@NonNull CustomerIOSDKConfig config) { private final String apiHost; @Nullable private final String cdnHost; - @Nullable - private final Integer flushInterval; - @Nullable - private final Integer flushAt; - @Nullable - private final Boolean screenTrackingEnabled; - @Nullable - private final Boolean deviceAttributesTrackingEnabled; - @Nullable - private final Boolean debugModeEnabled; + private final boolean screenTrackingEnabled; + private final boolean deviceAttributesTrackingEnabled; + @NonNull + private final CioLogLevel logLevel; + @NonNull + private final Region region; + private final boolean applicationLifecycleTrackingEnabled; + private final boolean testModeEnabled; + private final boolean inAppMessagingEnabled; public CustomerIOSDKConfig(@NonNull String cdpApiKey, @NonNull String siteId, @Nullable String apiHost, @Nullable String cdnHost, - @Nullable Integer flushInterval, - @Nullable Integer flushAt, - @Nullable Boolean screenTrackingEnabled, - @Nullable Boolean deviceAttributesTrackingEnabled, - @Nullable Boolean debugModeEnabled) { + boolean screenTrackingEnabled, + boolean deviceAttributesTrackingEnabled, + @NonNull CioLogLevel logLevel, + @NonNull Region region, + boolean applicationLifecycleTrackingEnabled, + boolean testModeEnabled, + boolean inAppMessagingEnabled) { this.cdpApiKey = cdpApiKey; this.siteId = siteId; this.apiHost = apiHost; this.cdnHost = cdnHost; - this.flushInterval = flushInterval; - this.flushAt = flushAt; this.screenTrackingEnabled = screenTrackingEnabled; this.deviceAttributesTrackingEnabled = deviceAttributesTrackingEnabled; - this.debugModeEnabled = debugModeEnabled; + this.logLevel = logLevel; + this.region = region; + this.applicationLifecycleTrackingEnabled = applicationLifecycleTrackingEnabled; + this.testModeEnabled = testModeEnabled; + this.inAppMessagingEnabled = inAppMessagingEnabled; } @NonNull @@ -145,48 +159,33 @@ public String getCdnHost() { return cdnHost; } - @Nullable - public Integer getFlushInterval() { - return flushInterval; - } - - @Nullable - public Integer getFlushAt() { - return flushAt; - } - - - @Nullable - public Boolean isScreenTrackingEnabled() { + public boolean isScreenTrackingEnabled() { return screenTrackingEnabled; } - @Nullable - public Boolean isDeviceAttributesTrackingEnabled() { + public boolean isDeviceAttributesTrackingEnabled() { return deviceAttributesTrackingEnabled; } - @Nullable - public Boolean isDebugModeEnabled() { - return debugModeEnabled; + @NonNull + public CioLogLevel getLogLevel() { + return logLevel; } - /** - * Features by default are nullable to help differentiate between default/null values and - * values set by user. - * Unwrapping nullable values here for ease of use by keeping single source of truth for whole - * sample app. - */ + public boolean isTestModeEnabled() { + return testModeEnabled; + } - public boolean screenTrackingEnabled() { - return Boolean.FALSE != screenTrackingEnabled; + public boolean isInAppMessagingEnabled() { + return inAppMessagingEnabled; } - public boolean deviceAttributesTrackingEnabled() { - return Boolean.FALSE != deviceAttributesTrackingEnabled; + public boolean isApplicationLifecycleTrackingEnabled() { + return applicationLifecycleTrackingEnabled; } - public boolean debugModeEnabled() { - return Boolean.FALSE != debugModeEnabled; + @NonNull + public Region getRegion() { + return region; } } diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/sdk/CustomerIORepository.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/sdk/CustomerIORepository.java index 952d64d25..05aa8a9be 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/sdk/CustomerIORepository.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/sdk/CustomerIORepository.java @@ -16,8 +16,6 @@ import io.customer.messagingpush.ModuleMessagingPushFCM; import io.customer.sdk.CustomerIO; import io.customer.sdk.CustomerIOBuilder; -import io.customer.sdk.core.util.CioLogLevel; -import io.customer.sdk.data.model.Region; /** * Repository class to hold all Customer.io related operations at single place @@ -31,24 +29,22 @@ public void initializeSdk(SampleApplication application) { // Initialize Customer.io SDK builder CustomerIOBuilder builder = new CustomerIOBuilder(application, sdkConfig.getCdpApiKey()); - // Enable detailed logging for debug builds. - if (sdkConfig.debugModeEnabled()) { - builder.logLevel(CioLogLevel.DEBUG); - } + // Modify SDK settings for testing purposes only. + // If you don't need to override any of these settings, you can skip this line. + configureSdk(builder, sdkConfig); // Enable optional features of the SDK by adding desired modules. // Enables push notification builder.addCustomerIOModule(new ModuleMessagingPushFCM()); - // Enables in-app messages - builder.addCustomerIOModule(new ModuleMessagingInApp( - new MessagingInAppModuleConfig.Builder(sdkConfig.getSiteId(), Region.US.INSTANCE) - .setEventListener(new InAppMessageEventListener(appGraph.getLogger())) - .build() - )); - // Modify SDK settings for testing purposes only. - // If you don't need to override any of these settings, you can skip this line. - configureSdk(builder, sdkConfig); + // Enables in-app messages + if (sdkConfig.isInAppMessagingEnabled()) { + builder.addCustomerIOModule(new ModuleMessagingInApp( + new MessagingInAppModuleConfig.Builder(sdkConfig.getSiteId(), sdkConfig.getRegion()) + .setEventListener(new InAppMessageEventListener(appGraph.getLogger())) + .build() + )); + } // Finally, build to finish initializing the SDK. builder.build(); @@ -72,21 +68,15 @@ private void configureSdk(CustomerIOBuilder builder, final CustomerIOSDKConfig s builder.cdnHost(cdnHost); } - if (sdkConfig.getFlushAt() != null) { - builder.flushAt(sdkConfig.getFlushAt()); - } - if (sdkConfig.getFlushInterval() != null) { - builder.flushInterval(sdkConfig.getFlushInterval()); + if (sdkConfig.isTestModeEnabled()) { + builder.flushAt(1); } - final Boolean screenTrackingEnabled = sdkConfig.isScreenTrackingEnabled(); - if (screenTrackingEnabled != null) { - builder.autoTrackActivityScreens(screenTrackingEnabled); - } - final Boolean deviceAttributesTrackingEnabled = sdkConfig.isDeviceAttributesTrackingEnabled(); - if (deviceAttributesTrackingEnabled != null) { - builder.autoTrackDeviceAttributes(deviceAttributesTrackingEnabled); - } + builder.autoTrackActivityScreens(sdkConfig.isScreenTrackingEnabled()); + builder.autoTrackDeviceAttributes(sdkConfig.isDeviceAttributesTrackingEnabled()); + builder.trackApplicationLifecycleEvents(sdkConfig.isApplicationLifecycleTrackingEnabled()); + builder.region(sdkConfig.getRegion()); + builder.logLevel(sdkConfig.getLogLevel()); } public void identify(@NonNull String email, @NonNull Map attributes) { diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/dashboard/DashboardActivity.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/dashboard/DashboardActivity.java index 720231f78..de935a540 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/dashboard/DashboardActivity.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/dashboard/DashboardActivity.java @@ -30,6 +30,7 @@ import io.customer.android.sample.java_layout.ui.common.SimpleFragmentActivity; import io.customer.android.sample.java_layout.ui.core.BaseActivity; import io.customer.android.sample.java_layout.ui.login.LoginActivity; +import io.customer.android.sample.java_layout.ui.settings.InternalSettingsActivity; import io.customer.android.sample.java_layout.ui.settings.SettingsActivity; import io.customer.android.sample.java_layout.ui.user.AuthViewModel; import io.customer.android.sample.java_layout.utils.Randoms; @@ -109,6 +110,10 @@ private void setupViews() { binding.settingsButton.setOnClickListener(view -> { startActivity(new Intent(DashboardActivity.this, SettingsActivity.class)); }); + binding.settingsButton.setOnLongClickListener(view -> { + startActivity(new Intent(DashboardActivity.this, InternalSettingsActivity.class)); + return true; + }); binding.sendRandomEventButton.setOnClickListener(view -> { sendRandomEvent(); }); diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/login/LoginActivity.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/login/LoginActivity.java index 7ee07c113..7a9e2b97d 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/login/LoginActivity.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/login/LoginActivity.java @@ -9,6 +9,7 @@ import io.customer.android.sample.java_layout.databinding.ActivityLoginBinding; import io.customer.android.sample.java_layout.ui.core.BaseActivity; import io.customer.android.sample.java_layout.ui.dashboard.DashboardActivity; +import io.customer.android.sample.java_layout.ui.settings.InternalSettingsActivity; import io.customer.android.sample.java_layout.ui.settings.SettingsActivity; import io.customer.android.sample.java_layout.ui.user.AuthViewModel; import io.customer.android.sample.java_layout.utils.Randoms; @@ -57,6 +58,10 @@ private void setupViews() { binding.settingsButton.setOnClickListener(view -> { startActivity(new Intent(LoginActivity.this, SettingsActivity.class)); }); + binding.settingsButton.setOnLongClickListener(view -> { + startActivity(new Intent(LoginActivity.this, InternalSettingsActivity.class)); + return true; + }); binding.loginButton.setOnClickListener(view -> { boolean isFormValid = true; String displayName = ViewUtils.getText(binding.displayNameTextInput); diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/InternalSettingsActivity.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/InternalSettingsActivity.java new file mode 100644 index 000000000..f53260ff4 --- /dev/null +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/InternalSettingsActivity.java @@ -0,0 +1,166 @@ +package io.customer.android.sample.java_layout.ui.settings; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.TextUtils; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import io.customer.android.sample.java_layout.R; +import io.customer.android.sample.java_layout.data.model.CustomerIOSDKConfig; +import io.customer.android.sample.java_layout.databinding.ActivityInternalSettingsBinding; +import io.customer.android.sample.java_layout.ui.core.BaseActivity; +import io.customer.android.sample.java_layout.ui.dashboard.DashboardActivity; +import io.customer.android.sample.java_layout.utils.OSUtils; +import io.customer.android.sample.java_layout.utils.ViewUtils; +import io.customer.sdk.CustomerIO; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + +public class InternalSettingsActivity extends BaseActivity { + + private SettingsViewModel settingsViewModel; + private final CompositeDisposable disposables = new CompositeDisposable(); + + @Override + protected ActivityInternalSettingsBinding inflateViewBinding() { + return ActivityInternalSettingsBinding.inflate(getLayoutInflater()); + } + + @Override + protected void injectDependencies() { + settingsViewModel = viewModelProvider.get(SettingsViewModel.class); + } + + @Override + protected void setupContent() { + setUpObservers(); + setUpActions(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + disposables.dispose(); + } + + private void setUpObservers() { + binding.settingsDeviceTokenLabel.setText(CustomerIO.instance().getRegisteredDeviceToken()); + settingsViewModel.getSDKConfigObservable().observe(this, config -> { + binding.progressIndicator.hide(); + updateUiWithConfig(config); + }); + } + + private void updateUiWithConfig(@NonNull CustomerIOSDKConfig config) { + ViewUtils.setTextWithSelectionIfFocused(binding.settingsApiHostLabel, config.getApiHost()); + ViewUtils.setTextWithSelectionIfFocused(binding.settingsCdnHostLabel, config.getCdnHost()); + } + + private void setUpActions() { + binding.topAppBar.setNavigationOnClickListener(view -> { + // For better user experience, navigate to launcher activity on navigate up button + if (isTaskRoot()) { + startActivity(new Intent(InternalSettingsActivity.this, DashboardActivity.class)); + } + onBackPressed(); + }); + binding.settingsDeviceTokenLayout.setEndIconOnClickListener(view -> { + String deviceToken = ViewUtils.getTextTrimmed(binding.settingsDeviceTokenLabel); + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.device_token), deviceToken); + clipboard.setPrimaryClip(clip); + Toast.makeText(this, R.string.token_copied, Toast.LENGTH_SHORT).show(); + }); + binding.settingsSaveButton.setOnClickListener(v -> saveSettings()); + binding.settingsRestoreDefaultsButton.setOnClickListener(v -> updateUiWithConfig(CustomerIOSDKConfig.getDefaultConfigurations())); + ViewUtils.clearErrorWhenTextedEntered(binding.settingsApiHostLabel, binding.settingsApiHostLayout); + ViewUtils.clearErrorWhenTextedEntered(binding.settingsCdnHostLabel, binding.settingsCdnHostLayout); + } + + private void saveSettings() { + CustomerIOSDKConfig currentSettings = settingsViewModel.getSDKConfigObservable().getValue(); + if (currentSettings == null) { + Toast.makeText(this, "Error! Cannot save settings!", Toast.LENGTH_SHORT).show(); + return; + } + + String apiHost = ViewUtils.getTextTrimmed(binding.settingsApiHostLabel); + String cdnHost = ViewUtils.getTextTrimmed(binding.settingsCdnHostLabel); + if (!validateUiInputs(apiHost, cdnHost)) { + return; + } + + CustomerIOSDKConfig newSettings = createNewSettings(currentSettings, apiHost, cdnHost); + settingsViewModel.updateConfigurations(newSettings); + + binding.progressIndicator.show(); + Disposable disposable = settingsViewModel + .updateConfigurations(newSettings) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(preferences -> { + binding.progressIndicator.hide(); + Toast.makeText(this, R.string.settings_save_msg, Toast.LENGTH_SHORT).show(); + OSUtils.restartApp(); + }); + disposables.add(disposable); + } + + private boolean validateUiInputs(@NonNull String apiHost, @NonNull String cdnHost) { + boolean valid = true; + if (isHostURLInvalid(apiHost)) { + valid = false; + ViewUtils.setError(binding.settingsApiHostLayout, getString(R.string.error_url_input_field)); + } + if (isHostURLInvalid(cdnHost)) { + valid = false; + ViewUtils.setError(binding.settingsCdnHostLayout, getString(R.string.error_url_input_field)); + } + return valid; + } + + @NonNull + private static CustomerIOSDKConfig createNewSettings(CustomerIOSDKConfig currentSettings, String apiHost, String cdnHost) { + return new CustomerIOSDKConfig( + currentSettings.getCdpApiKey(), + currentSettings.getSiteId(), + apiHost, + cdnHost, + currentSettings.isScreenTrackingEnabled(), + currentSettings.isDeviceAttributesTrackingEnabled(), + currentSettings.getLogLevel(), + currentSettings.getRegion(), + currentSettings.isApplicationLifecycleTrackingEnabled(), + currentSettings.isTestModeEnabled(), + currentSettings.isInAppMessagingEnabled() + ); + } + + private boolean isHostURLInvalid(String url) { + // Empty text is not considered valid + if (TextUtils.isEmpty(url)) { + return true; + } + + try { + Uri uri = Uri.parse(url); + // Since SDK does not support custom schemes, we manually append http:// to the URL + // So the URL is considered invalid if it ends with a slash, contains a scheme, query or fragment + return url.endsWith("/") + || !TextUtils.isEmpty(uri.getScheme()) + || !TextUtils.isEmpty(uri.getQuery()) + || !TextUtils.isEmpty(uri.getFragment()); + } catch (Exception ex) { + //noinspection CallToPrintStackTrace + ex.printStackTrace(); + return true; + } + } +} diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/SettingsActivity.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/SettingsActivity.java index df059b87f..aa237c585 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/SettingsActivity.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/ui/settings/SettingsActivity.java @@ -1,8 +1,5 @@ package io.customer.android.sample.java_layout.ui.settings; -import android.content.ClipData; -import android.content.ClipboardManager; -import android.content.Context; import android.content.Intent; import android.net.Uri; import android.text.TextUtils; @@ -19,9 +16,9 @@ import io.customer.android.sample.java_layout.ui.core.BaseActivity; import io.customer.android.sample.java_layout.ui.dashboard.DashboardActivity; import io.customer.android.sample.java_layout.utils.OSUtils; -import io.customer.android.sample.java_layout.utils.StringUtils; import io.customer.android.sample.java_layout.utils.ViewUtils; -import io.customer.sdk.CustomerIO; +import io.customer.sdk.core.util.CioLogLevel; +import io.customer.sdk.data.model.Region; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; @@ -77,11 +74,11 @@ private void parseLinkParams() { if (deepLinkUri != null) { String cdpApiKey = deepLinkUri.getQueryParameter("cdp_api_key"); if (cdpApiKey != null) { - ViewUtils.setTextWithSelectionIfFocused(binding.cdpApiKeyTextInput, cdpApiKey); + ViewUtils.setTextWithSelectionIfFocused(binding.settingsCdpApiKeyLabel, cdpApiKey); } String siteId = deepLinkUri.getQueryParameter("site_id"); if (siteId != null) { - ViewUtils.setTextWithSelectionIfFocused(binding.siteIdTextInput, siteId); + ViewUtils.setTextWithSelectionIfFocused(binding.settingsSiteIdKeyLabel, siteId); } } isLinkParamsPopulated = true; @@ -89,18 +86,10 @@ private void parseLinkParams() { private void prepareViewsForAutomatedTests() { ViewUtils.prepareForAutomatedTests(binding.topAppBar); - ViewUtils.prepareForAutomatedTests(binding.deviceTokenTextInput, R.string.acd_device_token_input); - ViewUtils.prepareForAutomatedTests(binding.apiHostTextInput, R.string.acd_api_host_input); - ViewUtils.prepareForAutomatedTests(binding.cdnHostTextInput, R.string.acd_cdn_host_input); - ViewUtils.prepareForAutomatedTests(binding.cdpApiKeyTextInput, R.string.acd_cdp_api_key_input); - ViewUtils.prepareForAutomatedTests(binding.siteIdTextInput, R.string.acd_site_id_input); - ViewUtils.prepareForAutomatedTests(binding.flushIntervalTextInput, R.string.acd_flush_interval_input); - ViewUtils.prepareForAutomatedTests(binding.flushAtTextInput, R.string.acd_flush_at_input); - ViewUtils.prepareForAutomatedTests(binding.trackScreensSwitch, R.string.acd_track_screens_switch); - ViewUtils.prepareForAutomatedTests(binding.trackDeviceAttributesSwitch, R.string.acd_track_device_attributes_switch); - ViewUtils.prepareForAutomatedTests(binding.debugModeSwitch, R.string.acd_debug_mode_switch); - ViewUtils.prepareForAutomatedTests(binding.saveButton, R.string.acd_save_settings_button); - ViewUtils.prepareForAutomatedTests(binding.restoreDefaultsButton, R.string.acd_restore_default_settings_button); + ViewUtils.prepareForAutomatedTests(binding.settingsCdpApiKeyLabel, R.string.acd_cdp_api_key_input); + ViewUtils.prepareForAutomatedTests(binding.settingsSiteIdKeyLabel, R.string.acd_site_id_input); + ViewUtils.prepareForAutomatedTests(binding.settingsSaveButton, R.string.acd_save_settings_button); + ViewUtils.prepareForAutomatedTests(binding.settingsRestoreDefaultsButton, R.string.acd_restore_default_settings_button); } private void setupViews() { @@ -111,24 +100,13 @@ private void setupViews() { } onBackPressed(); }); - binding.deviceTokenInputLayout.setEndIconOnClickListener(view -> { - String deviceToken = ViewUtils.getTextTrimmed(binding.deviceTokenTextInput); - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText(getString(R.string.device_token), deviceToken); - clipboard.setPrimaryClip(clip); - Toast.makeText(this, R.string.token_copied, Toast.LENGTH_SHORT).show(); - }); - binding.saveButton.setOnClickListener(view -> saveSettings()); - binding.restoreDefaultsButton.setOnClickListener(view -> { - updateIOWithConfig(CustomerIOSDKConfig.getDefaultConfigurations()); - saveSettings(); - }); + binding.settingsSaveButton.setOnClickListener(view -> saveSettings()); + binding.settingsRestoreDefaultsButton.setOnClickListener(view -> updateIOWithConfig(CustomerIOSDKConfig.getDefaultConfigurations())); + ViewUtils.clearErrorWhenTextedEntered(binding.settingsCdpApiKeyLabel, binding.settingsCdpApiKeyLayout); + ViewUtils.clearErrorWhenTextedEntered(binding.settingsSiteIdKeyLabel, binding.settingsSiteIdKeyLayout); } private void setupObservers() { - - binding.deviceTokenTextInput.setText(CustomerIO.instance().getRegisteredDeviceToken()); - settingsViewModel.getSDKConfigObservable().observe(this, config -> { binding.progressIndicator.hide(); updateIOWithConfig(config); @@ -136,27 +114,6 @@ private void setupObservers() { }); } - private boolean isHostURLInvalid(String url) { - // Empty text is not considered valid - if (TextUtils.isEmpty(url)) { - return true; - } - - try { - Uri uri = Uri.parse(url); - // Since SDK does not support custom schemes, we manually append http:// to the URL - // So the URL is considered invalid if it ends with a slash, contains a scheme, query or fragment - return url.endsWith("/") - || !TextUtils.isEmpty(uri.getScheme()) - || !TextUtils.isEmpty(uri.getQuery()) - || !TextUtils.isEmpty(uri.getFragment()); - } catch (Exception ex) { - //noinspection CallToPrintStackTrace - ex.printStackTrace(); - return true; - } - } - @SuppressWarnings("BooleanMethodIsAlwaysInverted") private > boolean isNumberValid(T number, T min) { // Compares if the value is not null and greater than or equal to min @@ -165,70 +122,35 @@ private > boolean isNumberValid(T number, T min } private void updateIOWithConfig(@NonNull CustomerIOSDKConfig config) { - ViewUtils.setTextWithSelectionIfFocused(binding.apiHostTextInput, config.getApiHost()); - ViewUtils.setTextWithSelectionIfFocused(binding.cdnHostTextInput, config.getCdnHost()); - ViewUtils.setTextWithSelectionIfFocused(binding.cdpApiKeyTextInput, config.getCdpApiKey()); - ViewUtils.setTextWithSelectionIfFocused(binding.siteIdTextInput, config.getSiteId()); - ViewUtils.setTextWithSelectionIfFocused(binding.flushIntervalTextInput, StringUtils.fromInteger(config.getFlushInterval())); - ViewUtils.setTextWithSelectionIfFocused(binding.flushAtTextInput, StringUtils.fromInteger(config.getFlushAt())); - binding.trackScreensSwitch.setChecked(config.screenTrackingEnabled()); - binding.trackDeviceAttributesSwitch.setChecked(config.deviceAttributesTrackingEnabled()); - binding.debugModeSwitch.setChecked(config.debugModeEnabled()); + ViewUtils.setTextWithSelectionIfFocused(binding.settingsCdpApiKeyLabel, config.getCdpApiKey()); + ViewUtils.setTextWithSelectionIfFocused(binding.settingsSiteIdKeyLabel, config.getSiteId()); + binding.settingsRegionValuesGroup.check(getCheckedRegionButtonId(config.getRegion())); + binding.settingsTrackDeviceAttrsValuesGroup.check(getCheckedAutoTrackDeviceAttributesButtonId(config.isDeviceAttributesTrackingEnabled())); + binding.settingsTrackScreenViewsValuesGroup.check(getCheckedTrackScreenViewsButtonId(config.isScreenTrackingEnabled())); + binding.settingsTrackAppLifecycleValuesGroup.check(getCheckedTrackAppLifecycleButtonId(config.isApplicationLifecycleTrackingEnabled())); + binding.settingsLogLevelValuesGroup.check(getCheckedLogLevelButtonId(config.getLogLevel())); + binding.settingsTestModeValuesGroup.check(getCheckedTestModeButtonId(config.isTestModeEnabled())); + binding.settingsInAppMessagingValuesGroup.check(getCheckedInAppMessagingButtonId(config.isInAppMessagingEnabled())); } private void saveSettings() { - boolean isFormValid; - - String apiHost = ViewUtils.getTextTrimmed(binding.apiHostTextInput); - isFormValid = updateErrorState(binding.apiHostInputLayout, isHostURLInvalid(apiHost), R.string.error_host_url); - - String cdnHost = ViewUtils.getTextTrimmed(binding.cdnHostTextInput); - isFormValid = updateErrorState(binding.cdnHostInputLayout, isHostURLInvalid(cdnHost), R.string.error_host_url) && isFormValid; - - String cdpApiKey = ViewUtils.getTextTrimmed(binding.cdpApiKeyTextInput); - isFormValid = updateErrorState(binding.cdpApiKeyInputLayout, TextUtils.isEmpty(cdpApiKey), R.string.error_text_input_field_blank) && isFormValid; + CustomerIOSDKConfig currentSettings = settingsViewModel.getSDKConfigObservable().getValue(); + if (currentSettings == null) { + Toast.makeText(this, "Error! Cannot save settings!", Toast.LENGTH_SHORT).show(); + return; + } - String siteId = ViewUtils.getTextTrimmed(binding.siteIdTextInput); - isFormValid = updateErrorState(binding.siteIdInputLayout, TextUtils.isEmpty(siteId), R.string.error_text_input_field_blank) && isFormValid; + boolean isFormValid; - String flushIntervalText = ViewUtils.getTextTrimmed(binding.flushIntervalTextInput); - Integer flushInterval = StringUtils.parseInteger(flushIntervalText, null); - boolean isFlushIntervalTextEmpty = TextUtils.isEmpty(flushIntervalText); - if (isFlushIntervalTextEmpty) { - isFormValid = updateErrorState(binding.flushIntervalInputLayout, true, R.string.error_text_input_field_blank) && isFormValid; - } else { - int minDelay = 1; - isFormValid = updateErrorState(binding.flushIntervalInputLayout, - !isNumberValid(flushInterval, minDelay), - getString(R.string.error_number_input_field_small, String.valueOf(minDelay))) && isFormValid; - } + String cdpApiKey = ViewUtils.getTextTrimmed(binding.settingsCdpApiKeyLabel); + isFormValid = updateErrorState(binding.settingsCdpApiKeyLayout, TextUtils.isEmpty(cdpApiKey), R.string.error_text_input_field_blank); - String flushAtText = ViewUtils.getTextTrimmed(binding.flushAtTextInput); - Integer flushAt = StringUtils.parseInteger(flushAtText, null); - boolean isFlushAtTextEmpty = TextUtils.isEmpty(flushAtText); - if (isFlushAtTextEmpty) { - isFormValid = updateErrorState(binding.flushAtInputLayout, true, R.string.error_text_input_field_blank) && isFormValid; - } else { - int minTasks = 1; - isFormValid = updateErrorState(binding.flushAtInputLayout, - !isNumberValid(flushAt, minTasks), - getString(R.string.error_number_input_field_small, String.valueOf(minTasks))) && isFormValid; - } + String siteId = ViewUtils.getTextTrimmed(binding.settingsSiteIdKeyLabel); + isFormValid = updateErrorState(binding.settingsSiteIdKeyLayout, TextUtils.isEmpty(siteId), R.string.error_text_input_field_blank) && isFormValid; if (isFormValid) { binding.progressIndicator.show(); - boolean featTrackScreens = binding.trackScreensSwitch.isChecked(); - boolean featTrackDeviceAttributes = binding.trackDeviceAttributesSwitch.isChecked(); - boolean featDebugMode = binding.debugModeSwitch.isChecked(); - CustomerIOSDKConfig config = new CustomerIOSDKConfig(cdpApiKey, - siteId, - apiHost, - cdnHost, - flushInterval, - flushAt, - featTrackScreens, - featTrackDeviceAttributes, - featDebugMode); + CustomerIOSDKConfig config = createNewSettings(cdpApiKey, siteId, currentSettings); Disposable disposable = settingsViewModel .updateConfigurations(config) .subscribeOn(Schedulers.io()) @@ -242,18 +164,102 @@ private void saveSettings() { } } - private boolean updateErrorState(TextInputLayout textInputLayout, - boolean isErrorEnabled, - @StringRes int errorResId) { - String error = isErrorEnabled ? getString(errorResId) : null; - ViewUtils.setError(textInputLayout, error); - return !isErrorEnabled; + @NonNull + private CustomerIOSDKConfig createNewSettings(String cdpApiKey, String siteId, CustomerIOSDKConfig currentSettings) { + boolean featTrackScreens = binding.settingsTrackScreenViewsValuesGroup.getCheckedButtonId() == R.id.settings_track_screen_views_yes_button; + boolean featTrackDeviceAttributes = binding.settingsTrackDeviceAttrsValuesGroup.getCheckedButtonId() == R.id.settings_track_device_attrs_yes_button; + boolean featTrackApplicationLifecycle = binding.settingsTrackAppLifecycleValuesGroup.getCheckedButtonId() == R.id.settings_track_app_lifecycle_yes_button; + boolean featTestModeEnabled = binding.settingsTestModeValuesGroup.getCheckedButtonId() == R.id.settings_test_mode_yes_button; + boolean featInAppMessagingEnabled = binding.settingsInAppMessagingValuesGroup.getCheckedButtonId() == R.id.settings_in_app_messaging_yes_button; + CioLogLevel logLevel = getSelectedLogLevel(); + Region region = getSelectedRegion(); + + return new CustomerIOSDKConfig(cdpApiKey, + siteId, + currentSettings.getApiHost(), + currentSettings.getCdnHost(), + featTrackScreens, + featTrackDeviceAttributes, + logLevel, + region, + featTrackApplicationLifecycle, + featTestModeEnabled, + featInAppMessagingEnabled); + } + + @NonNull + private CioLogLevel getSelectedLogLevel() { + int checkedButton = binding.settingsLogLevelValuesGroup.getCheckedButtonId(); + if (checkedButton == R.id.settings_log_level_none_button) { + return CioLogLevel.NONE; + } else if (checkedButton == R.id.settings_log_level_error_button) { + return CioLogLevel.ERROR; + } else if (checkedButton == R.id.settings_log_level_info_button) { + return CioLogLevel.INFO; + } else if (checkedButton == R.id.settings_log_level_debug_button) { + return CioLogLevel.DEBUG; + } + throw new IllegalStateException(); + } + + @NonNull + private Region getSelectedRegion() { + int checkedButton = binding.settingsRegionValuesGroup.getCheckedButtonId(); + if (checkedButton == R.id.settings_region_us_button) { + return Region.US.INSTANCE; + } else if (checkedButton == R.id.settings_region_eu_button) { + return Region.EU.INSTANCE; + } + throw new IllegalStateException(); + } + + private int getCheckedInAppMessagingButtonId(boolean enabled) { + return enabled ? R.id.settings_in_app_messaging_yes_button : R.id.settings_in_app_messaging_no_button; + } + + private int getCheckedTestModeButtonId(boolean enabled) { + return enabled ? R.id.settings_test_mode_yes_button : R.id.settings_test_mode_no_button; + } + + private int getCheckedLogLevelButtonId(@NonNull CioLogLevel logLevel) { + switch (logLevel) { + case NONE: + return R.id.settings_log_level_none_button; + case ERROR: + return R.id.settings_log_level_error_button; + case INFO: + return R.id.settings_log_level_info_button; + case DEBUG: + return R.id.settings_log_level_debug_button; + default: + throw new IllegalStateException(); + } + } + + private int getCheckedTrackAppLifecycleButtonId(boolean enabled) { + return enabled ? R.id.settings_track_app_lifecycle_yes_button + : R.id.settings_track_app_lifecycle_no_button; + } + + private int getCheckedTrackScreenViewsButtonId(boolean enabled) { + return enabled ? R.id.settings_track_screen_views_yes_button + : R.id.settings_track_screen_views_no_button; + } + + private int getCheckedAutoTrackDeviceAttributesButtonId(boolean enabled) { + return enabled ? R.id.settings_track_device_attrs_yes_button + : R.id.settings_track_device_attrs_no_button; + } + + private int getCheckedRegionButtonId(@NonNull Region region) { + return region instanceof Region.US ? R.id.settings_region_us_button + : R.id.settings_region_eu_button; } private boolean updateErrorState(TextInputLayout textInputLayout, boolean isErrorEnabled, - String errorMessage) { - String error = isErrorEnabled ? errorMessage : null; + @StringRes int errorResId) { + String error = isErrorEnabled ? getString(errorResId) : null; ViewUtils.setError(textInputLayout, error); return !isErrorEnabled; } diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/DefaultTextWatcher.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/DefaultTextWatcher.java new file mode 100644 index 000000000..885aed239 --- /dev/null +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/DefaultTextWatcher.java @@ -0,0 +1,25 @@ +package io.customer.android.sample.java_layout.utils; + +import android.text.Editable; +import android.text.TextWatcher; + +/** + * Convenience class to prevent classes from having to override all methods and only override the + * ones desired. + */ +public class DefaultTextWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + /* NO OP */ + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + /* NO OP */ + } + + @Override + public void afterTextChanged(Editable s) { + /* NO OP */ + } +} diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/StringUtils.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/StringUtils.java index bade5f226..966b83dd5 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/StringUtils.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/StringUtils.java @@ -43,7 +43,7 @@ public static String fromInteger(@Nullable Integer value) { return value == null ? null : value.toString(); } - public static Boolean parseBoolean(@Nullable String value, Boolean defaultValue) { + public static Boolean parseBoolean(@Nullable String value, boolean defaultValue) { if (TextUtils.isEmpty(value)) { return defaultValue; } diff --git a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/ViewUtils.java b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/ViewUtils.java index ac8243efe..b5363d5d1 100644 --- a/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/ViewUtils.java +++ b/samples/java_layout/src/main/java/io/customer/android/sample/java_layout/utils/ViewUtils.java @@ -12,6 +12,7 @@ import androidx.appcompat.widget.Toolbar; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; import java.util.Locale; @@ -70,4 +71,15 @@ public static MaterialAlertDialogBuilder createAlertDialog(@NonNull Activity act .setCancelable(true) .setPositiveButton(android.R.string.ok, null); } + + public static void clearErrorWhenTextedEntered(@NonNull TextInputEditText editText, + @NonNull TextInputLayout textInputLayout) { + editText.addTextChangedListener(new DefaultTextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + textInputLayout.setError(null); + textInputLayout.setErrorEnabled(false); + } + }); + } } diff --git a/samples/java_layout/src/main/res/layout/activity_internal_settings.xml b/samples/java_layout/src/main/res/layout/activity_internal_settings.xml new file mode 100644 index 000000000..fe2ecd036 --- /dev/null +++ b/samples/java_layout/src/main/res/layout/activity_internal_settings.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +