public final class InfinityExponentialBackoffManager implements BackoffManager {

    private static volatile InfinityExponentialBackoffManager instance;

    @NonNull
    private final BackoffManager registerBackoffAdapter;

    @NonNull
    private final BackoffManager unregisterBackoffAdapter;

    private InfinityExponentialBackoffManager() {
        registerBackoffAdapter = new RegisterBackoffAdapter<>(InfinityExponentialBackoff.class);
        unregisterBackoffAdapter = new UnregisterBackoffAdapter<>(InfinityExponentialBackoff.class);
    }

    @SuppressWarnings("PMD.NonThreadSafeSingleton")
    public static InfinityExponentialBackoffManager getInstance() {
        OPFChecks.checkThread(true);
        if (instance == null) {
            instance = new InfinityExponentialBackoffManager();
        }

        return instance;
    }

    public boolean hasTries(@NonNull final String providerName,
                            @NonNull final Operation operation) {
        OPFLog.logMethod(providerName, operation);
        return getManagerByOperation(operation).hasTries(providerName, operation);
    }

    public long getTryDelay(@NonNull final String providerName,
                            @NonNull final Operation operation) {
        OPFLog.logMethod(providerName, operation);
        return getManagerByOperation(operation).getTryDelay(providerName, operation);
    }

    public void reset(@NonNull final String providerName,
                      @NonNull final Operation operation) {
        OPFLog.logMethod(providerName, operation);
        getManagerByOperation(operation).reset(providerName, operation);
    }

    @NonNull
    private BackoffManager getManagerByOperation(@NonNull final Operation operation) {
        switch (operation) {
            case REGISTER:
                return registerBackoffAdapter;
            case UNREGISTER:
                return unregisterBackoffAdapter;
        }
        throw new IllegalStateException("Wrong operation : " + operation);
    }
} public final class RetryManager implements BackoffManager {

    private static volatile RetryManager instance;

    @NonNull
    private final Context appContext;

    @NonNull
    private final BackoffManager backoffManager;

    @NonNull
    private final AlarmManager alarmManager;

    private final Set<Pair<String, String>> retryProvidersActions;

    @Nullable
    private ConnectivityChangeReceiver connectivityChangeReceiver;

    private RetryManager(@NonNull final Context context,
                         @NonNull final BackoffManager backoffManager) {
        this.appContext = context.getApplicationContext();
        this.backoffManager = backoffManager;
        this.alarmManager = (AlarmManager) context.getSystemService(ALARM_SERVICE);
        this.retryProvidersActions = new HashSet<>();
    }

    @NonNull
    @SuppressWarnings("PMD.NonThreadSafeSingleton")
    public static RetryManager init(@NonNull final Context context,
                            @NonNull final BackoffManager backoffManager) {
        OPFChecks.checkThread(true);
        checkInit(false);
        return instance = new RetryManager(context, backoffManager);
    }

    @NonNull
    public static RetryManager getInstance() {
        OPFChecks.checkThread(true);
        checkInit(true);
        return instance;
    }

    private static void checkInit(final boolean initExpected) {
        final boolean isInit = instance != null;
        if (initExpected != isInit) {
            throw new InitException(isInit);
        }
    }

    @Override
    public boolean hasTries(@NonNull final String providerName,
                            @NonNull final Operation operation) {
        return backoffManager.hasTries(providerName, operation);
    }

    @Override
    public long getTryDelay(@NonNull final String providerName,
                            @NonNull final Operation operation) {
        return backoffManager.getTryDelay(providerName, operation);
    }

    @Override
    public void reset(@NonNull final String providerName, @NonNull final Operation operation) {
        backoffManager.reset(providerName, operation);
    }

    public void postRetryRegister(@NonNull final String providerName) {
        OPFLog.logMethod(providerName);
        postRetry(providerName, REGISTER, ACTION_RETRY_REGISTER);
    }

    public void postRetryUnregister(@NonNull final String providerName) {
        OPFLog.logMethod(providerName);
        postRetry(providerName, UNREGISTER, ACTION_RETRY_UNREGISTER);
    }

    public void cancelRetryAllOperations(@NonNull final String providerName) {
        cancelRetryRegister(providerName);
        cancelRetryUnregister(providerName);
    }

    public void cancelRetryRegister(@NonNull final String providerName) {
        OPFLog.logMethod(providerName);
        cancelRetry(providerName, REGISTER, ACTION_RETRY_REGISTER);
    }

    public void cancelRetryUnregister(@NonNull final String providerName) {
        OPFLog.logMethod(providerName);
        cancelRetry(providerName, UNREGISTER, ACTION_RETRY_UNREGISTER);
    }

    @NonNull
    public Set<Pair<String, String>> getRetryProvidersActions() {
        OPFLog.logMethod();
        return retryProvidersActions;
    }

    private void postRetry(@NonNull final String providerName,
                           @NonNull final Operation operation,
                           @NonNull final String action) {
        final long when = System.currentTimeMillis() + getTryDelay(providerName, operation);
        OPFLog.d("Post retry %s provider '%s' at %s",
                operation,
                providerName,
                SimpleDateFormat.getDateTimeInstance(
                        DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US
                ).format(new Date(when))
        );

        retryProvidersActions.add(new Pair<>(providerName, action));
        registerConnectivityChangeReceiver();

        final Intent intent = new Intent(appContext, RetryBroadcastReceiver.class);
        intent.setAction(action);
        intent.putExtra(EXTRA_PROVIDER_NAME, providerName);

        alarmManager.set(
                AlarmManager.RTC,
                when,
                PendingIntent.getBroadcast(appContext, providerName.hashCode(), intent, 0)
        );
    }

    private void cancelRetry(@NonNull final String providerName,
                             @NonNull final Operation operation,
                             @NonNull final String action) {
        reset(providerName, operation);

        retryProvidersActions.remove(new Pair<>(providerName, action));
        if (retryProvidersActions.isEmpty()) {
            unregisterConnectivityChangeReceiver();
        }

        final Intent intent = new Intent(appContext, RetryBroadcastReceiver.class);
        intent.setAction(action);

        final PendingIntent pendingIntent = PendingIntent
                .getBroadcast(appContext, providerName.hashCode(), intent, 0);
        alarmManager.cancel(pendingIntent);
        pendingIntent.cancel();
    }

    private void registerConnectivityChangeReceiver() {
        OPFLog.logMethod();
        if (connectivityChangeReceiver == null) {
            connectivityChangeReceiver = new ConnectivityChangeReceiver();
            appContext.registerReceiver(connectivityChangeReceiver, new IntentFilter(CONNECTIVITY_ACTION));
        }
    }

    private void unregisterConnectivityChangeReceiver() {
        OPFLog.logMethod();
        if (connectivityChangeReceiver != null) {
            appContext.unregisterReceiver(connectivityChangeReceiver);
            connectivityChangeReceiver = null;
        }
    }
} @SuppressWarnings("PMD.MissingStaticMethodInNonInstantiatableClass")
public final class Configuration {

    @NonNull
    private final List<PushProvider> providers;

    @Nullable
    private final EventListener eventListener;

    @Nullable
    private final CheckManifestHandler checkManifestHandler;

    private final boolean isSelectSystemPreferred;

    private Configuration(@NonNull final Collection<? extends PushProvider> providers,
                          @Nullable final EventListener eventListener,
                          final boolean selectSystemPreferred,
                          @Nullable CheckManifestHandler checkManifestHandler) {
        this.providers = Collections.unmodifiableList(new ArrayList<>(providers));
        this.eventListener = eventListener;
        this.isSelectSystemPreferred = selectSystemPreferred;
        this.checkManifestHandler = checkManifestHandler;
    }

    /**
     * Returns all available push providers.
     *
     * @return All available push providers.
     */
    @NonNull
    public List<PushProvider> getProviders() {
        return providers;
    }

    /**
     * Returns the instance of the {@link org.onepf.opfpush.listener.EventListener}.
     *
     * @return The instance of the {@link org.onepf.opfpush.listener.EventListener}.
     */
    @Nullable
    public EventListener getEventListener() {
        return eventListener;
    }

    /**
     * Returns {@code true} if the system push provider is preferred, false otherwise.
     *
     * @return {@code true} if the system push provider is preferred, false otherwise.
     */
    public boolean isSelectSystemPreferred() {
        return isSelectSystemPreferred;
    }

    /**
     * Returns the instance of the {@link CheckManifestHandler}.
     *
     * @return The instance of the {@link CheckManifestHandler}.
     */
    @Nullable
    public CheckManifestHandler getCheckManifestHandler() {
        return checkManifestHandler;
    }

    @Override
    public String toString() {
        return "Configuration {"
                + "providers = " + providers
                + ", isSelectSystemPreferred = " + isSelectSystemPreferred
                + '}';
    }

    /**
     * The builder class that creates an instance of the {@code Configuration} class.
     */
    public static final class Builder {

        @Nullable
        private Map<String, PushProvider> providersMap;

        @Nullable
        private EventListener eventListener;

        private boolean isSelectSystemPreferred;

        @Nullable
        private CheckManifestHandler checkManifestHandler;

        /**
         * See {@link #addProviders(java.util.List)}
         *
         * @return The current {@code Builder} instance.
         * @throws java.lang.IllegalArgumentException If a provider was already added
         */
        @NonNull
        public Builder addProviders(@NonNull final PushProvider... providers) {
            if (providers.length == 0) {
                return this;
            } else {
                return addProviders(Arrays.asList(providers));
            }
        }

        /**
         * Add push providers to the configuration. The priority of the providers corresponds to the order
         * in which they were added.
         *
         * @return The current {@code Builder} instance.
         * @throws java.lang.IllegalArgumentException If a provider was already added.
         */
        @NonNull
        public Builder addProviders(@NonNull final List<? extends PushProvider> providers) {
            if (providers.isEmpty()) {
                return this;
            }

            if (this.providersMap == null) {
                this.providersMap = new LinkedHashMap<>();
            }

            for (PushProvider provider : providers) {
                final String providerName = provider.getName();
                if (this.providersMap.containsKey(providerName)) {
                    throw new IllegalArgumentException(
                            String.format("Provider '%s' already added.", provider)
                    );
                } else {
                    this.providersMap.put(providerName, provider);
                }
            }
            return this;
        }

        @NonNull
        public Builder setEventListener(@NonNull final EventListener eventListener) {
            this.eventListener = eventListener;
            return this;
        }

        /**
         * If you set {@code true}, the system push provider will get the highest priority.
         * For Google devices this is Google Cloud Messaging, for Kindle devices - ADM.
         * False by default.
         *
         * @param isSelectSystemPreferred {@code true} if the system provider is preferred, {@code false} otherwise.
         * @return The current {@code Builder}.
         */
        public Builder setSelectSystemPreferred(final boolean isSelectSystemPreferred) {
            this.isSelectSystemPreferred = isSelectSystemPreferred;
            return this;
        }

        @NonNull
        public Builder setCheckManifestHandler(@NonNull final CheckManifestHandler checkManifestHandler) {
            this.checkManifestHandler = checkManifestHandler;
            return this;
        }

        /**
         * Create the instance of the {@link Configuration} class.
         *
         * @return The new {@link Configuration} object.
         * @throws java.lang.IllegalArgumentException If there are no any added providers.
         */
        @NonNull
        @SuppressWarnings("PMD.AccessorClassGeneration")
        public Configuration build() {
            if (providersMap == null) {
                throw new IllegalArgumentException("Need to add at least one push provider.");
            }

            return new Configuration(
                    providersMap.values(),
                    eventListener,
                    isSelectSystemPreferred,
                    checkManifestHandler
            );
        }

        @Override
        public String toString() {
            return "Builder{"
                    + "providersMap="
                    + providersMap
                    + ", systemPushPreferred="
                    + isSelectSystemPreferred
                    + '}';
        }
    }
} public interface EventListener {

    /**
     * Called when a new message has been received.
     *
     * @param context      The application context.
     * @param providerName The name of the provider which has received the event.
     * @param extras       The data associated with the message.
     */
    void onMessage(@NonNull Context context, @NonNull String providerName, @Nullable Bundle extras);

    /**
     * Called when a notification about deleted messages has been received.
     * Some providers don't send the count of deleted messages. In this case the message count will equal to
     * {@link org.onepf.opfpush.OPFConstants#MESSAGES_COUNT_UNKNOWN}.
     *
     * @param context       The application context.
     * @param providerName  The name of the provider which has received the event.
     * @param messagesCount The count of messages. The {@link org.onepf.opfpush.OPFConstants#MESSAGES_COUNT_UNKNOWN} value
     *                      if there is no information about the count.
     */
    void onDeletedMessages(@NonNull Context context, @NonNull String providerName, int messagesCount);

    /**
     * Called when a message about successful registration has been received.
     *
     * @param context        The application context.
     * @param providerName   The name of the provider which has received the event.
     * @param registrationId The registration ID for push notifications.
     */
    void onRegistered(@NonNull Context context, @NonNull String providerName, @NonNull String registrationId);

    /**
     * Called when the {@link org.onepf.opfpush.OPFPushHelper} becomes {@code UNREGISTERED}.
     * Calling this method doesn't mean that the provider is already unregistered. The unregistering
     * is performed in the background and is retried if an unregistration error is occurred.
     *
     * @param context        The application context.
     * @param providerName   The name of the provider which has received the event.
     * @param registrationId The old registration ID for push notification.
     */
    void onUnregistered(@NonNull Context context, @NonNull String providerName, @Nullable String registrationId);

    /**
     * The {@code OpenPushHelper} can't find any available provider for register push.
     * You should notify the user that push notifications will not be received.
     * <p/>
     * A push provider can be unavailable in two common reasons:
     * <ol>
     * <li>The {@link org.onepf.opfpush.pushprovider.PushProvider#getAvailabilityResult()} method has returned false value.</li>
     * <li>An unrecoverable registration error has occurred.</li>
     * </ol>
     * <p/>
     * In the first case an {@link org.onepf.opfpush.model.UnrecoverablePushError} object will be put
     * to the {@code Map} if {@link org.onepf.opfpush.model.AvailabilityResult#getErrorCode()} is not null.
     * In other words if a provider returned specific error code while an availability check,
     * you will get the {@link org.onepf.opfpush.model.UnrecoverablePushError.Type#AVAILABILITY_ERROR}
     * and the {@link org.onepf.opfpush.model.UnrecoverablePushError#getAvailabilityErrorCode()} will return
     * not null value.
     * If a provider is no available because the host app (store) of the provider isn't installed,
     * you will not get an error for the provider in the {@code Map}.
     * <p/>
     * In the second case you can get the {@link org.onepf.opfpush.model.UnrecoverablePushError} object
     * from the {@code pushErrors} map.
     * You can notify the user about the occurred error if the error can be resolved by user.
     * For example if you get the {@link org.onepf.opfpush.model.UnrecoverablePushError.Type#AUTHENTICATION_FAILED}
     * for the GCM push provider, you can ask the user whether he wants to add the google account.
     *
     * @param context            The application context.
     * @param pushErrors The map in which registration errors are stored with a push providers' name as a key
     *                           If there is no registration errors, the map is empty.
     */
    void onNoAvailableProvider(@NonNull Context context, @NonNull Map<String, UnrecoverablePushError> pushErrors);
} public final class AvailabilityResult {

    private boolean isAvailable;

    @Nullable
    private Integer errorCode;

    public AvailabilityResult(final boolean isAvailable) {
        this(isAvailable, null);
    }

    public AvailabilityResult(@NonNull final Integer errorCode) {
        this(false, errorCode);
    }

    public AvailabilityResult(final boolean isAvailable, @Nullable final Integer errorCode) {
        this.isAvailable = isAvailable;
        this.errorCode = errorCode;
    }

    /**
     * Returns {@code true} if a provider is available, false otherwise.
     *
     * @return {@code true} if a provider is available, false otherwise.
     */
    public boolean isAvailable() {
        return isAvailable;
    }

    /**
     * Returns a specific provider error code.
     *
     * @return The availability error code, or {@code null} if a provider hasn't returned a error code.
     */
    @Nullable
    public Integer getErrorCode() {
        return errorCode;
    }
} public class Message implements Parcelable {

    public static final Creator<Message> CREATOR = new Creator<Message>() {

        @Override
        public Message createFromParcel(Parcel source) {
            return new Message(source);
        }

        @Override
        public Message[] newArray(int size) {
            return new Message[size];
        }
    };

    private final String id;
    private final Bundle data;
    private final long timeToLeave;

    /**
     * Create new message.
     *
     * @param id   Message's ID.
     * @param data Messages' data to send.
     */
    public Message(@NonNull final String id, @NonNull final Bundle data) {
        this(id, data, 0);
    }

    /**
     * Create new message.
     *
     * @param id          Message's ID.
     * @param data        Messages' data to send.
     * @param timeToLeave How long message is valid. Set 0 to default value.
     * @throws java.lang.IllegalArgumentException If {@code timeToLeave} is negative.
     */
    public Message(@NonNull final String id, @NonNull final Bundle data, final long timeToLeave) {
        if (timeToLeave < 0) {
            throw new IllegalArgumentException(String.format("timeToLeave='%d'."
                    + " Time to leave must be non negative value.", timeToLeave));
        }
        this.id = id;
        this.data = data;
        this.timeToLeave = timeToLeave;
    }

    private Message(final Parcel parcel) {
        id = parcel.readString();
        timeToLeave = parcel.readLong();
        data = parcel.readBundle();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(id);
        dest.writeLong(timeToLeave);
        dest.writeBundle(data);
    }

    @NonNull
    public String getId() {
        return id;
    }

    @NonNull
    public Bundle getData() {
        return data;
    }

    public long getTimeToLeave() {
        return timeToLeave;
    }

    @NonNull
    @Override
    public String toString() {
        return "Message{"
                + "messageId='" + id + '\''
                + ", data='" + OPFUtils.toString(data)
                + '\''
                + ", TTL='" + timeToLeave + '\''
                + '}';
    }
} public abstract class PushError<T extends ErrorType> {

    @NonNull
    protected final T type;

    @NonNull
    protected final String providerName;

    @NonNull
    protected final String originalError;

    protected PushError(@NonNull final T type,
                        @NonNull final String providerName,
                        @NonNull final String originalError) {
        this.type = type;
        this.providerName = providerName;
        this.originalError = originalError;
    }

    /**
     * Returns {@code true} if the error is recoverable, {@code false} otherwise.
     *
     * @return {@code true} if the error is recoverable, {@code false} otherwise.
     */
    public abstract boolean isRecoverable();

    /**
     * Returns the type of the error.
     *
     * @return The type of the error.
     */
    @NonNull
    public T getType() {
        return type;
    }

    /**
     * Returns the name of the provider that occurs the error.
     *
     * @return The name of the provider that occurs the error.
     */
    @NonNull
    public String getProviderName() {
        return providerName;
    }

    /**
     * Returns the original error id.
     * Error id is a {@code String} that is returned by a specific push provider.
     * This {@code String} usually is described in the documentation of a push provider.
     *
     * @return The error id.
     */
    @NonNull
    public String getOriginalError() {
        return originalError;
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "PushError : \n{\"providerName\" : \"%1$s\",\n\"type\" : \"%2$s\",\n\"originalError\":\"%3$s\"}",
                providerName,
                type,
                originalError);
    }
} public enum State {

    /**
     * Indicates that an unregistration has been completed.
     */
    UNREGISTERED(0),

    /**
     * Indicates that a registration has been completed.
     */
    REGISTERED(2),

    /**
     * Indicates that a registration is being performed.
     */
    REGISTERING(3);

    private int value;

    State(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    @Nullable
    public static State fromValue(final int fromValue) {
        final State[] values = values();
        for (final State state : values) {
            final int stateValue = state.getValue();
            if (stateValue == fromValue) {
                return state;
            }
        }

        return null;
    }
} public final class UnrecoverablePushError extends PushError<UnrecoverablePushError.Type> {

    @Nullable
    private Integer availabilityErrorCode;

    public UnrecoverablePushError(@NonNull final Type type,
                                  @NonNull final String providerName) {
        this(type, providerName, type.name(), null);
    }

    public UnrecoverablePushError(@NonNull final Type type,
                                  @NonNull final String providerName,
                                  @NonNull final String errorId) {
        this(type, providerName, errorId, null);
    }

    public UnrecoverablePushError(@NonNull final Type type,
                                  @NonNull final String providerName,
                                  @NonNull final Integer availabilityErrorCode) {
        this(type, providerName, type.name(), availabilityErrorCode);
    }

    public UnrecoverablePushError(@NonNull final Type type,
                                  @NonNull final String providerName,
                                  @NonNull final String errorId,
                                  @Nullable final Integer availabilityErrorCode) {
        super(type, providerName, errorId);
        this.availabilityErrorCode = availabilityErrorCode;
    }

    /**
     * Returns an availability error code, or {@code null} if a provider hasn't returned a specific availability error.
     * For GCM it corresponds to the {@code ConnectionResult} codes.
     *
     * @return The error code indicates the specific provider availability error. Can be {@code null}.
     */
    @Nullable
    public Integer getAvailabilityErrorCode() {
        return availabilityErrorCode;
    }

    @Override
    public boolean isRecoverable() {
        return false;
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "UnrecoverablePushError : \n"
                        + "{\"providerName\" : \"%1$s\",\n"
                        + "\"type\" : \"%2$s\",\n"
                        + "\"originalError\":\"%3$s\",\n"
                        + "\"availabilityErrorCode\":\"%4$s\"}",
                providerName,
                type,
                originalError,
                availabilityErrorCode);
    }

    public enum Type implements ErrorType {

        /**
         * Invalid parameters have been sent to register provider.
         */
        INVALID_PARAMETERS,

        /**
         * An invalid sender ID has been used for the registration.
         */
        INVALID_SENDER,

        /**
         * The authentication failure.
         */
        AUTHENTICATION_FAILED,

        /**
         * A provider specific error has occurred while a registration.
         * Look {@link #getOriginalError()} for more information.
         */
        PROVIDER_SPECIFIC_ERROR,

        /**
         * Occurred when a provider is unavailable by provider specific error.
         * Use {@link #getAvailabilityErrorCode()} to know about the reason of a provider unavailability.
         */
        AVAILABILITY_ERROR
    }
} public interface NotificationMaker {

    /**
     * Returns true if a notification must be shown for the bundle. False otherwise.
     *
     * @param bundle The bundle received from a push provider.
     * @return Returns true if a notification should be shown for the bundle. False otherwise.
     */
    boolean needShowNotification(@NonNull final Bundle bundle);

    /**
     * Shows notification using bundle data.
     *
     * @param context The Context instance.
     * @param bundle  Received data.
     */
    void showNotification(@NonNull final Context context, @NonNull final Bundle bundle);
} public interface PushProvider {
    /**
     * Initiates the registration of the provider. Must be async.
     * <p/>
     * Intended for the internal use, should never be called directly.
     * To start the registration call {@link org.onepf.opfpush.OPFPushHelper#register()}.
     */
    void register();

    /**
     * Initiates the unregistering of the provider. Must be async.
     * <p/>
     * Intended for internal use, should never be called directly.
     * To start registration call {@link org.onepf.opfpush.OPFPushHelper#unregister()}.
     */
    void unregister();

    /**
     * Checks whether the provider is available.
     *
     * @return The instance of {@link org.onepf.opfpush.model.AvailabilityResult}
     * that contains result of availability check.
     */
    @NonNull
    AvailabilityResult getAvailabilityResult();

    /**
     * Checks whether the application was successfully registered on the service.
     *
     * @return {@code true} if the application was successfully registered on the service, otherwise false.
     */
    boolean isRegistered();

    /**
     * Returns the registration ID or null if provider isn't registered.
     *
     * @return The registration ID or null if provider isn't registered.
     */
    @Nullable
    String getRegistrationId();

    /**
     * Returns the name of the provider. Must be unique for all providers.
     *
     * @return The name of the provider.
     */
    @NonNull
    String getName();

    /**
     * Returns the package of the application that contains API of the provider.
     * Usually, this is a store application.
     *
     * @return The package of the application that contains API of the provider.
     */
    @Nullable
    String getHostAppPackage();

    /**
     * Returns the {@link NotificationMaker} object associated with provider.
     *
     * @return The {@link NotificationMaker} instance.
     */
    @NonNull
    NotificationMaker getNotificationMaker();

    /**
     * Verify that application manifest contains all needed permissions.
     *
     * @param checkManifestHandler If not null an exception isn't thrown.
     * @throws java.lang.IllegalStateException If not all required permissions and components have been
     *                                         described in the AndroidManifest.xml file.
     */
    void checkManifest(@Nullable CheckManifestHandler checkManifestHandler);

    /**
     * Callback method, that called when the application state change, like update to new version,
     * or system state changed, like update firmware to a newer version.
     * <p/>
     * If this method is called, your registration becomes invalid
     * and you have to reset all saved registration data.
     */
    void onRegistrationInvalid();

    /**
     * Callback method for notify that the provider became unavailable.
     * In this method you have to reset all saved registration data.
     */
    void onUnavailable();
} public static final String ACTION_NO_AVAILABLE_PROVIDER = "org.onepf.opfpush.intent.NO_AVAILABLE_PROVIDER"; public static final String ACTION_RECEIVE = "org.onepf.opfpush.intent.RECEIVE"; public static final String ACTION_REGISTRATION = "org.onepf.opfpush.intent.REGISTRATION"; public static final String ACTION_UNREGISTRATION = "org.onepf.opfpush.intent.UNREGISTRATION";
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.onepf.opfpush.backoff.InfinityExponentialBackoffManager;
import org.onepf.opfpush.backoff.RetryManager;
import org.onepf.opfpush.configuration.Configuration;
import org.onepf.opfpush.listener.EventListener;
import org.onepf.opfpush.model.AvailabilityResult;
import org.onepf.opfpush.model.Message;
import org.onepf.opfpush.model.PushError;
import org.onepf.opfpush.model.State;
import org.onepf.opfpush.model.UnrecoverablePushError;
import org.onepf.opfpush.notification.NotificationMaker;
import org.onepf.opfpush.pushprovider.PushProvider;
import org.onepf.opfutils.OPFChecks;
import org.onepf.opfutils.OPFLog;
import org.onepf.opfutils.OPFUtils;
import org.onepf.opfutils.exception.InitException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import static android.provider.Settings.Secure;
import static android.provider.Settings.Secure.ANDROID_ID;
import static org.onepf.opfpush.OPFConstants.ACTION_NO_AVAILABLE_PROVIDER;
import static org.onepf.opfpush.OPFConstants.ACTION_RECEIVE;
import static org.onepf.opfpush.OPFConstants.ACTION_REGISTRATION;
import static org.onepf.opfpush.OPFConstants.ACTION_UNREGISTRATION;
import static org.onepf.opfpush.model.Operation.REGISTER;
import static org.onepf.opfpush.model.Operation.UNREGISTER;
import static org.onepf.opfpush.model.State.REGISTERED;
import static org.onepf.opfpush.model.State.REGISTERING;
import static org.onepf.opfpush.model.State.UNREGISTERED;
import static org.onepf.opfpush.model.UnrecoverablePushError.Type.AVAILABILITY_ERROR;
    /**
     * Returns the registration ID if there's the registered push provider, null otherwise.
     *
     * @return The registration ID if there's the registered push provider, null otherwise.
     */
    @Nullable
    @Override
    public String getRegistrationId() {
        if (currentProvider != null) {
            return currentProvider.getRegistrationId();
        }

        return null;
    }

    /**
     * Returns the current provider name if there's the registered push provider, null otherwise.
     *
     * @return The current provider name if there's the registered push provider, null otherwise.
     */
    @Nullable
    @Override
    public String getProviderName() {
        if (currentProvider != null) {
            return currentProvider.getName();
        }

        return null;
    }

    /**
     * Returns the registered push provider.
     *
     * @return The registered push provider. Can be null.
     */
    @Nullable
    @Override
    public PushProvider getCurrentProvider() {
        return currentProvider;
    }

    /**
     * Returns the {@link org.onepf.opfpush.OPFPushHelper.ReceivedMessageHandler} instance.
     * Intended for the internal use, should never be called directly.
     *
     * @return The {@link org.onepf.opfpush.OPFPushHelper.ReceivedMessageHandler} instance.
     */
    @NonNull
    @Override
    public ReceivedMessageHandler getReceivedMessageHandler() {
        checkInit(true);
        return receivedMessageHandler;
    }

    /**
     * Returns {@code true} if the {@code OPFPushHelper} is registered, false otherwise.
     *
     * @return {@code true} if the {@code OPFPushHelper} is registered, false otherwise.
     */
    @Override
    public boolean isRegistered() {
        return settings.getState() == REGISTERED;
    }

    /**
     * Returns {@code true} if the registration operation is being performed at the moment.
     *
     * @return {@code true} if the registration operation is being performed at the moment.
     */
    @Override
    public boolean isRegistering() {
        return settings.getState() == REGISTERING;
    }

    @Override
    public void onNeedRetryRegistration() {
        OPFLog.logMethod();
        OPFLog.d("Current provider : " + currentProvider);
        settings.clear();
        if (currentProvider != null) {
            currentProvider.onRegistrationInvalid();
            currentProvider = null;
        }
        register();
    }

    @NonNull
    @Override
    public String toString() {
        return "OPFPushHelper{"
                + "options="
                + configuration
                + ", currentProvider="
                + currentProvider
                + ", initDone="
                + isInitDone()
                + ", registered="
                + isRegistered()
                + '}';
    }

    @SuppressFBWarnings({"DC_DOUBLECHECK", "DC_DOUBLECHECK"})
    @Override
    void init(@NonNull final Configuration initialConfiguration) {
        OPFLog.logMethod(initialConfiguration);

        if (this.configuration == null) {
            synchronized (initLock) {
                if (this.configuration == null) {
                    this.configuration = initialConfiguration;
                }
            }
        }

        final List<PushProvider> providers = configuration.getProviders();
        for (PushProvider provider : providers) {
            provider.checkManifest(configuration.getCheckManifestHandler());
            providersByHostApps.put(provider.getHostAppPackage(), provider.getName());
        }

        retryManager = RetryManager.init(appContext, InfinityExponentialBackoffManager.getInstance());