maoni/src/main/java/org/rm3l/maoni/Maoni.java

Summary

Maintainability
D
3 days
Test Coverage
/*
 * Copyright (c) 2016-2022 Armel Soro
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.rm3l.maoni;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import android.view.Window;

import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.rm3l.maoni.common.contract.Handler;
import org.rm3l.maoni.common.contract.Listener;
import org.rm3l.maoni.common.contract.UiListener;
import org.rm3l.maoni.common.contract.Validator;
import org.rm3l.maoni.email.MaoniEmailListener;
import org.rm3l.maoni.ui.MaoniActivity;
import org.rm3l.maoni.utils.ContextUtils;
import org.rm3l.maoni.utils.ViewUtils;

import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.rm3l.maoni.Maoni.CallbacksConfiguration.getInstance;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_BUILD_CONFIG_BUILD_TYPE;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_BUILD_CONFIG_DEBUG;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_BUILD_CONFIG_FLAVOR;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_PACKAGE_NAME;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_VERSION_CODE;
import static org.rm3l.maoni.ui.MaoniActivity.APPLICATION_INFO_VERSION_NAME;
import static org.rm3l.maoni.ui.MaoniActivity.CALLER_ACTIVITY;
import static org.rm3l.maoni.ui.MaoniActivity.CONTENT_ERROR_TEXT;
import static org.rm3l.maoni.ui.MaoniActivity.CONTENT_HINT;
import static org.rm3l.maoni.ui.MaoniActivity.EXTRA_LAYOUT;
import static org.rm3l.maoni.ui.MaoniActivity.FILE_PROVIDER_AUTHORITY;
import static org.rm3l.maoni.ui.MaoniActivity.HEADER;
import static org.rm3l.maoni.ui.MaoniActivity.INCLUDE_LOGS_TEXT;
import static org.rm3l.maoni.ui.MaoniActivity.INCLUDE_SCREENSHOT_TEXT;
import static org.rm3l.maoni.ui.MaoniActivity.LOGS_CAPTURING_FEATURE_ENABLED;
import static org.rm3l.maoni.ui.MaoniActivity.MESSAGE;
import static org.rm3l.maoni.ui.MaoniActivity.SCREENSHOT_FILE;
import static org.rm3l.maoni.ui.MaoniActivity.SCREENSHOT_HINT;
import static org.rm3l.maoni.ui.MaoniActivity.SCREENSHOT_TOUCH_TO_PREVIEW_HINT;
import static org.rm3l.maoni.ui.MaoniActivity.SCREEN_CAPTURING_FEATURE_ENABLED;
import static org.rm3l.maoni.ui.MaoniActivity.SHARED_PREFERENCES;
import static org.rm3l.maoni.ui.MaoniActivity.SHOW_KEYBOARD_ON_START;
import static org.rm3l.maoni.ui.MaoniActivity.THEME;
import static org.rm3l.maoni.ui.MaoniActivity.TOOLBAR_SUBTITLE_TEXT_COLOR;
import static org.rm3l.maoni.ui.MaoniActivity.TOOLBAR_TITLE_TEXT_COLOR;
import static org.rm3l.maoni.ui.MaoniActivity.WINDOW_SUBTITLE;
import static org.rm3l.maoni.ui.MaoniActivity.WINDOW_TITLE;
import static org.rm3l.maoni.ui.MaoniActivity.WORKING_DIR;

/**
 * Maoni configuration
 */
@SuppressWarnings("WeakerAccess")
public class Maoni {

    private static final String LOG_TAG = Maoni.class.getSimpleName();

    private static final String MAONI_FEEDBACK_SCREENSHOT_FILENAME = "maoni_feedback_screenshot.png";

    private static final String DEBUG = "DEBUG";
    private static final String FLAVOR = "FLAVOR";
    private static final String BUILD_TYPE = "BUILD_TYPE";
    public static final String DEFAULT_LISTENER_EMAIL_TO = "DEFAULT_LISTENER_EMAIL_TO";

    /**
     * The feedback window title
     */
    @Nullable
    public final CharSequence windowTitle;
    /**
     * The feedback window sub-title
     */
    @Nullable
    public final CharSequence windowSubTitle;
    /**
     * The feedback window title color
     */
    @ColorRes
    @Nullable
    public final Integer windowTitleTextColor;
    /**
     * The feedback window sub-title color
     */
    @ColorRes
    @Nullable
    public final Integer windowSubTitleTextColor;
    /**
     * The message to display to the user
     */
    @Nullable
    public final CharSequence message;
    /**
     * The feedback form field error message to display to the user
     */
    @Nullable
    public final CharSequence contentErrorMessage;
    /**
     * The feedback form field hint message
     */
    @Nullable
    public final CharSequence feedbackContentHint;
    /**
     * Some text to display to the user, such as how the screenshot will be used by you,
     * and any links to your privacy policy
     */
    @Nullable
    public final CharSequence screenshotHint;
    /**
     * Header image
     */
    @DrawableRes
    @Nullable
    public final Integer header;
    /**
     * Text do display next to the "Include logs" checkbox
     */
    @Nullable
    public final CharSequence includeLogsText;
    /**
     * Text do display next to the "Include screenshot" checkbox
     */
    @Nullable
    public final CharSequence includeScreenshotText;
    /**
     * The "Touch to preview" text (displayed below the screenshot thumbnail).
     * Keep it short and to the point
     */
    @Nullable
    public final CharSequence touchToPreviewScreenshotText;
    /**
     * Extra layout resource.
     * Will be displayed between the feedback content field and the "Include screenshot" checkbox.
     */
    @LayoutRes
    @Nullable
    public final Integer extraLayout;
    @StyleRes
    @Nullable
    public final Integer theme;
    private final String fileProviderAuthority;
    @Nullable
    private final Context context;
    private File maoniWorkingDir;

    private final HashMap<String, Object> sharedPreferencesContentMap;

    private boolean showKeyboardOnStart;
    private boolean screenCapturingFeatureEnabled = true;
    private boolean logsCapturingFeatureEnabled = true;

    private final AtomicBoolean mUsed = new AtomicBoolean(false);

    /**
     * Default constructor: non-instantiable
     */
    @SuppressWarnings("unused")
    private Maoni() {
        throw new UnsupportedOperationException("Non instantiable this way. Use Maoni.Builder builder class instead.");
    }

    /**
     * Constructor
     * @param builder the builder instance to use for instantiated a Maoni instance
     */
    private Maoni(final Builder builder) {
        this.context = builder.context;
        this.sharedPreferencesContentMap = builder.sharedPreferences;
        this.fileProviderAuthority = builder.fileProviderAuthority;
        this.windowSubTitle = builder.windowSubTitle;
        this.windowTitleTextColor = builder.windowTitleTextColor;
        this.windowSubTitleTextColor = builder.windowSubTitleTextColor;
        this.theme = builder.theme;
        this.windowTitle = builder.windowTitle;
        this.message = builder.message;
        this.contentErrorMessage = builder.contentErrorMessage;
        this.feedbackContentHint = builder.feedbackContentHint;
        this.screenshotHint = builder.screenshotHint;
        this.header = builder.header;
        this.includeLogsText = builder.includeLogsText;
        this.includeScreenshotText = builder.includeScreenshotText;
        this.touchToPreviewScreenshotText = builder.touchToPreviewScreenshotText;
        this.extraLayout = builder.extraLayout;
        this.maoniWorkingDir = builder.maoniWorkingDir;
        this.showKeyboardOnStart = builder.showKeyboardOnStart;
        this.screenCapturingFeatureEnabled = builder.screenCapturingFeatureEnabled;
        this.logsCapturingFeatureEnabled = builder.logsCapturingFeatureEnabled;
    }

    /**
     * Start the Maoni Activity. If screenCapturingFeatureEnabled is true this will take a
     * screenshot of the activity window
     *
     * @param callerActivity the caller activity
     */
    public void start(@Nullable final Activity callerActivity) {
        start(callerActivity, callerActivity.getWindow());
    }

    /**
     * Start the Maoni Activity. If screenCapturingFeatureEnabled is true this will take a
     * screenshot of the dialog window
     *
     * @param dialog the caller dialog
     */
    public void start(@Nullable Dialog dialog) {
        Activity activity = ContextUtils.scanForActivity(dialog.getContext());
        start(activity, dialog.getWindow());
    }

    /**
     * Internal helper method used to start MaoniActivity
     *
     * @param callerActivity activity context
     * @param window window for screen (if enabled)
     */
    private void start(Activity callerActivity, Window window) {
        if (mUsed.getAndSet(true)) {
            this.clear();
            throw new UnsupportedOperationException(
                    "Maoni instance cannot be reused to start a new activity. " +
                            "Please build a new Maoni instance.");
        }

        if (callerActivity == null) {
            Log.d(LOG_TAG, "Target activity is undefined");
            return;
        }

        final Intent maoniIntent = new Intent(callerActivity, MaoniActivity.class);

        //Set app-related info
        final PackageManager packageManager = callerActivity.getPackageManager();
        try {
            if (packageManager != null) {
                final PackageInfo packageInfo = packageManager
                        .getPackageInfo(callerActivity.getPackageName(), 0);
                if (packageInfo != null) {
                    maoniIntent.putExtra(APPLICATION_INFO_VERSION_CODE, packageInfo.versionCode);
                    maoniIntent.putExtra(APPLICATION_INFO_VERSION_NAME, packageInfo.versionName);
                    maoniIntent.putExtra(APPLICATION_INFO_PACKAGE_NAME, packageInfo.packageName);
                }
            }
        } catch (final PackageManager.NameNotFoundException nnfe) {
            //No worries
            nnfe.printStackTrace();
        }
        final Object buildConfigDebugValue = ContextUtils.getBuildConfigValue(callerActivity,
                DEBUG);
        if (buildConfigDebugValue instanceof Boolean) {
            maoniIntent.putExtra(APPLICATION_INFO_BUILD_CONFIG_DEBUG,
                    (Boolean) buildConfigDebugValue);
        }
        final Object buildConfigFlavorValue = ContextUtils.getBuildConfigValue(callerActivity,
                FLAVOR);
        if (buildConfigFlavorValue != null) {
            maoniIntent.putExtra(APPLICATION_INFO_BUILD_CONFIG_FLAVOR,
                    buildConfigFlavorValue.toString());
        }
        final Object buildConfigBuildTypeValue = ContextUtils.getBuildConfigValue(callerActivity,
                BUILD_TYPE);
        if (buildConfigBuildTypeValue != null) {
            maoniIntent.putExtra(APPLICATION_INFO_BUILD_CONFIG_BUILD_TYPE,
                    buildConfigBuildTypeValue.toString());
        }

        maoniIntent.putExtra(FILE_PROVIDER_AUTHORITY, fileProviderAuthority);

        maoniIntent.putExtra(SHOW_KEYBOARD_ON_START, showKeyboardOnStart);

        maoniIntent.putExtra(WORKING_DIR,
                maoniWorkingDir != null ?
                        maoniWorkingDir.getAbsolutePath() : callerActivity.getCacheDir().getAbsolutePath());

        maoniIntent.putExtra(SCREEN_CAPTURING_FEATURE_ENABLED, screenCapturingFeatureEnabled);
        if (this.screenCapturingFeatureEnabled) {
            //Create screenshot file
            final File screenshotFile = new File(maoniWorkingDir != null ? maoniWorkingDir : callerActivity.getCacheDir(),
                MAONI_FEEDBACK_SCREENSHOT_FILENAME);
            ViewUtils.exportViewToFile(callerActivity, window.getDecorView(),
                screenshotFile);
            maoniIntent.putExtra(SCREENSHOT_FILE, screenshotFile.getAbsolutePath());

            if (screenshotHint != null) {
                maoniIntent.putExtra(SCREENSHOT_HINT, screenshotHint);
            }

            if (includeScreenshotText != null) {
                maoniIntent.putExtra(INCLUDE_SCREENSHOT_TEXT, includeScreenshotText);
            }

            if (touchToPreviewScreenshotText != null) {
                maoniIntent.putExtra(SCREENSHOT_TOUCH_TO_PREVIEW_HINT, touchToPreviewScreenshotText);
            }
        }

        maoniIntent.putExtra(CALLER_ACTIVITY, callerActivity.getClass().getCanonicalName());

        if (theme != null) {
            maoniIntent.putExtra(THEME, theme);
        }

        if (windowTitle != null) {
            maoniIntent.putExtra(WINDOW_TITLE, windowTitle);
        }

        if (windowSubTitle != null) {
            maoniIntent.putExtra(WINDOW_SUBTITLE, windowSubTitle);
        }

        if (windowTitleTextColor != null) {
            maoniIntent.putExtra(TOOLBAR_TITLE_TEXT_COLOR, windowTitleTextColor);
        }

        if (windowSubTitleTextColor != null) {
            maoniIntent.putExtra(TOOLBAR_SUBTITLE_TEXT_COLOR, windowSubTitleTextColor);
        }

        if (message != null) {
            maoniIntent.putExtra(MESSAGE, message);
        }

        if (header != null) {
            maoniIntent.putExtra(HEADER, header);
        }

        if (extraLayout != null) {
            maoniIntent.putExtra(EXTRA_LAYOUT, extraLayout);
        }

        if (feedbackContentHint != null) {
            maoniIntent.putExtra(CONTENT_HINT, feedbackContentHint);
        }

        if (contentErrorMessage != null) {
            maoniIntent.putExtra(CONTENT_ERROR_TEXT, contentErrorMessage);
        }

        maoniIntent.putExtra(LOGS_CAPTURING_FEATURE_ENABLED, logsCapturingFeatureEnabled);
        if (logsCapturingFeatureEnabled) {
            if (includeLogsText != null) {
                maoniIntent.putExtra(INCLUDE_LOGS_TEXT, includeLogsText);
            }
        }

        maoniIntent.putExtra(SHARED_PREFERENCES, sharedPreferencesContentMap);

        callerActivity.startActivity(maoniIntent);
    }

    public Maoni unregisterListener() {
        getInstance(context).setListener(null);
        return this;
    }

    public Maoni unregisterUiListener() {
        getInstance(context).setUiListener(null);
        return this;
    }

    public Maoni unregisterValidator() {
        getInstance(context).setValidator(null);
        return this;
    }

    public Maoni unregisterHandler() {
        return this.unregisterListener()
                .unregisterUiListener()
                .unregisterValidator();
    }

    public Maoni clear() {
        return this.unregisterHandler();
    }

    /**
     * Maoni Builder
     */
    public static class Builder {

        @Nullable
        private final Context context;

        @Nullable
        private final String fileProviderAuthority;
        @StyleRes
        @Nullable
        public Integer theme;
        @Nullable
        private File maoniWorkingDir;
        @Nullable
        private CharSequence windowTitle;
        @Nullable
        private CharSequence windowSubTitle;
        @ColorRes
        @Nullable
        private Integer windowTitleTextColor;
        @ColorRes
        @Nullable
        private Integer windowSubTitleTextColor;
        @Nullable
        private CharSequence message;
        @Nullable
        private CharSequence contentErrorMessage;
        @Nullable
        private CharSequence feedbackContentHint;
        @Nullable
        private CharSequence screenshotHint;
        @DrawableRes
        @Nullable
        private Integer header;
        @Nullable
        private CharSequence includeScreenshotText;
        @Nullable
        private CharSequence includeLogsText;
        @Nullable
        private CharSequence touchToPreviewScreenshotText;
        @LayoutRes
        @Nullable
        private Integer extraLayout;

        private boolean showKeyboardOnStart;
        private boolean screenCapturingFeatureEnabled = true;
        private boolean logsCapturingFeatureEnabled = true;

        @NonNull
        private HashMap<String, Object> sharedPreferences = new HashMap<>();

        /**
         * Constructor
         *
         * @param fileProviderAuthority the file provider authority.
         *                              If {@literal null}, screenshot file sharing will not be available
         */
        public Builder(@Nullable final String fileProviderAuthority) {
            this(null, fileProviderAuthority);
        }

        /**
         * Constructor
         *
         * @param context the context
         * @param fileProviderAuthority the file provider authority.
         *                              If {@literal null}, screenshot file sharing will not be available
         */
        public Builder(@Nullable final Context context, @Nullable final String fileProviderAuthority) {
            this.fileProviderAuthority = fileProviderAuthority;
            this.context = context;
        }

        public Builder withMaoniWorkingDir(@Nullable File maoniWorkingDir) {
            this.maoniWorkingDir = maoniWorkingDir;
            return this;
        }

        public Builder withTheme(@StyleRes @Nullable Integer theme) {
            this.theme = theme;
            return this;
        }

        public Builder withWindowTitle(@Nullable CharSequence windowTitle) {
            this.windowTitle = windowTitle;
            return this;
        }

        public Builder withWindowSubTitle(@Nullable CharSequence windowSubTitle) {
            this.windowSubTitle = windowSubTitle;
            return this;
        }

        public Builder withWindowTitleTextColor(@ColorRes @Nullable Integer windowTitleTextColor) {
            this.windowTitleTextColor = windowTitleTextColor;
            return this;
        }

        public Builder withWindowSubTitleTextColor(@ColorRes @Nullable Integer windowSubTitleTextColor) {
            this.windowSubTitleTextColor = windowSubTitleTextColor;
            return this;
        }

        public Builder withExtraLayout(@LayoutRes @Nullable Integer extraLayout) {
            this.extraLayout = extraLayout;
            return this;
        }

        public Builder withFeedbackContentHint(@Nullable CharSequence feedbackContentHint) {
            this.feedbackContentHint = feedbackContentHint;
            return this;
        }

        public Builder withIncludeLogsText(@Nullable CharSequence includeLogsText) {
            this.includeLogsText = includeLogsText;
            return this;
        }

        public Builder withIncludeScreenshotText(@Nullable CharSequence includeScreenshotText) {
            this.includeScreenshotText = includeScreenshotText;
            return this;
        }

        public CharSequence getTouchToPreviewScreenshotText() {
            return touchToPreviewScreenshotText;
        }

        public Builder withTouchToPreviewScreenshotText(@Nullable CharSequence touchToPreviewScreenshotText) {
            this.touchToPreviewScreenshotText = touchToPreviewScreenshotText;
            return this;
        }

        public Builder withMessage(@Nullable CharSequence message) {
            this.message = message;
            return this;
        }

        public Builder withContentErrorMessage(@Nullable CharSequence contentErrorMessage) {
            this.contentErrorMessage = contentErrorMessage;
            return this;
        }

        public Builder withHeader(@Nullable Integer header) {
            this.header = header;
            return this;
        }

        public Builder showKeyboardOnStart(final boolean showKeyboardOnStart) {
            this.showKeyboardOnStart = showKeyboardOnStart;
            return this;
        }

        public Builder showKeyboardOnStart() {
            return this.showKeyboardOnStart(true);
        }

        public Builder hideKeyboardOnStart() {
            return this.showKeyboardOnStart(false);
        }

        public Builder disableScreenCapturingFeature() {
            this.screenCapturingFeatureEnabled = false;
            return this;
        }

        public Builder enableScreenCapturingFeature() {
            this.screenCapturingFeatureEnabled = true;
            return this;
        }

        public Builder withScreenCapturingFeature(final boolean screenCapturingFeature) {
            if (screenCapturingFeature) {
                this.enableScreenCapturingFeature();
            } else {
                this.disableScreenCapturingFeature();
            }
            return this;
        }

        public Builder disableLogsCapturingFeature() {
            this.logsCapturingFeatureEnabled = false;
            return this;
        }

        public Builder enableLogsCapturingFeature() {
            this.logsCapturingFeatureEnabled = true;
            return this;
        }

        public Builder withLogsCapturingFeature(final boolean logsCapturingFeature) {
            if (logsCapturingFeature) {
                this.enableLogsCapturingFeature();
            } else {
                this.disableLogsCapturingFeature();
            }
            return this;
        }

        public Builder disableCapturingFeature() {
            this.disableLogsCapturingFeature();
            this.disableScreenCapturingFeature();
            return this;
        }

        public Builder enableCapturingFeature() {
            this.enableLogsCapturingFeature();
            this.enableScreenCapturingFeature();
            return this;
        }

        public Builder withCapturingFeature(final boolean capturingFeature) {
            if (capturingFeature) {
                this.enableCapturingFeature();
            } else {
                this.disableCapturingFeature();
            }
            return this;
        }

        public Builder withScreenshotHint(@Nullable CharSequence screenshotHint) {
            this.screenshotHint = screenshotHint;
            return this;
        }

        public Builder withValidator(@Nullable final Validator validator) {
            getInstance(context).setValidator(validator);
            return this;
        }

        public Builder withListener(@Nullable final Listener listener) {
            getInstance(context).setListener(listener);
            return this;
        }

        public Builder withUiListener(@Nullable final UiListener uiListener) {
            getInstance(context).setUiListener(uiListener);
            return this;
        }

        public Builder withHandler(@Nullable final Handler handler) {
            return this
                    .withListener(handler)
                    .withValidator(handler)
                    .withUiListener(handler);
        }

        /**
         * If no listener is set (i.e no call to {@link #withListener(Listener)}),
         * {@link MaoniEmailListener} is used by default (provided {@link #context} is non-null).
         * <p>
         * This allows to configure the 'to' email addresses to use for the default listener.
         * If {@link #withListener(Listener)} is called explicitly, then this
         * method will have no effect at all.
         * @param toAddresses the 'to' addresses
         * @return this Builder
         */
        @SuppressLint("ApplySharedPref")
        public Builder withDefaultToEmailAddress(@Nullable final String... toAddresses) {
            if (context != null && toAddresses != null) {
                context.getSharedPreferences(
                    Maoni.class.getPackage().getName(),
                    Context.MODE_PRIVATE)
                    .edit()
                    .putStringSet(DEFAULT_LISTENER_EMAIL_TO,
                        new HashSet<>(Arrays.asList(toAddresses)))
                    .commit();
            }
            return this;
        }

        /**
         * Include SharedPreferences value map.
         * <p>
         * SharedPreferences files are opened with the default operating mode.
         * @param sharedPreferences the names of each {@link android.content.SharedPreferences} file
         * @return this Builder
         */
        public Builder withSharedPreferences(@Nullable final String... sharedPreferences) {
            return this.withSharedPreferences(Context.MODE_PRIVATE, sharedPreferences);
        }

        /**
         * Include SharedPreferences value map.
         * @param mode Operating mode.  Use 0 or {@link Context#MODE_PRIVATE} for the
         * default operation.
         * @param sharedPreferences the names of each {@link android.content.SharedPreferences} file
         * @return this Builder
         */
        public Builder withSharedPreferences(final int mode, @Nullable final String... sharedPreferences) {
            if (sharedPreferences == null) {
                return this;
            }
            final Map<String, Integer> sharedPreferencesModeMap = new HashMap<>();
            for (final String sharedPreference : sharedPreferences) {
                if (sharedPreference == null) {
                    continue;
                }
                sharedPreferencesModeMap.put(sharedPreference, mode);
            }
            return this.withSharedPreferences(sharedPreferencesModeMap);
        }

        /**
         * Include SharedPreferences value map.
         * @param sharedPreferencesModeMap the map of {@link android.content.SharedPreferences} and their operating mode
         * @return this builder
         */
        public Builder withSharedPreferences(@Nullable final Map<String, Integer> sharedPreferencesModeMap) {
            if (sharedPreferencesModeMap == null || sharedPreferencesModeMap.isEmpty()) {
                return this;
            }
            if (this.context == null) {
                throw new IllegalArgumentException("A context is needed to load the shared preferences");
            }
            for (final Entry<String, Integer> entry : sharedPreferencesModeMap.entrySet()) {
                final String sharedPreference = entry.getKey();
                final Integer sharedPreferenceMode = entry.getValue();
                if (sharedPreference == null || sharedPreferenceMode == null) {
                    continue;
                }
                final Map<String, ?> sharedPreferencesContent = 
                    this.context.getSharedPreferences(sharedPreference, sharedPreferenceMode).getAll();
                for (final Entry<String, ?> sharedPreferencesContentEntry : sharedPreferencesContent.entrySet()) {
                    this.sharedPreferences.put(
                        "SharedPreferences/" + sharedPreference + "/" + sharedPreferencesContentEntry.getKey(),
                        sharedPreferencesContentEntry.getValue());
                }
            }
            return this;
        }

        public Maoni build() {
            return new Maoni(this);
        }
    }

    /**
     * Callbacks Configuration for Maoni
     */
    public static class CallbacksConfiguration {

        @Nullable
        private static CallbacksConfiguration SINGLETON = null;

        @Nullable
        private Validator validator;

        @Nullable
        private Listener listener;

        @Nullable
        private UiListener uiListener;

        private CallbacksConfiguration(@Nullable final Context context) {
            //Default listener comes from maoni-email
            if (context != null) {
                final Set<String> defaultToAddresses =
                    context.getSharedPreferences(Maoni.class.getPackage().getName(),
                        Context.MODE_PRIVATE)
                        .getStringSet(DEFAULT_LISTENER_EMAIL_TO, new HashSet<String>());
                this.listener =
                    new MaoniEmailListener(context,
                        defaultToAddresses.toArray(new String[defaultToAddresses.size()]));
            } else {
                Log.d(LOG_TAG, "context is NULL => no default listener configured");
            }
        }

        @NonNull
        public static CallbacksConfiguration getInstance(@Nullable final Context context) {
            if (SINGLETON == null) {
                SINGLETON = new CallbacksConfiguration(context);
            }
            return SINGLETON;
        }

        @Nullable
        public Validator getValidator() {
            return validator;
        }

        @NonNull
        public CallbacksConfiguration setValidator(@Nullable final Validator validator) {
            this.validator = validator;
            return this;
        }

        @Nullable
        public Listener getListener() {
            return listener;
        }

        @NonNull
        public CallbacksConfiguration setListener(@Nullable final Listener listener) {
            this.listener = listener;
            return this;
        }

        @Nullable
        public UiListener getUiListener() {
            return uiListener;
        }

        @NonNull
        public CallbacksConfiguration setUiListener(@Nullable final UiListener uiListener) {
            this.uiListener = uiListener;
            return this;
        }

        @SuppressWarnings("UnusedReturnValue")
        public CallbacksConfiguration reset() {
            return this.setUiListener(null)
                    .setListener(null)
                    .setValidator(null);
        }
    }

}