public class AdminApi {

    private static final String AUTHORIZATION = "Authorization";
    private static final String AUTHORIZATION_HEADER = AUTHORIZATION + ": Bearer %s";

    @SuppressWarnings("WeakerAccess")
    final AdminApiService adminApiService;
    private String clientId;

    @SuppressWarnings("WeakerAccess")
    String token;

    @SuppressWarnings("WeakerAccess")
    final Gson gson;

    @SuppressWarnings("WeakerAccess")
    static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    @SuppressWarnings("WeakerAccess")
    final Executor executorBackground;

    @SuppressWarnings("WeakerAccess")
    @Nullable
    ZerokitCountingIdlingResource idlingResource;

    private static AdminApi instance;

    public static AdminApi init(@NonNull String host, String clientId) {
        instance = new AdminApi(host, clientId);
        return instance;
    }

    public static AdminApi getInstance() {
        return instance;
    }

    private AdminApi(String appbackendUrl, String clientId) {
        this.clientId = clientId;
        OkHttpClient.Builder builderHttpClient = new OkHttpClient.Builder();

        HttpLoggingInterceptor interceptorLogging = new HttpLoggingInterceptor();
        interceptorLogging.setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE);
        Interceptor interceptorAuthorization = new Interceptor() {

            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                String authHeader = request.headers().get(AUTHORIZATION);
                if (authHeader != null)
                    request = request.newBuilder().removeHeader(AUTHORIZATION).addHeader(AUTHORIZATION, String.format(authHeader, token)).build();
                return chain.proceed(request);
            }
        };

        builderHttpClient.addInterceptor(interceptorAuthorization);
        builderHttpClient.addInterceptor(interceptorLogging);

        Retrofit.Builder builder = new Retrofit.Builder().baseUrl(appbackendUrl).client(builderHttpClient.build()).addConverterFactory(GsonConverterFactory.create());
        Retrofit retrofit = builder.build();
        adminApiService = retrofit.create(AdminApiService.class);
        gson = new Gson();
        executorBackground = Executors.newSingleThreadExecutor();
    }

    public String getClientId() {
        return clientId;
    }

    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    public void clearClientId() {
        setClientId(null);
    }

    public void setToken(String token) {
        this.token = token;
    }

    public void clearToken() {
        setToken(null);
    }

    public String getToken() {
        return token;
    }

    private String getData(String data) {
        return getData(data, null);
    }

    private String getData(String data, String tresorId) {
        return new JSONObject().put("data", data).put("tresorId", tresorId).toString();
    }

    public Call<String, ResponseAdminApiError> getUserId(final String userName) {
        return new CallRetrofit<>(adminApiService.getUserId(userName));
    }

    public Call<ResponseAdminApiInitUserRegistration, ResponseAdminApiError> initReg(final String userName, final String profileData) {
        return new CallRetrofit<>(adminApiService.initReg(userName, profileData));
    }

    public Call<Void, ResponseAdminApiError> finishReg(final String userId, final String validationVerifier) {
        return new CallRetrofit<>(adminApiService.finishReg(userId, validationVerifier));
    }

    public Call<String, ResponseAdminApiError> validateUser(final String userId, final String validationCode) {
        return new CallRetrofit<>(adminApiService.validateUser(userId, validationCode));
    }

    public Call<String, ResponseAdminApiError> setProfile(final String profile) {
        return new CallRetrofit<>(adminApiService.setProfile(RequestBody.create(JSON, getData(profile))));
    }

    public Call<String, ResponseAdminApiError> getProfile() {
        return new CallRetrofit<>(adminApiService.getProfile());
    }

    public Call<Void, ResponseAdminApiError> storePublicProfile(final String publicProfile) {
        return new CallRetrofit<>(adminApiService.storePublicProfile(RequestBody.create(JSON, getData(publicProfile))));
    }

    public Call<String, ResponseAdminApiError> getPublicProfile(String userId) {
        return new CallRetrofit<>(adminApiService.getPublicProfile(userId));
    }

    public Call<ResponseAdminApiLoginByCode, ResponseAdminApiError> login(final String code) {
        return new CallRetrofit<>(adminApiService.login(clientId, code));
    }

    public Call<Void, ResponseAdminApiError> createdTresor(final String tresorId) {
        return new CallRetrofit<>(adminApiService.createdTresor(tresorId));
    }

    public Call<Void, ResponseAdminApiError> sharedTresor(final String operationId) {
        return new CallRetrofit<>(adminApiService.sharedTresor(operationId));
    }

    public Call<Void, ResponseAdminApiError> kickedUser(final String operationId) {
        return new CallRetrofit<>(adminApiService.kickedUser(operationId));
    }

    public Call<Void, ResponseAdminApiError> invitedUser(final String operationId) {
        return new CallRetrofit<>(adminApiService.invitedUser(operationId));
    }

    @SuppressWarnings("WeakerAccess")
    public Call<Void, ResponseAdminApiError> storeData(final String tresorId, final String id, final String data) {
        return new CallRetrofit<>(adminApiService.storeData(id, RequestBody.create(JSON, getData(data, tresorId))));
    }

    @SuppressWarnings("WeakerAccess")
    public Call<String, ResponseAdminApiError> fetchData(final String id) {
        return new CallRetrofit<>(adminApiService.fetchData(id));
    }

    public Call<Void, ResponseAdminApiError> acceptedInvitationLink(final String operationId) {
        return new CallRetrofit<>(adminApiService.acceptedInvitationLink(operationId));
    }

    public Call<Void, ResponseAdminApiError> createdInvitationLink(final String operationId) {
        return new CallRetrofit<>(adminApiService.createdInvitationLink(operationId));
    }

    public Call<Void, ResponseAdminApiError> revokedInvitationLink(final String operationId) {
        return new CallRetrofit<>(adminApiService.revokedInvitationLink(operationId));
    }


    private class CallRetrofit<T> extends CallBase<T, ResponseAdminApiError> {

        final retrofit2.Call<T> call;

        CallRetrofit(retrofit2.Call<T> call) {
            this.call = call;
        }

        @Override
        public void enqueue(Callback<? super T, ? super ResponseAdminApiError> callback) {
            incrementIdlingResource();
            call.enqueue(new CallbackRetrofit<>(callback));
        }

        @Override
        public Response<T, ResponseAdminApiError> execute() {
            incrementIdlingResource();
            final Holder<Response<T, ResponseAdminApiError>> result = new Holder<>();
            final CountDownLatch signal = new CountDownLatch(1);
            executorBackground.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        result.t = getResponseFromResponse(call.execute());
                    } catch (EOFException e) {
                        result.t = Response.fromValue(null);
                    } catch (IOException e) {
                        result.t = Response.fromError(new ResponseAdminApiError(e.getMessage()));
                    }
                    signal.countDown();
                }
            });
            if (signal.getCount() > 0)
                try {
                    signal.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            return result.t;
        }
    }


    @SuppressWarnings("WeakerAccess")
    void incrementIdlingResource() {
        if (idlingResource != null) idlingResource.increment();
    }

    @SuppressWarnings("WeakerAccess")
    void decrementIdlingResource() {
        if (idlingResource != null) idlingResource.decrement();
    }


    private class CallbackRetrofit<T> implements retrofit2.Callback<T> {
        private final Callback<? super T, ? super ResponseAdminApiError> callback;

        CallbackRetrofit(Callback<? super T, ? super ResponseAdminApiError> callback) {
            this.callback = callback;
        }

        @Override
        public void onResponse(retrofit2.Call<T> call, retrofit2.Response<T> response) {
            decrementIdlingResource();
            Response<? extends T, ? extends ResponseAdminApiError> responseFromResponse = getResponseFromResponse(response);
            if (responseFromResponse.isError())
                callback.onError(responseFromResponse.getError());
            else
                callback.onSuccess(responseFromResponse.getResult());
        }

        @Override
        public void onFailure(retrofit2.Call<T> call, Throwable t) {
            decrementIdlingResource();
            if (t instanceof EOFException)
                callback.onSuccess(null);
            else
                callback.onError(new ResponseAdminApiError(t.getMessage()));
        }
    }


    @SuppressWarnings("WeakerAccess")
    <T> Response<T, ResponseAdminApiError> getResponseFromResponse(retrofit2.Response<T> response) {
        Response<T, ResponseAdminApiError> result;
        if (response.isSuccessful())
            result = Response.fromValue(response.body());
        else {
            try {
                ResponseBody responseBody = response.errorBody();
                if (responseBody.contentType() != null && "json".equals(responseBody.contentType().subtype()))
                    result = Response.fromError(gson.fromJson(new String(responseBody.bytes()), ResponseAdminApiError.class));
                else
                    result = Response.fromError(new ResponseAdminApiError(new String(responseBody.bytes())));
            } catch (IOException e) {
                result = Response.fromError(new ResponseAdminApiError(e.getMessage()));
            }
        }
        return result;
    }

    private interface AdminApiService {

        @GET("/api/user/get-user-id")
        retrofit2.Call<String> getUserId(@Query("userName") String userName);

        @FormUrlEncoded
        @POST("/api/user/init-user-registration")
        retrofit2.Call<ResponseAdminApiInitUserRegistration> initReg(@Field("userName") String userName, @Field("profileData") String profileData);

        @FormUrlEncoded
        @POST("/api/user/finish-user-registration")
        retrofit2.Call<Void> finishReg(@Field("userId") String userId, @Field("validationVerifier") String validationVerifier);

        @FormUrlEncoded
        @POST("/api/user/validate-user")
        retrofit2.Call<String> validateUser(@Field("userId") String userId, @Field("validationCode") String validationCode);

        @FormUrlEncoded
        @POST("/api/auth/login-by-code?token=true")
        retrofit2.Call<ResponseAdminApiLoginByCode> login(@Query("clientId") String clientId, @Field("code") String code);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/tresor/created")
        retrofit2.Call<Void> createdTresor(@Field("tresorId") String tresorId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/tresor/invited-user")
        retrofit2.Call<Void> sharedTresor(@Field("operationId") String operationId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/tresor/kicked-user")
        retrofit2.Call<Void> kickedUser(@Field("operationId") String operationId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/user/invited-user")
        retrofit2.Call<Void> invitedUser(@Field("operationId") String operationId);

        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/data/store")
        retrofit2.Call<Void> storeData(@Query("id") String id, @Body RequestBody data);

        @Headers(AUTHORIZATION_HEADER)
        @GET("/api/data/get")
        retrofit2.Call<String> fetchData(@Query("id") String id);

        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/data/profile")
        retrofit2.Call<String> setProfile(@Body RequestBody data);

        @Headers(AUTHORIZATION_HEADER)
        @GET("/api/data/profile")
        retrofit2.Call<String> getProfile();

        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/data/public-profile")
        retrofit2.Call<Void> storePublicProfile(@Body RequestBody data);

        @Headers(AUTHORIZATION_HEADER)
        @GET("/api/data/public-profile")
        retrofit2.Call<String> getPublicProfile(@Query("id") String userId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/invitationLinks/created")
        retrofit2.Call<Void> createdInvitationLink(@Field("operationId") String operationId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/invitationLinks/revoked")
        retrofit2.Call<Void> revokedInvitationLink(@Field("operationId") String operationId);

        @FormUrlEncoded
        @Headers(AUTHORIZATION_HEADER)
        @POST("/api/invitationLinks/accepted")
        retrofit2.Call<Void> acceptedInvitationLink(@Field("operationId") String operationId);
    }


    private class ZerokitCountingIdlingResource implements IdlingResource {

        private final AtomicInteger counter;

        private volatile ResourceCallback resourceCallback;

        ZerokitCountingIdlingResource() {
            this(0);
        }

        ZerokitCountingIdlingResource(int initialCount) {
            this.counter = new AtomicInteger(initialCount);
        }

        @Override
        public String getName() {
            return getClass().getName();
        }

        @Override
        public boolean isIdleNow() {
            return counter.get() == 0;
        }

        @Override
        public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
            this.resourceCallback = resourceCallback;
        }

        void increment() {
            counter.getAndIncrement();
        }

        void decrement() {
            int counterVal = counter.decrementAndGet();
            if (counterVal == 0) {
                if (null != resourceCallback) {
                    resourceCallback.onTransitionToIdle();
                }
            }

            if (counterVal < 0) {
                throw new IllegalArgumentException("Counter has been corrupted!");
            }
        }
    }

    @VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
        if (idlingResource == null) idlingResource = new ZerokitCountingIdlingResource();
        return idlingResource;
    }

    private class JSONObject {

        private org.json.JSONObject jsonObject;

        public JSONObject() {
            jsonObject = new org.json.JSONObject();
        }

        public String getString(String name) {
            if (jsonObject != null)
                try {
                    return jsonObject.getString(name);
                } catch (JSONException e) {
                    //e.printStackTrace();
                }
            return "";
        }


        public JSONObject put(String name, Object value)  {
            if (jsonObject != null)
                try {
                    jsonObject.put(name, value);
                } catch (JSONException e) {
                    //e.printStackTrace();
                }
            return this;
        }

        @Override
        public String toString() {
            return jsonObject != null ? jsonObject.toString() : "";
        }
    }
} public final class PasswordEditText extends AppCompatEditText {

    private final LongSparseArray<Editable> editablePool = new LongSparseArray<>();
    OnChangeListener onChangeListener;

    public PasswordEditText(Context context) {
        super(context);
        init();
    }

    PasswordEditText(Context context, CharSequence text) {
        super(context);
        init();
        initText(text);
    }

    public PasswordEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PasswordEditText(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        super.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
        super.setText("");
        super.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (onChangeListener != null)
                    onChangeListener.onChanged();
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });
    }

    public final void setPasswordExporter(PasswordExporter passwordExporter) {
        passwordExporter.setEditText(this);
    }

    public final void setOnChangeListener(OnChangeListener onChangeListener) {
        this.onChangeListener = onChangeListener;
    }

    public final PasswordExporter getPasswordExporter() {
        return new PasswordExporter(this);
    }

    @Override
    public final Editable getText() {
        if (editablePool == null) return super.getText();
        int length = super.getText().length();
        @SuppressWarnings("ConstantConditions")
        Editable result = editablePool == null ? super.getText() : editablePool.get(length);
        if (result == null) {
            char[] chars = new char[length];
            Arrays.fill(chars, '*');
            result = Editable.Factory.getInstance().newEditable(new String(chars));
            editablePool.put(length, result);
        }
        return result;
    }

    @Override
    public final void setText(CharSequence text, BufferType type) {
        super.setText("", type);
    }

    void initText(CharSequence text){
        super.setText(text, BufferType.NORMAL);
    }

    @Override
    public final void setInputType(int type) {
    }


    @Override
    public final Parcelable onSaveInstanceState() {
        clear();
        return super.onSaveInstanceState();
    }

    @Override
    public final void addTextChangedListener(TextWatcher watcher) {
    }


    //**********************************

    public final void clear() {
        super.getText().clear();
    }

    @Override
    public final int length() {
        return super.getText().length();
    }

    public final boolean isEmpty() {
        return length() <= 0;
    }

    public final boolean isContentEqual(PasswordEditText passwordEditText) {
        char[] charArray1 = getCharArray(false);
        char[] charArray2 = passwordEditText.getCharArray(false);
        boolean result = Arrays.equals(charArray1, charArray2);
        Arrays.fill(charArray1, '\u0000');
        Arrays.fill(charArray2, '\u0000');
        return result;
    }

    public final boolean isContentEqual(PasswordExporter exporter) {
        return isContentEqual(exporter.editText);
    }

    final char[] getCharArray(boolean clear) {
        Editable editable = super.getText();
        char[] chars = new char[editable.length()];
        editable.getChars(0, chars.length, chars, 0);
        if (clear) editable.clear();
        return chars;
    }

    public static final class PasswordExporter  {

        PasswordEditText editText;
        OnChangeListener onChangeListener;

        public PasswordExporter() {
        }

        PasswordExporter(PasswordEditText editText) {
            this.editText = editText;
        }

        public final int length() {
            return editText.length();
        }

        public final boolean isEmpty() {
            return editText.isEmpty();
        }

        public void clear() {
            editText.clear();
        }

        public final boolean isContentEqual(PasswordExporter passwordExporter) {
            return editText.isContentEqual(passwordExporter);
        }

        public final boolean isContentEqual(PasswordEditText editText) {
            return this.editText.isContentEqual(editText);
        }

        public final void setOnChangeListener(OnChangeListener changeListener){
            this.onChangeListener = changeListener;
            if (editText != null) editText.setOnChangeListener(changeListener);
        }

        final void setEditText(PasswordEditText editText) {
            this.editText = editText;
            setOnChangeListener(onChangeListener);
        }

        final PasswordEditText getEditText() {
            return editText;
        }

        final char[] getCharArray(boolean clear) {
            return editText.getCharArray(clear);
        }
    }

    public interface OnChangeListener{
        void onChanged();
    }

} public final class Zerokit {

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "Zerokit #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<>(128);

    @SuppressWarnings("WeakerAccess")
    final Executor executorWebView;

    @SuppressWarnings("WeakerAccess")
    final Executor executorBackground;

    private final static class MainThreadExecutor implements Executor {
        private final Handler handler;

        MainThreadExecutor() {
            handler = new Handler(Looper.getMainLooper());
        }

        @Override
        public void execute(Runnable r) {
            if (isMainThread()) r.run();
            else handler.post(r);
        }
    }

    @SuppressWarnings("WeakerAccess")
    static boolean isMainThread() {
        return Looper.myLooper() == Looper.getMainLooper();
    }


    @SuppressWarnings("WeakerAccess")
    static final String HTTPS_SCHEME = "https";
    private static final String REDIRECT_URI_FORMAT_PATTERN = "%1$s.%2$s.api.tresorit.io";

    @SuppressWarnings("WeakerAccess")
    static final String param_error = "error";
    @SuppressWarnings("WeakerAccess")
    static final String param_error_description = "error_description";
    @SuppressWarnings("WeakerAccess")
    static final String param_code = "code";
    @SuppressWarnings("WeakerAccess")
    static final String param_state = "state";
    @SuppressWarnings("WeakerAccess")
    static final String param_id_token = "id_token";

    private static final String ALPHANUMERIC_CHARACTERS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

    private final SecureRandom secureRandom;

    /**
     * Log tag
     */
    private static final String TAG = "Zerokit";

    /**
     * Key to get the api root metadata from manifest file
     */
    public static final String API_ROOT = "com.tresorit.zerokitsdk.API_ROOT";

    /**
     * The name of the keystore file
     */
    private static final String KEY_STORE_PATH = "login";
    /**
     * The name of the keystore alias
     */
    private static final String KEY_STORE_ALIAS = "key_login";
    /**
     * The name of the Android Keystore instance
     */
    private static final String ANDROID_KEYSTORE = "AndroidKeyStore";

    /**
     * Singleton instance for Zerokit
     */
    @SuppressLint("StaticFieldLeak")
    private static Zerokit instance;

    /**
     * The webview instance, which will be responsible for the javascript communication.
     * This webview requires a context, which need to be provided by the SDK user.
     * This view will be not attached to any other view.
     */
    @SuppressWarnings("WeakerAccess")
    WebView webView;

    @SuppressWarnings("WeakerAccess")
    WebView webViewIDP;
    /**
     * Own webview client which is responsible for handle the 'page-finished' events
     */
    @SuppressWarnings("WeakerAccess")
    final ZerokitWebViewClientBase clientWebView;

    @SuppressWarnings("WeakerAccess")
    final ZerokitWebViewClientBase clientIdpWebView;

    @SuppressWarnings("WeakerAccess")
    final StateHandler idpStateHandler;

    /**
     * Represents the states of the initialization process
     */
    @SuppressWarnings("WeakerAccess")
    final StateHandler initStateHandler;

    /**
     * Collection of the registered observers, which will be triggered after a javascript function returns a result
     */
    @SuppressWarnings("WeakerAccess")
    final Map<String, Callback<? super String, ? super String>> observers;

    /**
     * The api root url
     */
    @SuppressWarnings("WeakerAccess")
    final String apiRoot;

    @SuppressWarnings("WeakerAccess")
    final String apiRootUrl;

    @SuppressWarnings("WeakerAccess")
    final String tenantId;

    /**
     * The javascript source of javascript
     */
    @SuppressWarnings("WeakerAccess")
    final String serializerJavaScriptSource;

    @SuppressWarnings("WeakerAccess")
    final String idpHelperJavaScriptSource;

    /**
     * The JavaScript interface which is callable from the javascript side and handles the results of the promises
     */
    @SuppressWarnings("WeakerAccess")
    final JSInterfaceResponseHandler jsInterfaceResponseHandler;
    /**
     * The JavaScript interface which is responsible for pass byte arrays to the javascript side
     */
    @SuppressWarnings("WeakerAccess")
    final JSInterfaceByteArrayProvider jsInterfaceByteArrayProvider;

    @SuppressWarnings("WeakerAccess")
    final JSInterfaceHtmlExporter jsInterfaceHtmlExporter;

    @SuppressWarnings("WeakerAccess")
    @Nullable
    ZerokitCountingIdlingResource idlingResource;

    /**
     * Represents the states of the initialization process
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({State.NOT_STARTED, State.RUNNING, State.FINISHED})
    private @interface State {
        int NOT_STARTED = 0;
        int RUNNING = 1;
        int FINISHED = 2;
    }

    /**
     * Init the Zerokit singleton instance
     *
     * @param context a Context object used to pass it to the WebView and to get the metadata from manifest file
     */
    static Zerokit init(@NonNull Context context, @NonNull String url) {
        if (TextUtils.isEmpty(url))
            throw new IllegalStateException("No ApiRoot definition found in the AndroidManifest.xml");
        instance = new Zerokit(context, url);
        return instance;
    }

    /**
     * Returns the Zerokit instance
     *
     * @return the Zerokit instance
     */
    public static Zerokit getInstance() {
        return instance;
    }

    /**
     * Constructs a new Zerokit instance with a Context object.
     *
     * @param context a Context object used to pass it to the WebView
     * @param baseUrl     the url of the provided api root
     */
    @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"})
    private Zerokit(@NonNull final Context context, @NonNull String baseUrl) {
        Uri uri = Uri.parse(baseUrl);

        executorWebView = new MainThreadExecutor();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        executorBackground = threadPoolExecutor;
        secureRandom = new SecureRandom();

        apiRoot = uri.getAuthority();
        tenantId = TenantIdResolver.getTenantId(baseUrl);
        apiRootUrl = uri.buildUpon().appendPath("static").appendPath("v4").appendPath("api.html").build().toString();

        observers = new ConcurrentHashMap<>();

        clientWebView = new ZerokitWebViewClientBase();
        clientIdpWebView = new ZerokitWebViewClientBase();

        idpStateHandler = new StateHandler();
        initStateHandler = new StateHandler();

        idpStateHandler.addListener(new StateChangeListener() {
            @Override
            public void onStateChanged(@State int state) {
                switch (state) {
                    case State.FINISHED:
                        decrementIdlingResource();
                        jsInterfaceHtmlExporter.removeAllListeners();
                        break;
                    case State.RUNNING:
                        incrementIdlingResoure();
                        break;
                }
            }
        });

        jsInterfaceResponseHandler = new JSInterfaceResponseHandler();
        jsInterfaceByteArrayProvider = new JSInterfaceByteArrayProvider();
        jsInterfaceHtmlExporter = new JSInterfaceHtmlExporter();

        clientWebView.addPageFinishListener(new PageFinishListener() {
            @Override
            public void onPageFinished(String url) {
                if (initStateHandler.getState() == State.RUNNING) {
                    log("Init finished: " + url);
                    initStateHandler.setState(State.FINISHED);
                    clientWebView.removePageFinishListener(this);
                }
            }

            @Override
            public void onReceivedError(int errorCode) {
                switch (errorCode) {
                    case WebViewClient.ERROR_HOST_LOOKUP:
                        log("Init failed: " + errorCode);
                        initStateHandler.setState(State.NOT_STARTED);
                        break;
                }
            }
        });

        serializerJavaScriptSource = loadJavaScriptSource(context, R.raw.javascript);
        idpHelperJavaScriptSource = loadJavaScriptSource(context, R.raw.javascript_idp);

        Runnable init = new Runnable() {
            @Override
            public void run() {
                webView = createWebView(context);
                webView.addJavascriptInterface(jsInterfaceResponseHandler, "JSInterfaceResponseHandler");
                webView.addJavascriptInterface(jsInterfaceByteArrayProvider, "JSInterfaceByteArrayProvider");
                webView.setWebViewClient(clientWebView);
                webView.setWebChromeClient(new ZerokitWebChromeClient());

                webViewIDP = createWebView(context);
                webViewIDP.addJavascriptInterface(jsInterfaceHtmlExporter, "JSInterfaceHtmlExporter");
                webViewIDP.setWebViewClient(clientIdpWebView);

                synchronized (executorWebView) {
                    executorWebView.notify();
                }
            }
        };
        if (isMainThread()) init.run();
        else {
            executorWebView.execute(init);
            synchronized (executorWebView) {
                try {
                    executorWebView.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @SuppressWarnings({"WeakerAccess", "SetJavaScriptEnabled"})
    WebView createWebView(Context context) {
        WebView webView = new WebView(context);
        webView.setWillNotDraw(true);
        webView.getSettings().setJavaScriptEnabled(true);
        webView.getSettings().setDomStorageEnabled(true);
        return webView;
    }

    @NonNull
    private String loadJavaScriptSource(@NonNull Context context, int resId) {
        BufferedReader reader = null;
        StringBuilder stringBuffer = new StringBuilder();
        try {
            reader = new BufferedReader(new InputStreamReader(context.getResources().openRawResource(resId)));
            String line = reader.readLine();
            while (line != null) {
                stringBuffer.append(line);
                line = reader.readLine();
            }
        } catch (IOException e) {
            Log.e(TAG, Log.getStackTraceString(e));
        } finally {
            if (reader != null)
                try {
                    reader.close();
                } catch (IOException e) {
                    Log.e(TAG, Log.getStackTraceString(e));
                }
        }
        return stringBuffer.toString();
    }

    @SuppressWarnings({"WeakerAccess"})
    CallAsync<Void, String> initIdpCheck() {
        return new CallAsyncAction<>(new ActionCallback<Void, String>() {
            @Override
            public void call(final Callback<? super Void, ? super String> subscriber) {
                if (idpStateHandler.getState() == State.RUNNING) {
                    idpStateHandler.addListener(new StateChangeListener() {
                        @Override
                        public void onStateChanged(@State int state) {
                            if (state == State.FINISHED) {
                                subscriber.onSuccess(null);
                                idpStateHandler.removeListener(this);
                            }
                        }
                    });
                } else
                    /*
                    * If the idp request has been finished
                    */
                    subscriber.onSuccess(null);
            }
        });
    }

    /**
     * The init method of the webview. This function is responsible for load init url
     *
     * @return an Observable object which will tell us when the init finished
     */
    @NonNull
    @SuppressWarnings({"WeakerAccess"})
    CallAsync<Void, String> initMainCheck() {
        return new CallAsyncAction<>(new ActionCallback<Void, String>() {
            @Override
            public void call(final Callback<? super Void, ? super String> subscriber) {
                if (initStateHandler.getState() != State.FINISHED) {
                    log("Init not finished");
                    /*
                    * If the init url not loaded yet
                    */
                    initStateHandler.addListener(new StateChangeListener() {
                        @Override
                        public void onStateChanged(@State int state) {
                            if (state == State.FINISHED) {
                                subscriber.onSuccess(null);
                                initStateHandler.removeListener(this);
                            } else if (state == State.NOT_STARTED) {
                                subscriber.onError("Initialization failed");
                                initStateHandler.removeListener(this);
                            }
                        }
                    });

                    /*
                    * Ensure that the init url has been called only once
                    */
                    if (initStateHandler.getState() == State.NOT_STARTED) {
                        if (!URLUtil.isValidUrl(apiRootUrl)) subscriber.onError("Invalid root url");
                        else {
                            log("Init started");
                            initStateHandler.setState(State.RUNNING);
                            loadUrl(webView, apiRootUrl, false);
                        }
                    }
                } else
                    /*
                    * If the init already has been finished
                    */
                    subscriber.onSuccess(null);
            }
        });
    }

    public CallAsync<Void, String> initMain() {
        return initMainCheck();
    }


    @SuppressWarnings({"WeakerAccess"})
    <T> void callFunction(@NonNull final Function function, @NonNull final CallbackByteArrayIds<T> callback, final Object... arguments) {
        callFunction(function, callback, callback.ids, arguments);
    }


    /**
     * The common "function caller" method, which transmits a request to the javascript section
     *
     * @param function   The concrete function which we would like to call
     * @param subscriber an Observer object, which will handle the result of the method
     * @param arguments  the arguments of the function which we would like to call
     */
    @SuppressWarnings({"WeakerAccess", "unchecked"})
    void callFunction(@NonNull final Function function, @NonNull final Callback<String, String> subscriber, final String[] ids, final Object... arguments) {
        log(String.format("call: %s", function.name()));

        initMainCheck().enqueue(new Action<Void>() {
            @Override
            public void call(Void result) {
                String id = UUID.randomUUID().toString();
                observers.put(id, subscriber);
                incrementIdlingResoure();

                try {
                    JSONObject callData = new JSONObject();
                    callData.put("id", id);
                    callData.put("type", function.type);
                    callData.put("functionName", function.name());
                    JSONArray args = new JSONArray();
                    for (Object arg : arguments) args.put(arg);
                    callData.put("args", args);


                    JSONArray extraArgs = new JSONArray();
                    for (int i = 0; i < function.extraArgs.length; i++) {
                        ExtraArg extraArg = function.extraArgs[i];
                        JSONObject jsonExtraArg = new JSONObject();
                        jsonExtraArg.put("position", (extraArg.position == -1) ? (args.length() + i) : extraArg.position);
                        jsonExtraArg.put("id", ids[i]);
                        jsonExtraArg.put("type", extraArg.type);
                        extraArgs.put(jsonExtraArg);
                    }
                    callData.put("extraArgs", extraArgs);

                    callData.put("responseFormatter", function.responseFormatter);

                    loadUrl(webView,
                            "javascript:\n" +
                                    serializerJavaScriptSource +
                                    String.format("\ncallFunction(%s);", JSONObject.quote(callData.toString())));

                } catch (JSONException e1) {
                    observers.remove(id);
                    decrementIdlingResource();
                    subscriber.onError(new ResponseZerokitError("JSONException").toJSON());
                }
            }
        }, new Action<String>() {
            @Override
            public void call(String error) {
                subscriber.onError(new ResponseZerokitError(error).toJSON());
            }
        });
    }

    @SuppressWarnings("WeakerAccess")
    void callFunctionIdp() {
        initMainCheck().enqueue(new Action<Void>() {
            @Override
            public void call(Void aVoid) {
                loadUrl(webViewIDP, "javascript:\n" +
                        idpHelperJavaScriptSource +
                        "\ncallFunction();");
            }
        });
    }

    @SuppressWarnings("WeakerAccess")
    void callIdpUrl(final String url) {
        initMainCheck().enqueue(new Action<Void>() {
            @Override
            public void call(Void aVoid) {
                loadUrl(webViewIDP, url, false);
            }
        });
    }

    void loadUrl(@NonNull final WebView webView, @NonNull final String url){
        loadUrl(webView, url, true);
    }

    /**
     * Loads an url in the WebView instance (on the Handler thread)
     *
     * @param url The url which will be loaded in the webview
     */
    @SuppressWarnings("WeakerAccess")
    void loadUrl(@NonNull final WebView webView, @NonNull final String url, final boolean isJS) {
        executorWebView.execute(new Runnable() {
            @Override
            public void run() {
                if (isJS)
                    webView.evaluateJavascript(url, null);
                else
                    webView.loadUrl(url);
            }
        });
    }

    @SuppressWarnings("WeakerAccess")
    void storeSecret(@NonNull String alias, @NonNull String secret) {
        Context context = webView.getContext();
        FileOutputStream fileOutputStream = null;
        try {
            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
            keyStore.load(null);

            // Create the keys if necessary
            if (!keyStore.containsAlias(alias)) generateNewKey(context, alias);

            Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            inCipher.init(Cipher.ENCRYPT_MODE, ((KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null)).getCertificate().getPublicKey());

            fileOutputStream = new FileOutputStream(new File(context.getFilesDir(), KEY_STORE_PATH));
            CipherOutputStream cipherOutputStream = new CipherOutputStream(fileOutputStream, inCipher);
            cipherOutputStream.write(secret.getBytes("UTF-8"));
            cipherOutputStream.close();

        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException | KeyStoreException | CertificateException | IOException | UnrecoverableEntryException | InvalidKeyException | UnsupportedOperationException | NoSuchPaddingException e) {
            Log.e(TAG, Log.getStackTraceString(e));
        } finally {
            if (fileOutputStream != null)
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    Log.e(TAG, Log.getStackTraceString(e));
                }
        }
    }


    @SuppressWarnings("WeakerAccess")
    String getSecret(@NonNull String alias) {
        Context context = webView.getContext();
        FileInputStream fileInputStream = null;
        try {
            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
            keyStore.load(null);

            Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            KeyStore.Entry entry = keyStore.getEntry(alias, null);
            if (entry != null) {
                outCipher.init(Cipher.DECRYPT_MODE, ((KeyStore.PrivateKeyEntry) entry).getPrivateKey());

                fileInputStream = new FileInputStream(new File(context.getFilesDir(), KEY_STORE_PATH));
                CipherInputStream cipherInputStream = new CipherInputStream(fileInputStream, outCipher);

                int nextByte;
                ArrayList<Byte> values = new ArrayList<>();
                while ((nextByte = cipherInputStream.read()) != -1) values.add((byte) nextByte);
                byte[] bytes = new byte[values.size()];
                for (int i = 0; i < bytes.length; i++) bytes[i] = values.get(i);
                return new String(bytes, 0, bytes.length, "UTF-8");
            }

        } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException | UnrecoverableEntryException | InvalidKeyException | UnsupportedOperationException | NoSuchPaddingException e) {
            Log.e(TAG, Log.getStackTraceString(e));
        } finally {
            if (fileInputStream != null)
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    Log.e(TAG, Log.getStackTraceString(e));
                }
        }
        return null;
    }

    @SuppressWarnings("WeakerAccess")
    void deleteSecret(@NonNull String alias) {
        try {
            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
            keyStore.load(null);
            keyStore.deleteEntry(alias);
        } catch (NoSuchAlgorithmException | KeyStoreException | CertificateException | IOException | UnsupportedOperationException e) {
            Log.e(TAG, Log.getStackTraceString(e));
        }
    }

    private void generateNewKey(@NonNull Context context, @NonNull String alias) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
        Calendar notBefore = Calendar.getInstance();
        Calendar notAfter = Calendar.getInstance();
        notAfter.add(Calendar.YEAR, 1);

        KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
                .setAlias(alias)
                .setSubject(new X500Principal("CN=zerokit"))
                .setSerialNumber(BigInteger.ONE)
                .setStartDate(notBefore.getTime())
                .setEndDate(notAfter.getTime())
                .build();
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", ANDROID_KEYSTORE);
        generator.initialize(spec);
        generator.generateKeyPair();
    }

    @SuppressWarnings("WeakerAccess")
    String getSha256(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        messageDigest.update(text.getBytes("UTF-8"));
        return Base64.encodeToString(messageDigest.digest(), Base64.URL_SAFE).split("=")[0];
    }

    @SuppressWarnings("WeakerAccess")
    String getRandomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        int lengthAll = ALPHANUMERIC_CHARACTERS.length();
        for (int i = 0; i < length; i++)
            sb.append(ALPHANUMERIC_CHARACTERS.charAt(secureRandom.nextInt(lengthAll)));
        return sb.toString();
    }

    @SuppressWarnings("WeakerAccess")
    void requireIdp(final String clientId, final boolean useProofKey, final Callback<? super IdentityTokens, ? super ResponseZerokitError> callback) {
        try {
            final String redirectUriAuthority = String.format(REDIRECT_URI_FORMAT_PATTERN, clientId, tenantId).toLowerCase();

            final String nonce = getRandomString(64);
            final String state = getRandomString(64);
            Uri.Builder builder = initIdpUriBuilder(nonce, state, clientId);

            final String codeVerifier = useProofKey ? getRandomString(64) : null;
            if (useProofKey)
                builder.appendQueryParameter("code_challenge_method", "S256")
                        .appendQueryParameter("code_challenge", getSha256(codeVerifier));

            final PageFinishListener[] pageFinishListener = new PageFinishListener[1];
            final InterfaceHtmlExporter[] interfaceHtmlExporter = new InterfaceHtmlExporter[1];

            pageFinishListener[0] = new PageFinishListener() {

                @Override
                public void onReceivedError(int errorCode) {

                }

                @Override
                public void onPageFinished(String url) {
                    Uri uri = Uri.parse(url);
                    if (redirectUriAuthority.equals(uri.getAuthority())) {
                        clientIdpWebView.removePageFinishListener(this);
                        String fragment = uri.getFragment();
                        if (!TextUtils.isEmpty(fragment)) {

                            Map<String, String> parameters = new HashMap<>();
                            for (String pair : fragment.split("&")) {
                                String[] split = pair.split("=");
                                parameters.put(split[0], split[1]);
                            }

                            if (parameters.containsKey(param_error)) {
                                String error = parameters.get(param_error);
                                String error_desc = parameters.containsKey(param_error_description) ? parameters.get(param_error_description) : "";
                                if (!useProofKey && "invalid_request".equals(error) && "code challenge required".equals(error_desc)) {
                                    jsInterfaceHtmlExporter.removeListener(interfaceHtmlExporter[0]);
                                    requireIdp(clientId, true, callback);
                                } else {
                                    callback.onError(new ResponseZerokitError(error, error_desc));
                                }
                            } else if (parameters.containsKey(param_code)) {
                                if (!state.equals(parameters.get(param_state)))
                                    callback.onError(new ResponseZerokitError("The state has changed during the idp process"));
                                else
                                    callback.onSuccess(new IdentityTokens(parameters.get(param_code), parameters.containsKey(param_id_token) ? parameters.get(param_id_token) : "", codeVerifier));
                            }
                        } else {
                            if (uri.getQueryParameterNames().contains(param_error))
                                callback.onError(new ResponseZerokitError(uri.getQueryParameter(param_error)));
                        }
                    } else callFunctionIdp();
                }
            };

            interfaceHtmlExporter[0] = new InterfaceHtmlExporter() {
                @Override
                public void onGetError(String error, String description) {
                    if (!TextUtils.isEmpty(error) || !TextUtils.isEmpty(error)) {
                        callback.onError(new ResponseZerokitError(error, description));
                        jsInterfaceHtmlExporter.removeListener(this);
                        clientIdpWebView.removePageFinishListener(pageFinishListener[0]);
                    }
                }
            };

            clientIdpWebView.addPageFinishListener(pageFinishListener[0]);
            jsInterfaceHtmlExporter.addListener(interfaceHtmlExporter[0]);

            callIdpUrl(builder.build().toString());
        } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
            callback.onError(new ResponseZerokitError(e.getMessage()));
        }
    }


    @SuppressWarnings("WeakerAccess")
    Uri.Builder initIdpUriBuilder(String nonce, String state, String clientId) {
        return new Uri.Builder()
                .scheme(HTTPS_SCHEME)
                .authority(apiRoot)
                .appendPath("idp")
                .appendPath("connect")
                .appendPath("authorize")
                .appendQueryParameter("client_id", clientId)
                .appendQueryParameter("redirect_uri", new Uri.Builder().scheme(HTTPS_SCHEME).authority(String.format(REDIRECT_URI_FORMAT_PATTERN, clientId, tenantId)).appendPath("").build().toString())
                .appendQueryParameter("response_type", "code id_token")
                .appendQueryParameter("scope", "openid profile")
                .appendQueryParameter("state", state)
                .appendQueryParameter("nonce", nonce)
                .appendQueryParameter("response_mode", "fragment")
                .appendQueryParameter("prompt", "none");
    }

    /**
     * Converts char array to byte array securely
     *
     * @param chars the char array which will be converted to byte array
     * @return the byte array
     */
    @NonNull
    private byte[] toBytes(@NonNull char[] chars) {
        CharBuffer charBuffer = CharBuffer.wrap(chars);
        ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
        byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
        Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
        Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
        return bytes;
    }

    @SuppressWarnings("WeakerAccess")
    boolean isNetworkAvailable() {
        NetworkInfo activeNetworkInfo = ((ConnectivityManager) webView.getContext().getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
        return activeNetworkInfo != null && activeNetworkInfo.isConnected();
    }

    /**
     * Send a log message if app is in debug mode
     *
     * @param msg The message you would like logged.
     */
    @SuppressWarnings("WeakerAccess")
    void log(@NonNull String msg) {
        if (BuildConfig.DEBUG)
            Log.d("Zerokit", msg);
    }

    @SuppressWarnings("WeakerAccess")
    void incrementIdlingResoure() {
        if (idlingResource != null) idlingResource.increment();
    }

    @SuppressWarnings("WeakerAccess")
    void decrementIdlingResource() {
        if (idlingResource != null) idlingResource.decrement();
    }

    private class ZerokitCountingIdlingResource implements IdlingResource {

        private final AtomicInteger counter;

        private volatile ResourceCallback resourceCallback;

        ZerokitCountingIdlingResource() {
            this(0);
        }

        ZerokitCountingIdlingResource(int initialCount) {
            this.counter = new AtomicInteger(initialCount);
        }

        @Override
        public String getName() {
            return getClass().getName();
        }

        @Override
        public boolean isIdleNow() {
            return counter.get() == 0;
        }

        @Override
        public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
            this.resourceCallback = resourceCallback;
        }

        void increment() {
            counter.getAndIncrement();
        }

        void decrement() {
            int counterVal = counter.decrementAndGet();
            if (counterVal == 0) {
                if (null != resourceCallback) {
                    resourceCallback.onTransitionToIdle();
                }
            }

            if (counterVal < 0) {
                throw new IllegalArgumentException("Counter has been corrupted!");
            }
        }
    }

    @VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
        if (idlingResource == null) idlingResource = new ZerokitCountingIdlingResource();
        return idlingResource;
    }


    private class JSInterfaceByteArrayProvider {

        private final Map<String, byte[]> byteArrays;

        /**
         * Package private constructor to avoid method generation to access private constructor of inner class
         */
        JSInterfaceByteArrayProvider() {
            byteArrays = new HashMap<>();
        }

        public String add(@NonNull byte[] array) {
            String key = UUID.randomUUID().toString();
            byteArrays.put(key, Arrays.copyOf(array, array.length));
            return key;
        }

        void remove(String key) {
            byte[] bytes = byteArrays.get(key);
            if (bytes != null) Arrays.fill(bytes, (byte) 0);
            byteArrays.remove(key);
        }

        @JavascriptInterface
        @SuppressWarnings("unused")
        public byte getByte(String id, int index) {
            return byteArrays.get(id)[index];
        }


        @JavascriptInterface
        @SuppressWarnings("unused")
        public int getLength(String id) {
            return byteArrays.get(id).length;
        }
    }

    interface InterfaceHtmlExporter {

        void onGetError(String error, String description);
    }

    private class JSInterfaceHtmlExporter implements InterfaceHtmlExporter {

        final List<InterfaceHtmlExporter> listeners;

        JSInterfaceHtmlExporter() {
            listeners = new ArrayList<>();
        }

        void addListener(InterfaceHtmlExporter listener) {
            listeners.add(listener);
        }

        void removeListener(InterfaceHtmlExporter listener) {
            listeners.remove(listener);
        }

        void removeAllListeners() {
            listeners.clear();
        }

        @JavascriptInterface
        @Override
        public void onGetError(final String error, final String description) {
            for (InterfaceHtmlExporter listener : listeners)
                listener.onGetError(error, description);
        }
    }

    /**
     * The Java interface which is callable from the javascript side and handles the results from the promises
     */
    private class JSInterfaceResponseHandler {

        /**
         * Package private constructor to avoid method generation to access private constructor of inner class
         */
        JSInterfaceResponseHandler() {
        }

        /**
         * Called if a function has finished successfully
         *
         * @param result the result which is returned by the javascript method
         * @param key    the key which identify which observers are responsible for handle the result
         */
        @JavascriptInterface
        @SuppressWarnings("unused")
        public void onSuccess(final String result, final String key) {
            final Callback<? super String, ? super String> subscriber = observers.get(key);
            if (subscriber != null) {
                subscriber.onSuccess(result);
                observers.remove(key);
                decrementIdlingResource();
            }
        }

        /**
         * Called if a function has finished with error
         *
         * @param result the error result which is returned by the javascript method
         * @param key    the key which identify which observers are responsible for handle the error
         */
        @JavascriptInterface
        @SuppressWarnings("unused")
        public void onError(final String result, final String key) {
            final Callback<? super String, ? super String> subscriber = observers.get(key);
            if (subscriber != null) {
                subscriber.onError(result);
                observers.remove(key);
                decrementIdlingResource();
            }
        }
    }

    private enum ArgType {
        ByteArray, JSONToken
    }

    private static class ExtraArg {
        private final ArgType type;
        private final int position;

        ExtraArg(ArgType type) {
            this(type, -1);
        }

        ExtraArg(ArgType type, int position) {
            this.type = type;
            this.position = position;
        }
    }

    /**
     * An enum which represents a javascript function with parameters
     * From this will be generated the concrete javascript function
     */
    enum Function {
        zxcvbn(Default, null, new ExtraArg(ArgType.ByteArray, 0)),

        login(MobileCmd, null, new ExtraArg(ArgType.ByteArray)),
        loginByRememberMeKey(MobileCmd, null),
        getRememberMeKey(MobileCmd, null, new ExtraArg(ArgType.ByteArray)),
        register(MobileCmd, null, new ExtraArg(ArgType.ByteArray)),
        createInvitationLink(MobileCmd, null, new ExtraArg(ArgType.ByteArray)),
        acceptInvitationLink(MobileCmd, null, new ExtraArg(ArgType.JSONToken, 0), new ExtraArg(ArgType.ByteArray)),
        changePassword(MobileCmd, null, new ExtraArg(ArgType.ByteArray), new ExtraArg(ArgType.ByteArray)),

        acceptInvitationLinkNoPassword(new ExtraArg(ArgType.JSONToken, 0)),
        createInvitationLinkNoPassword,
        createTresor,
        decrypt,
        encrypt,
        getInvitationLinkInfo(Cmd, ArgType.JSONToken),
        revokeInvitationLink,
        kickFromTresor,
        logout,
        shareTresor,
        whoAmI;


        @IntDef({Default, Cmd, MobileCmd})
        @interface Type {
            int Default = 0;
            int Cmd = 1;
            int MobileCmd = 2;
        }

        /**
         * Modifies the url, which the function will be called on
         */
        @Type
        final int type;
        final ExtraArg[] extraArgs;
        final ArgType responseFormatter;

        /**
         * Constructs a Function with the given parameters, which will represent a javascript function.
         * The isMobile is false by default
         */
        Function() {
            this(Cmd, null);
        }

        Function(ExtraArg... extraArgs) {
            this(Cmd, null, extraArgs);
        }

        /**
         * Constructs a Function with the given parameters, which will represent a javascript function
         */
        Function(@Type int type, ArgType responseFormatter, ExtraArg... extraArgs) {
            this.type = type;
            this.extraArgs = extraArgs;
            this.responseFormatter = responseFormatter;
        }

    }

    /**
     * Interface for page finish listening
     */
    interface PageFinishListener {


        /**
         * Notify the host application that a page has finished loading. This method
         * is called only for main frame. When onPageFinished() is called, the
         * rendering picture may not be updated yet. To get the notification for the
         * new Picture, use {@link WebView.PictureListener#onNewPicture}.
         *
         * @param url The url of the page.
         */
        void onPageFinished(@SuppressWarnings("UnusedParameters") String url);

        /**
         * Report an error to the host application. These errors are unrecoverable
         * (i.e. the main resource is unavailable). The errorCode parameter
         * corresponds to one of the ERROR_* constants.
         *
         * @param errorCode The error code corresponding to an ERROR_* value.
         */
        void onReceivedError(int errorCode);
    }


    interface StateChangeListener {
        void onStateChanged(@State int state);
    }

    private class StateHandler {
        private final List<StateChangeListener> stateChangeListeners;

        @State
        private int state;

        StateHandler() {
            state = State.NOT_STARTED;
            stateChangeListeners = new ArrayList<>();
        }

        void setState(@State int newState) {
            if (newState != state) {
                this.state = newState;
                for (StateChangeListener stateChangeListener : new ArrayList<>(stateChangeListeners))
                    stateChangeListener.onStateChanged(state);
            }
        }

        @State
        int getState() {
            return state;
        }

        void addListener(StateChangeListener listener) {
            stateChangeListeners.add(listener);
        }

        void removeListener(StateChangeListener listener) {
            stateChangeListeners.remove(listener);
        }
    }

    /**
     * WebViewClient subclass, which can handle more than one pageFinishListener registration
     */
    private class ZerokitWebViewClientBase extends WebViewClient {
        /**
         * The registered PageFinishListeners
         */
        final List<PageFinishListener> pageFinishListeners;

        /**
         * Constructs a ZerokitWebViewClient
         */
        ZerokitWebViewClientBase() {
            pageFinishListeners = new LinkedList<>();
        }

        /**
         * Register a PageFinishListener
         *
         * @param pageFinishListener which will be registered
         */
        void addPageFinishListener(@NonNull PageFinishListener pageFinishListener) {
            pageFinishListeners.add(pageFinishListener);
        }


        /**
         * Remove a registered a PageFinishListener
         *
         * @param pageFinishListener which will be removed
         */
        void removePageFinishListener(@NonNull PageFinishListener pageFinishListener) {
            pageFinishListeners.remove(pageFinishListener);
        }

        /**
         * Notify the host application that a page has finished loading. This method
         * is called only for main frame. When onPageFinished() is called, the
         * rendering picture may not be updated yet. To get the notification for the
         * new Picture, use {@link WebView.PictureListener#onNewPicture}.
         *
         * @param view The WebView that is initiating the callback.
         * @param url  The url of the page.
         */
        @Override
        public void onPageFinished(@NonNull final WebView view, @NonNull final String url) {
            super.onPageFinished(view, url);
            for (PageFinishListener pageFinishListener : new LinkedList<>(pageFinishListeners)) {
                pageFinishListener.onPageFinished(url);
            }
        }

        /**
         * Report an error to the host application. These errors are unrecoverable
         * (i.e. the main resource is unavailable). The errorCode parameter
         * corresponds to one of the ERROR_* constants.
         *
         * @param view        The WebView that is initiating the callback.
         * @param errorCode   The error code corresponding to an ERROR_* value.
         * @param description A String describing the error.
         * @param failingUrl  The url that failed to load.
         */
        @Override
        public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
            super.onReceivedError(view, errorCode, description, failingUrl);
            for (PageFinishListener pageFinishListener : new LinkedList<>(pageFinishListeners)) {
                pageFinishListener.onReceivedError(errorCode);
            }
        }

    }

    /**
     * WebChromeClient implementation which is responsible for handle console errors
     */
    private class ZerokitWebChromeClient extends WebChromeClient {

        /**
         * Package private constructor to avoid method generation to access private constructor of inner class
         */
        ZerokitWebChromeClient() {
        }

        /**
         * Report a JavaScript console message to the host application. The ChromeClient
         * should override this to process the log message as they see fit.
         *
         * @param consoleMessage Object containing details of the console message.
         * @return true if the message is handled by the client.
         */
        @Override
        public boolean onConsoleMessage(@NonNull ConsoleMessage consoleMessage) {
            if (consoleMessage.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
                for (Callback<? super String, ? super String> callback : observers.values())
                    callback.onError(new ResponseZerokitError(!isNetworkAvailable() ? "No network connection" : consoleMessage.message()).toJSON());
                observers.clear();
            }
            return true;
        }
    }

    private abstract class CallbackByteArrayIds<T> implements Callback<String, String> {

        String[] ids = null;

        /**
         * the subscriber which will handle the results
         */
        final Callback<? super T, ? super ResponseZerokitError> subscriber;

        private CallbackByteArrayIds(@NonNull Callback<? super T, ? super ResponseZerokitError> subscriber) {
            this(subscriber, (String) null);
        }

        private CallbackByteArrayIds(@NonNull Callback<? super T, ? super ResponseZerokitError> subscriber, String... ids) {
            this.ids = ids;
            this.subscriber = subscriber;
        }

        @Override
        public void onError(@NonNull String e) {
            Zerokit.this.log("onError " + e);
            if (ids != null)
                for (String id : ids) jsInterfaceByteArrayProvider.remove(id);
            subscriber.onError(new ResponseZerokitError().parse(e));
        }

        @Override
        public void onSuccess(@NonNull String result) {
            Zerokit.this.log("onSuccess " + result);
            if (ids != null)
                for (String id : ids) jsInterfaceByteArrayProvider.remove(id);
            subscriber.onSuccess(getResult(result));
        }

        abstract T getResult(String result);
    }

    /**
     * Observer implementation for handle JSON responses
     *
     * @param <T> the concrete JSON response type
     */
    private class CallbackJsonResult<T extends ZerokitJson> extends CallbackByteArrayIds<T> {

        private final T response;

        /**
         * Constructs an observer object which can handle JSON responses
         *
         * @param subscriber which will handle the responses
         * @param response   the response object
         */
        CallbackJsonResult(@NonNull Callback<? super T, ? super ResponseZerokitError> subscriber, @NonNull T response) {
            super(subscriber);
            this.response = response;
        }

        CallbackJsonResult(@NonNull Callback<? super T, ? super ResponseZerokitError> subscriber, @NonNull T response, String... ids) {
            super(subscriber, ids);
            this.response = response;
        }

        @Override
        @NonNull
        T getResult(@NonNull String result) {
            return response.parse(result);
        }
    }

    /**
     * Observer implementation for handle String responses
     */
    private class CallbackStringResult extends CallbackByteArrayIds<String> {

        /**
         * Constructs an observer object which can handle JSON responses
         *
         * @param subscriber which will handle the responses
         */
        CallbackStringResult(@NonNull Callback<? super String, ? super ResponseZerokitError> subscriber) {
            super(subscriber);
        }

        CallbackStringResult(@NonNull Callback<? super String, ? super ResponseZerokitError> subscriber, String... ids) {
            super(subscriber, ids);
        }

        @Override
        @NonNull
        String getResult(@NonNull String result) {
            if (!"null".equals(result)) try {
                return (String) new JSONTokener(result).nextValue();
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    private class CallAction<T, S> extends CallAsyncAction<T, S> implements Call<T, S> {

        CallAction(ActionCallback<T, S> action) {
            super(action);
        }

        void checkThreadSync() {
            if (isMainThread() && initStateHandler.getState() != State.FINISHED)
                throw new IllegalStateException("Sync method call from the main thread is only possible after the initialization was finished." +
                        "You can call sync method from background thread any time, or from main thread after the initialization is done." +
                        "You can call async method from any looper thread any time");
        }

        @Override
        public final void enqueue(final Callback<? super T, ? super S> callback) {
            if (Looper.myLooper() == null)
                throw new IllegalStateException("Asynchronous method only possible from looper threads.");
            final Handler handler = new Handler(Looper.myLooper());
            executorBackground.execute(new Runnable() {
                @Override
                public void run() {
                    action.call(new Callback<T, S>() {
                        @Override
                        public void onSuccess(final T result) {
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onSuccess(result);
                                }
                            });
                        }

                        @Override
                        public void onError(final S e) {
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    callback.onError(e);
                                }
                            });
                        }
                    });
                }
            });
        }

        @Override
        public final Response<T, S> execute() {
            checkThreadSync();

            final Holder<Response<T, S>> result = new Holder<>();
            final CountDownLatch signal = new CountDownLatch(1);

            action.call(new Callback<T, S>() {
                @Override
                public void onSuccess(T t) {
                    result.t = Response.fromValue(t);
                    signal.countDown();
                }

                @Override
                public void onError(S e) {
                    result.t = Response.fromError(e);
                    signal.countDown();
                }
            });
            if (signal.getCount() > 0)
                try {
                    signal.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            return result.t;
        }
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     *
     * @param userId   The userId of the user to log in.
     * @param password The password of the user to log in.
     * @return Resolved userId of the logged in user.
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    CallAsync<ResponseZerokitLogin, ResponseZerokitError> _login(@NonNull final String userId, @NonNull final byte[] password) {
        return new CallAsyncAction<>(new ActionCallback<ResponseZerokitLogin, ResponseZerokitError>() {

            @Override
            public void call(final Callback<? super ResponseZerokitLogin, ? super ResponseZerokitError> subscriber) {
                callFunction(Function.login, new CallbackJsonResult<>(subscriber, new ResponseZerokitLogin(), jsInterfaceByteArrayProvider.add(password)), userId);
            }
        });
    }

    /**
     * Use this method for login if 'remember me' was set to yes for a previous login with password.
     *
     * @param userId The user ID to log in with
     * @param key    The remember key
     * @return Resolved userId of the logged in user.
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    CallAsync<String, ResponseZerokitError> _loginByRememberMeKey(@NonNull final String userId, @NonNull final String key) {
        return new CallAsyncAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.loginByRememberMeKey, new CallbackStringResult(subscriber), userId, key);
            }
        });
    }

    /**
     * Returns the "remember key" for the given user
     *
     * @param userId   The user ID for the key
     * @param password The password for the given user id
     * @return the key
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    CallAsync<String, ResponseZerokitError> _getRememberMeKey(@NonNull final String userId, @NonNull final byte[] password) {
        return new CallAsyncAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                callFunction(Function.getRememberMeKey, new CallbackStringResult(subscriber, jsInterfaceByteArrayProvider.add(password)), userId);
            }
        });
    }


    /**
     * Log out the current user
     *
     * @return Observable about the results
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    CallAsync<String, ResponseZerokitError> _logout() {
        return new CallAsyncAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.logout, new CallbackStringResult(subscriber));
            }
        });
    }

    /**
     * Use this methods to get the logged in user's identity.
     *
     * @return the user ID if logged in or `null` if not.
     */
    @NonNull
    CallAsync<String, ResponseZerokitError> _whoAmI() {
        return new CallAsyncAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.whoAmI, new CallbackStringResult(subscriber));
            }
        });
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param userId      Parameter to specify the id of the user changing password. This is only required if the user is not logged in.
     * @param oldPassword The currently used password
     * @param newPassword The new password
     * @return Resolves to the userId of the logged in user
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    CallAsync<ResponseZerokitChangePassword, ResponseZerokitError> _changePassword(@NonNull final String userId, @NonNull final byte[] oldPassword, @NonNull final byte[] newPassword) {
        return new CallAsyncAction<>(new ActionCallback<ResponseZerokitChangePassword, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitChangePassword, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.changePassword, new CallbackJsonResult<>(subscriber, new ResponseZerokitChangePassword(), jsInterfaceByteArrayProvider.add(oldPassword), jsInterfaceByteArrayProvider.add(newPassword)), userId);
            }
        });
    }


    //::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

    // ====================================
    //  Identity tokens
    // ====================================

    /**
     * Get authorization code and identity tokens for the currenty logged in user.
     *
     * @param clientId The cliend ID for the current ZeroKit OpenID Connect client set up in the management portal.
     * @return IdentityTokens which contains: Authorization code, Identity token, Code Verifier (Contains the code verifier if you have 'Requires proof key' enabled for your client)
     */
    @NonNull
    @SuppressWarnings("WeakerAccess")
    public Call<IdentityTokens, ResponseZerokitError> getIdentityTokens(final String clientId) {
        return getIdentityTokens(clientId, false);
    }

    /**
     * Get authorization code and identity tokens for the currenty logged in user.
     *
     * @param clientId    The cliend ID for the current ZeroKit OpenID Connect client set up in the management portal.
     * @param useProofKey Option to use proof key
     * @return IdentityTokens which contains: Authorization code, Identity token, Code Verifier (Contains the code verifier if you have 'Requires proof key' enabled for your client)
     */
    @NonNull
    private Call<IdentityTokens, ResponseZerokitError> getIdentityTokens(final String clientId, final boolean useProofKey) {
        return new CallAction<IdentityTokens, ResponseZerokitError>(new ActionCallback<IdentityTokens, ResponseZerokitError>() {

            @Override
            public void call(final Callback<? super IdentityTokens, ? super ResponseZerokitError> subscriberInner) {
                initIdpCheck().enqueue(new Action<Void>() {
                    @Override
                    public void call(Void aVoid) {
                        idpStateHandler.setState(State.RUNNING);

                        requireIdp(clientId, useProofKey, new Callback<IdentityTokens, ResponseZerokitError>() {
                            @Override
                            public void onSuccess(IdentityTokens result) {
                                subscriberInner.onSuccess(result);
                                idpStateHandler.setState(State.FINISHED);
                            }

                            @Override
                            public void onError(ResponseZerokitError e) {
                                subscriberInner.onError(e);
                                idpStateHandler.setState(State.FINISHED);
                            }
                        });
                    }
                });
            }
        }) {
            @Override
            protected void checkThreadSync() {
                if (isMainThread())
                    throw new IllegalStateException("Sync IDP method call from the main thread is not possible." +
                            "You can call sync method from background thread." +
                            "You can call async method from any looper thread any time");
            }
        };
    }

    // ====================================
    //  Password
    // ====================================

    /**
     * This methods gives meta-information about the password the user entered
     *
     * @param password The password to get the strength of it
     * @return Part of the result of running zxcvbn on the password
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitPasswordStrength, ResponseZerokitError> getPasswordStrength(@NonNull final byte[] password) {
        return new CallAction<>(new ActionCallback<ResponseZerokitPasswordStrength, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitPasswordStrength, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.zxcvbn, new CallbackJsonResult<>(subscriber, new ResponseZerokitPasswordStrength(), jsInterfaceByteArrayProvider.add(password)));
            }
        });
    }

    /**
     * This methods gives meta-information about the password the user entered
     *
     * @param passwordExporter The password to get the strength of it
     * @return Part of the result of running zxcvbn on the password
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitPasswordStrength, ResponseZerokitError> getPasswordStrength(@NonNull final PasswordEditText.PasswordExporter passwordExporter) {
        return getPasswordStrength(toBytes(passwordExporter.getCharArray(false)));
    }

    /**
     * This methods gives meta-information about the password the user entered
     *
     * @param passwordEditText The password to get the strength of it
     * @return Part of the result of running zxcvbn on the password
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitPasswordStrength, ResponseZerokitError> getPasswordStrength(@NonNull final PasswordEditText passwordEditText) {
        return getPasswordStrength(passwordEditText.getPasswordExporter());
    }

    // ====================================
    //  Invitation links
    // ====================================

    /**
     * A link with no password can be accepted by any logged in user that has access to the token returned by getInvitationLinkInfo through the basic sdk.
     *
     * @param token The token is the token field of the InvitationLinkPublicInfo of the link returned by getInvitationLinkInfo.
     * @return Resolves to the operation id that must be approved for the operation to be effective.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> acceptInvitationLinkNoPassword(@NonNull final String token) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.acceptInvitationLinkNoPassword, new CallbackStringResult(subscriber, token));
            }
        });
    }


    /**
     * You can get some information about the link by calling getInvitationLinkInfo with the link secret.
     * The returned object contains a token necessary to accept the invitation.
     * This also is a client side secret, that should never be uploaded to your site as that would compromise the zero knowledge nature of the system by providing ways to open the tresor.
     *
     * @param secret The secret is the one that was concatenated to the end of the url in createInvitationLink.
     * @return Resolves to all the information available.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitInvitationLinkInfo, ResponseZerokitError> getInvitationLinkInfo(@NonNull final String secret) {
        return new CallAction<>(new ActionCallback<ResponseZerokitInvitationLinkInfo, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitInvitationLinkInfo, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.getInvitationLinkInfo, new CallbackJsonResult<>(subscriber, new ResponseZerokitInvitationLinkInfo()), secret);
            }
        });
    }

    /**
     * Invitation links can be revoked, but to do this the user has to be both a member of the tresor and have the link secret.
     * Revokes the link from the tresor with the secret provided
     *
     * @param tresorId the id of the tresor
     * @param secret The secret is the one that was concatenated to the end of the url in createInvitationLink.
     * @return Resolves to the operation id that must be approved for the operation to be effective.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> revokeInvitationLink(@NonNull final String tresorId, @NonNull final String secret) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.revokeInvitationLink, new CallbackStringResult(subscriber), tresorId, secret);
            }
        });
    }


    /**
     * You can create an invitation link with no password.
     *
     * @param linkBase the base of the link. The link secret is concatenated after this after a '#'
     * @param tresorId the id of the tresor
     * @param message  optional arbitrary string data that can be retrieved without a password or any other information
     * @return Resolves to the operation id and the url of the created link. The operation must be approved before the link is enabled.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitCreateInvitationLink, ResponseZerokitError> createInvitationLinkNoPassword(@NonNull final String linkBase, @NonNull final String tresorId, @Nullable final String message) {
        return new CallAction<>(new ActionCallback<ResponseZerokitCreateInvitationLink, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitCreateInvitationLink, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.createInvitationLinkNoPassword, new CallbackJsonResult<>(subscriber, new ResponseZerokitCreateInvitationLink()), linkBase, tresorId, TextUtils.isEmpty(message) ? "" : message);
            }
        });
    }

    /**
     * This method creates an invitation link with the password entered
     *
     * @param linkBase the base of the link. The link secret is concatenated after this after a '#'
     * @param tresorId the id of the tresor
     * @param message  optional arbitrary string data that can be retrieved without a password or any other information
     * @param password the password to accept the link
     * @return Resolves to the operation id and the url of the created link. The operation must be approved before the link is enabled.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitCreateInvitationLink, ResponseZerokitError> createInvitationLink(@NonNull final String linkBase, @NonNull final String tresorId, @Nullable final String message, @NonNull final byte[] password) {
        return new CallAction<>(new ActionCallback<ResponseZerokitCreateInvitationLink, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitCreateInvitationLink, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.createInvitationLink, new CallbackJsonResult<>(subscriber, new ResponseZerokitCreateInvitationLink(), jsInterfaceByteArrayProvider.add(password)), linkBase, tresorId, TextUtils.isEmpty(message) ? "" : message);
            }
        });
    }

    /**
     * This method creates an invitation link with the password entered
     *
     * @param linkBase         the base of the link. The link secret is concatenated after this after a '#'
     * @param tresorId         the id of the tresor
     * @param message          optional arbitrary string data that can be retrieved without a password or any other information
     * @param passwordExporter the passwordexporter that holds the password to accept the link
     * @return Resolves to the operation id and the url of the created link. The operation must be approved before the link is enabled.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitCreateInvitationLink, ResponseZerokitError> createInvitationLink(@NonNull final String linkBase, @NonNull final String tresorId, @Nullable final String message, @NonNull PasswordEditText.PasswordExporter passwordExporter) {
        return createInvitationLink(linkBase, tresorId, message, toBytes(passwordExporter.getCharArray(true)));
    }

    /**
     * This method creates an invitation link with the password entered
     *
     * @param linkBase         the base of the link. The link secret is concatenated after this after a '#'
     * @param tresorId         the id of the tresor
     * @param message          optional arbitrary string data that can be retrieved without a password or any other information
     * @param passwordEditText the passwordEditText that holds the password to accept the link
     * @return Resolves to the operation id and the url of the created link. The operation must be approved before the link is enabled.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitCreateInvitationLink, ResponseZerokitError> createInvitationLink(@NonNull final String linkBase, @NonNull final String tresorId, @Nullable final String message, @NonNull PasswordEditText passwordEditText) {
        return createInvitationLink(linkBase, tresorId, message, passwordEditText.getPasswordExporter());
    }

    /**
     * This method will add the user to the tresor of the link using the password entered.
     *
     * @param token    The token is the $token field of the InvitationLinkPublicInfo of the link returned by getInvitationLinkInfo.
     * @param password The password for the link
     * @return Resolves to the operation id that must be approved for the operation to be effective.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> acceptInvitationLink(@NonNull final String token, @NonNull final byte[] password) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.acceptInvitationLink, new CallbackStringResult(subscriber, token, jsInterfaceByteArrayProvider.add(password)));
            }
        });
    }


    /**
     * This method will add the user to the tresor of the link using the password entered.
     *
     * @param token            The token is the $token field of the InvitationLinkPublicInfo of the link returned by getInvitationLinkInfo.
     * @param passwordExporter The passwordexporter that holds the password for the link
     * @return Resolves to the operation id that must be approved for the operation to be effective.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> acceptInvitationLink(@NonNull final String token, PasswordEditText.PasswordExporter passwordExporter) {
        return acceptInvitationLink(token, toBytes(passwordExporter.getCharArray(true)));
    }

    /**
     * This method will add the user to the tresor of the link using the password entered.
     *
     * @param token            The token is the $token field of the InvitationLinkPublicInfo of the link returned by getInvitationLinkInfo.
     * @param passwordEditText The passwordEditText that holds the password for the link
     * @return Resolves to the operation id that must be approved for the operation to be effective.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> acceptInvitationLink(@NonNull final String token, PasswordEditText passwordEditText) {
        return acceptInvitationLink(token, passwordEditText.getPasswordExporter());
    }

    // ====================================
    //  User handling
    // ====================================

    /**
     * Registers the user in ZKit with the provided userId.
     * The returned value is the regValidationVerifier which is used during user validation, so it should be saved on the app server.
     *
     * @param userId       The userId provided by the InitUserRegistration API call for the given alias
     * @param regSessionId The regSessionId provided by the InitUserRegistration API call for the given alias
     * @param password     The password provided by the User
     * @return the RegValidationVerifier property.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitRegister, ResponseZerokitError> register(@NonNull final String userId, @NonNull final String regSessionId, @NonNull final byte[] password) {
        return new CallAction<>(new ActionCallback<ResponseZerokitRegister, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super ResponseZerokitRegister, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.register, new CallbackJsonResult<>(subscriber, new ResponseZerokitRegister(), jsInterfaceByteArrayProvider.add(password)), userId, regSessionId);
            }
        });
    }

    /**
     * Registers the user in ZKit with the provided userId
     * The returned value is the regValidationVerifier which is used during user validation, so it should be saved on the app server.
     *
     * @param userId           The userId provided by the InitUserRegistration API call for the given alias
     * @param regSessionId     The regSessionId provided by the InitUserRegistration API call for the given alias
     * @param passwordEditText field that holds the password provided by the user
     * @return the RegValidationVerifier property.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitRegister, ResponseZerokitError> register(@NonNull final String userId, @NonNull final String regSessionId, @NonNull PasswordEditText passwordEditText) {
        return register(userId, regSessionId, passwordEditText.getPasswordExporter());
    }

    /**
     * Registers the user in ZKit with the provided userId
     * The returned value is the regValidationVerifier which is used during user validation, so it should be saved on the app server.
     *
     * @param userId           The userId provided by the InitUserRegistration API call for the given alias
     * @param regSessionId     The regSessionId provided by the InitUserRegistration API call for the given alias
     * @param passwordExporter exporter that holds the password provided by the User
     * @return the RegValidationVerifier property.
     */
    @NonNull
    public Call<ResponseZerokitRegister, ResponseZerokitError> register(@NonNull final String userId, @NonNull final String regSessionId, @NonNull PasswordEditText.PasswordExporter passwordExporter) {
        return register(userId, regSessionId, toBytes(passwordExporter.getCharArray(true)));
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     *
     * @param userId     The userId of the user to log in.
     * @param password   The password of the user to log in.
     * @param rememberMe If true, than next time the login without password will be possible
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final byte[] password, final boolean rememberMe) {
        return new CallAction<>(new ActionCallback<ResponseZerokitLogin, ResponseZerokitError>() {
            @Override
            public void call(final Callback<? super ResponseZerokitLogin, ? super ResponseZerokitError> subscriber) {
                _login(userId, password).enqueue(new Action<ResponseZerokitLogin>() {
                    @Override
                    public void call(final ResponseZerokitLogin responseLogin) {
                        if (rememberMe)
                            _getRememberMeKey(userId, password).enqueue(new Action<String>() {
                                @Override
                                public void call(String rememberKey) {
                                    storeSecret(KEY_STORE_ALIAS, rememberKey);
                                    subscriber.onSuccess(responseLogin);
                                }
                            }, new Action<ResponseZerokitError>() {
                                @Override
                                public void call(ResponseZerokitError responseZerokitError) {
                                    subscriber.onSuccess(responseLogin);
                                }
                            });
                        else
                            subscriber.onSuccess(responseLogin);
                        Arrays.fill(password, (byte) 0);
                    }
                }, new Action<ResponseZerokitError>() {
                    @Override
                    public void call(ResponseZerokitError responseZerokitError) {
                        Arrays.fill(password, (byte) 0);
                        subscriber.onError(responseZerokitError);
                    }
                });
            }
        });
    }

    /**
     * This method tries to log in the given user with the stored remember me key
     *
     * @param userId The userId of the user to log in.
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> login(@NonNull final String userId) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(final Callback<? super String, ? super ResponseZerokitError> subscriber) {
                String secret = getSecret(KEY_STORE_ALIAS);
                if (secret == null)
                    subscriber.onError(new ResponseZerokitError("No secret found"));
                else
                    _loginByRememberMeKey(userId, secret).enqueue(new Action<String>() {
                        @Override
                        public void call(String s) {
                            subscriber.onSuccess(s);
                        }
                    }, new Action<ResponseZerokitError>() {
                        @Override
                        public void call(ResponseZerokitError responseZerokitError) {
                            subscriber.onError(responseZerokitError);
                        }
                    });
            }
        });
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     * The logged in user will not be remembered by default
     *
     * @param userId        The userId of the user to log in.
     * @param passwordField field that stores password of the user to log in.
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final PasswordEditText passwordField) {
        return login(userId, passwordField, false);
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     * The logged in user will not be remembered by default
     *
     * @param userId           The userId of the user to log in.
     * @param passwordExporter exporter that stores password of the user to log in.
     * @return Resolved userId of the logged in user.
     */
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final PasswordEditText.PasswordExporter passwordExporter) {
        return login(userId, passwordExporter, false);
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     * The logged in user will not be remembered by default
     *
     * @param userId   The userId of the user to log in.
     * @param password The password of the user to log in.
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final byte[] password) {
        return login(userId, password, false);
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     *
     * @param userId        The userId of the user to log in.
     * @param passwordField field that stores the password of the user to log in.
     * @param rememberMe    If true, than next time the login without password will be possible
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final PasswordEditText passwordField, final boolean rememberMe) {
        return login(userId, passwordField.getPasswordExporter(), rememberMe);
    }

    /**
     * This method tries to log in the given user with the given password entered by the user
     *
     * @param userId           The userId of the user to log in.
     * @param passwordExporter exporter that stores the password of the user to log in.
     * @param rememberMe       If true, than next time the login without password will be possible
     * @return Resolved userId of the logged in user.
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitLogin, ResponseZerokitError> login(@NonNull final String userId, @NonNull final PasswordEditText.PasswordExporter passwordExporter, final boolean rememberMe) {
        return login(userId, toBytes(passwordExporter.getCharArray(true)), rememberMe);
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param userId      Optional parameter to specify the id of the user changing password. This is only required if the user is not logged in.
     * @param oldPassword The currently used password
     * @param newPassword The new password
     * @return Resolves to the userId of the logged in user
     * <p>
     * InvalidAuthorization    - Invalid username or password
     * UserNameDoesntExist	   - The user does not exist
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@Nullable final String userId, @NonNull final byte[] oldPassword, @NonNull final byte[] newPassword) {
        return new CallAction<>(new ActionCallback<ResponseZerokitChangePassword, ResponseZerokitError>() {
            @Override
            public void call(final Callback<? super ResponseZerokitChangePassword, ? super ResponseZerokitError> subscriber) {
                final Action<ResponseZerokitError> onFail = new Action<ResponseZerokitError>() {
                    @Override
                    public void call(ResponseZerokitError responseZerokitError) {
                        Arrays.fill(newPassword, (byte) 0);
                        Arrays.fill(oldPassword, (byte) 0);
                        subscriber.onError(responseZerokitError);
                    }
                };
                Action<String> onSuccessWhoAmI = new Action<String>() {
                    @Override
                    public void call(final String userId_) {
                        _changePassword(userId_, oldPassword, newPassword).enqueue(new Action<ResponseZerokitChangePassword>() {
                            @Override
                            public void call(final ResponseZerokitChangePassword changePasswordResult) {
                                if (getSecret(KEY_STORE_ALIAS) != null) {
                                    _getRememberMeKey(userId_, newPassword).enqueue(new Action<String>() {
                                        @Override
                                        public void call(String rememberKey) {
                                            storeSecret(KEY_STORE_ALIAS, rememberKey);
                                            subscriber.onSuccess(changePasswordResult);
                                        }
                                    }, new Action<ResponseZerokitError>() {
                                        @Override
                                        public void call(ResponseZerokitError responseZerokitError) {
                                            subscriber.onSuccess(changePasswordResult);
                                        }
                                    });
                                } else subscriber.onSuccess(changePasswordResult);
                                Arrays.fill(newPassword, (byte) 0);
                                Arrays.fill(oldPassword, (byte) 0);
                            }
                        }, onFail);
                    }
                };
                if (TextUtils.isEmpty(userId))
                    _whoAmI().enqueue(onSuccessWhoAmI, onFail);
                else onSuccessWhoAmI.call(userId);
            }
        });
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param oldPassword The currently used password
     * @param newPassword The new password
     * @return Resolves to the userId of the logged in user
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@NonNull final byte[] oldPassword, @NonNull final byte[] newPassword) {
        return changePassword(null, oldPassword, newPassword);
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param userId              Optional parameter to specify the id of the user changing password. This is only required if the user is not logged in.
     * @param passwordEditTextOld The currently used password
     * @param passwordEditTextNew The new password
     * @return Resolves to the userId of the logged in user
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@NonNull final String userId, @NonNull final PasswordEditText passwordEditTextOld, @NonNull PasswordEditText passwordEditTextNew) {
        return changePassword(userId, passwordEditTextOld.getPasswordExporter(), passwordEditTextNew.getPasswordExporter());
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param passwordEditTextOld The currently used password
     * @param passwordEditTextNew The new password
     * @return Resolves to the userId of the logged in user
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@NonNull final PasswordEditText passwordEditTextOld, @NonNull PasswordEditText passwordEditTextNew) {
        return changePassword(passwordEditTextOld.getPasswordExporter(), passwordEditTextNew.getPasswordExporter());
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param userId              Optional parameter to specify the id of the user changing password. This is only required if the user is not logged in.
     * @param passwordExporterOld The currently used password
     * @param passwordExporterNew The new password
     * @return Resolves to the userId of the logged in user
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@NonNull final String userId, @NonNull final PasswordEditText.PasswordExporter passwordExporterOld, @NonNull PasswordEditText.PasswordExporter passwordExporterNew) {
        return changePassword(userId, toBytes(passwordExporterOld.getCharArray(true)), toBytes(passwordExporterNew.getCharArray(true)));
    }

    /**
     * This method logs into a security session and changes the password of the user.
     *
     * @param passwordExporterOld The currently used password
     * @param passwordExporterNew The new password
     * @return Resolves to the userId of the logged in user
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<ResponseZerokitChangePassword, ResponseZerokitError> changePassword(@NonNull final PasswordEditText.PasswordExporter passwordExporterOld, @NonNull PasswordEditText.PasswordExporter passwordExporterNew) {
        return changePassword(toBytes(passwordExporterOld.getCharArray(true)), toBytes(passwordExporterNew.getCharArray(true)));
    }

    /**
     * Log out the current user
     *
     * @param deleteRememberMe If true, after logout the remember me key will be deleted
     * @return the results
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> logout(final boolean deleteRememberMe) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(final Callback<? super String, ? super ResponseZerokitError> subscriber) {
                if (deleteRememberMe)
                    _whoAmI().enqueue(new Action<String>() {
                        @Override
                        public void call(String userId) {
                            deleteSecret(KEY_STORE_ALIAS);
                            _logout().enqueue(new Action<String>() {
                                @Override
                                public void call(String s) {
                                    subscriber.onSuccess(s);
                                }
                            }, new Action<ResponseZerokitError>() {
                                @Override
                                public void call(ResponseZerokitError responseZerokitError) {
                                    subscriber.onError(responseZerokitError);
                                }
                            });
                        }
                    }, new Action<ResponseZerokitError>() {
                        @Override
                        public void call(ResponseZerokitError responseZerokitError) {
                            subscriber.onError(responseZerokitError);
                        }
                    });
                else
                    _logout().enqueue(new Action<String>() {
                        @Override
                        public void call(String s) {
                            subscriber.onSuccess(s);
                        }
                    }, new Action<ResponseZerokitError>() {
                        @Override
                        public void call(ResponseZerokitError responseZerokitError) {
                            subscriber.onError(responseZerokitError);
                        }
                    });
            }
        });
    }

    /**
     * Log out the current user
     *
     * @return Observable about the results
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> logout() {
        return logout(true);
    }

    /**
     * Use this methods to get the logged in user's identity.
     *
     * @return the user ID if logged in or `null` if not.
     */
    @NonNull
    public Call<String, ResponseZerokitError> whoAmI() {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.whoAmI, new CallbackStringResult(subscriber));
            }
        });
    }

    // ====================================
    //  Tresors
    // ====================================

    /**
     * Tresors are the basic unit of key handling and sharing.
     * They can be referenced by a server generated id, returned on tresor creation.
     * We currently provide no means to list a user's tresors, so the application should save these ids.
     * Both tresor creation and sharing needs administrative approval to be effective.
     * Since the encrypted data has the tresor id included, it can be decrypted even if the tresorId is lost from the application database.
     *
     * @return Resolves to the tresorId of the newly created tresor. This id can be used to approve the tresor creation and to encrypt/decrypt using the tresor.
     */
    @NonNull
    public Call<String, ResponseZerokitError> createTresor() {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.createTresor, new CallbackStringResult(subscriber));
            }
        });
    }

    /**
     * The shareTresor method will share the tresor with the given user.
     * The operation will only be effective after it is approved using the returned OperationId.
     * This uploads a modified tresor, but the new version is downloadable only after it has been approved.
     * This should be done as soon as possible, as approving any operation to a tresor may invalidate any pending ones.
     *
     * @param tresorId The id of the tresor to invite the user to.
     * @param userId   The id of the user to invite. Important to notice, that this is not an alias.
     * @return Resolves to the OperationId that can be used to approve this share.
     * <p>
     * BadInput	        - Invalid tresor or userId
     * TresorNotExists  - Couldn't find a tresor by the give tresorId
     * Forbidden	    - This user does not have access to the tresor
     * UserNotFound     - There is no user by that id
     */
    @NonNull
    public Call<String, ResponseZerokitError> shareTresor(@NonNull final String tresorId, @NonNull final String userId) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.shareTresor, new CallbackStringResult(subscriber), tresorId, userId);
            }
        });
    }

    /**
     * Removes the given user from the tresor. The operation will only be effective after it is approved using the returned OperationId.
     *
     * @param tresorId The id of the tresor, which from the user will be kicked out
     * @param userId   The id of the user, who will be kicked out
     * @return Resolves to the operation id. The operation must be approved before the user is kicked out.
     * <p>
     * InvalidAuthorization     - Invalid username or password
     * UserNameDoesntExist	    - The user does not exist
     */
    @SuppressWarnings("WeakerAccess")
    @NonNull
    public Call<String, ResponseZerokitError> kickFromTresor(@NonNull final String tresorId, @NonNull final String userId) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.kickFromTresor, new CallbackStringResult(subscriber), tresorId, userId);
            }
        });
    }


    // ====================================
    //  Encryption/Decryption
    // ====================================

    /**
     * Decrypts the given cipherText
     *
     * @param cipherText ZeroKit encrypted text
     * @return Resolves to the plain text.
     * <p>
     * BadInput 	- Invalid cipherText
     * Forbidden	- This user does not have access to the tresor
     */
    @NonNull
    public Call<String, ResponseZerokitError> decrypt(@NonNull final String cipherText) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.decrypt, new CallbackStringResult(subscriber), cipherText);
            }
        });
    }

    /**
     * Encrypts the plaintext by the given tresor.
     *
     * @param tresorId  The id of the tresor, that will be used to encrypt the text
     * @param plainText The plainText to encrypt
     * @return Resolves to the cipher text. It contains the tresorId, so the it can be decrypted by itself.
     * <p>
     * BadInput         - The tresorId and plainText has to be a non-empty string
     * BadInput         - Invalid tresorId
     * TresorNotExists  - Couldn't find a tresor by the given id
     * Forbidden	    - This user does not have access to the tresor
     */
    @NonNull
    public Call<String, ResponseZerokitError> encrypt(@NonNull final String tresorId, @NonNull final String plainText) {
        return new CallAction<>(new ActionCallback<String, ResponseZerokitError>() {
            @Override
            public void call(Callback<? super String, ? super ResponseZerokitError> subscriber) {
                Zerokit.this.callFunction(Function.encrypt, new CallbackStringResult(subscriber), tresorId, plainText);
            }
        });
    }

} public interface Action<T> {
    void call(T t);
} public class ResponseAdminApiError {
    private String code;
    private String message;
    private ResponseExtensionException exception;

    public ResponseAdminApiError(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public String toString() {
        return String.format("code: %s, message: %s", code, message);
    }
} public class ResponseAdminApiInitUserRegistration  {
    private String userId;
    private String regSessionId;

    public String getUserId() {
        return userId;
    }

    public String getRegSessionId() {
        return regSessionId;
    }


} public class ResponseZerokitError extends ZerokitJson {
    private String type;
    private String code;
    private String message;
    private String description;
    private final ResponseZerokitInternalException internalException;

    public ResponseZerokitError(String type, String code, String message, String description) {
        this.type = type;
        this.code = code;
        this.message = message;
        this.description = description;
        this.internalException = new ResponseZerokitInternalException();
    }

    @Override
    public String toString() {
        return String.format("type: %s, code: %s, message: %s, description: %s", getType(), getCode(), getMessage(), getDescription());
    }

    public ResponseZerokitError(String description) {
        this("", "", "", description);
    }

    public ResponseZerokitError(String message, String description) {
        this("", "", message, description);
    }

    public ResponseZerokitError() {
        this("", "", "", "");
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getType() {
        return type;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getDescription() {
        return description;
    }

    public String toJSON() {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("description", description);
        jsonObject.put("code", code);
        jsonObject.put("message", message);
        jsonObject.put("type", type);
        return jsonObject.toString();
    }

    @SuppressWarnings("unchecked")
    @Override
    public ResponseZerokitError parse(String json) {
        JSONObject jsonobject = new JSONObject(json);
        description = jsonobject.getString("description");
        type = jsonobject.getString("type");
        code = jsonobject.getString("code");
        message = jsonobject.getString("message");
        internalException.parse(jsonobject.getJSONObject("internalException").toString());
        return this;
    }

} public class ResponseZerokitPasswordStrength extends ZerokitJson {
    private CrackTimesSeconds crack_times_seconds;
    private Feedback feedback;
    private double guesses_log10;
    private int length;
    private int score;

    public CrackTimesSeconds getCrack_times_seconds() {
        return crack_times_seconds;
    }

    public double getGuesses_log10() {
        return guesses_log10;
    }

    public Feedback getFeedback() {
        return feedback;
    }

    public int getLength() {
        return length;
    }

    public int getScore() {
        return score;
    }

    @Override
    public String toString() {
        return String.format("crack_times_seconds: %s, feedback: %s, guesses_log10: %s, length: %s, score: %s", crack_times_seconds, feedback, guesses_log10, length, score);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T extends ZerokitJson> T parse(String json) {
        JSONObject jsonObject = new JSONObject(json);
        crack_times_seconds = new CrackTimesSeconds().parse(jsonObject.getJSONObject("crack_times_seconds").toString());
        feedback = new Feedback().parse(jsonObject.getJSONObject("feedback").toString());
        guesses_log10 = jsonObject.getDouble("guesses_log10");
        length = jsonObject.getInt("length");
        score = jsonObject.getInt("score");
        return (T) this;
    }
} public class ResponseZerokitRegister extends ZerokitJson {
    private String RegValidationVerifier;

    public String getRegValidationVerifier() {
        return RegValidationVerifier;
    }

    @SuppressWarnings("unchecked")
    @Override
    public ResponseZerokitRegister parse(String json) {
        try {
            JSONObject jsonobject = new JSONObject(json);
            RegValidationVerifier = jsonobject.getString("RegValidationVerifier");
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return this;
    }

} public class JSONObject {

    private org.json.JSONObject jsonObject;

    public JSONObject() {
        jsonObject = new org.json.JSONObject();
    }

    public JSONObject(String json) {
        try {
            jsonObject = new org.json.JSONObject(json);
        } catch (JSONException e) {
            //e.printStackTrace();
            jsonObject = new org.json.JSONObject();
        }
    }

    public org.json.JSONObject getJSONObject(String name) {
        if (jsonObject != null)
            try {
                return jsonObject.getJSONObject(name);
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return new org.json.JSONObject();
    }


    public String getString(String name) {
        if (jsonObject != null)
            try {
                return jsonObject.getString(name);
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return "";
    }

    public double getDouble(String name) {
        if (jsonObject != null)
            try {
                return jsonObject.getDouble(name);
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return 0;
    }

    public int getInt(String name) {
        if (jsonObject != null)
            try {
                return jsonObject.getInt(name);
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return 0;
    }

    public List<String> getStringArray(String name) {
        if (jsonObject != null)
            try {
                JSONArray jsonArray = jsonObject.getJSONArray(name);
                List<String> result = new ArrayList<>();
                for (int i = 0; i < jsonArray.length(); i++)
                    result.add(jsonArray.getString(i));
                return result;
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return new ArrayList<>();
    }

    public JSONObject put(String name, Object value)  {
        if (jsonObject != null)
            try {
                jsonObject.put(name, value);
            } catch (JSONException e) {
                //e.printStackTrace();
            }
        return this;
    }

    @Override
    public String toString() {
        return jsonObject != null ? jsonObject.toString() : "";
    }
} public class ShowMessageMessage {

    private final String message;

    public ShowMessageMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.databinding.BaseObservable;
import android.databinding.ObservableField;
import android.databinding.ObservableInt;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.text.TextUtils;
import android.view.View;
import com.tresorit.zerokit.AdminApi;
import com.tresorit.zerokit.PasswordEditText;
import com.tresorit.zerokit.Zerokit;
import com.tresorit.zerokit.call.Action;
import com.tresorit.zerokit.response.ResponseAdminApiError;
import com.tresorit.zerokit.response.ResponseAdminApiInitUserRegistration;
import com.tresorit.zerokit.response.ResponseZerokitError;
import com.tresorit.zerokit.response.ResponseZerokitPasswordStrength;
import com.tresorit.zerokit.response.ResponseZerokitRegister;
import com.tresorit.zerokit.util.JSONObject;
import com.tresorit.zerokitsdk.R;
import com.tresorit.zerokitsdk.message.ShowMessageMessage;
import org.greenrobot.eventbus.EventBus;
import javax.inject.Inject;
package com.tresorit.zerokitsdk.viewmodel;





public class RegistrationViewModel extends BaseObservable {

    @SuppressWarnings("WeakerAccess")
    public final ObservableField<String> userName;
    @SuppressWarnings("WeakerAccess")
    public final ObservableField<Boolean> inProgress;
    @SuppressWarnings("WeakerAccess")
    public final ObservableField<String> passwordError;
    @SuppressWarnings("WeakerAccess")
    public final ObservableField<String> passwordConfirmError;
    @SuppressWarnings("WeakerAccess")
    public final ObservableField<String> userNameError;

    @SuppressWarnings("WeakerAccess")
    public final ObservableField<Drawable> seekbarColor;

    @SuppressWarnings("WeakerAccess")
    public final ObservableInt passwordStrength;

    @SuppressWarnings("WeakerAccess")
    public final PasswordEditText.PasswordExporter passwordExporter;
    @SuppressWarnings("WeakerAccess")
    public final PasswordEditText.PasswordExporter passwordExporterConfirm;

    @SuppressWarnings("WeakerAccess")
    final Zerokit zerokit;
    @SuppressWarnings("WeakerAccess")
    final AdminApi adminApi;
    @SuppressWarnings("WeakerAccess")
    final EventBus eventBus;
    @SuppressWarnings("WeakerAccess")
    final SharedPreferences sharedPreferences;

    @SuppressWarnings("WeakerAccess")
    final Action<ResponseAdminApiError> errorResponseHandlerAdmin;
    @SuppressWarnings("WeakerAccess")
    final Action<ResponseZerokitError> errorResponseHandlerSdk;

    @SuppressWarnings("WeakerAccess")
    public final View.OnClickListener clickListenerRegistration;
    @SuppressWarnings("WeakerAccess")
    public final View.OnFocusChangeListener focusChangeListener;

    final int colorRes[];

    @Inject
    public RegistrationViewModel(final Zerokit zerokit, AdminApi adminApi, final EventBus eventBus, SharedPreferences sharedPreferences, final Resources resources) {
        this.zerokit = zerokit;
        this.adminApi = adminApi;
        this.eventBus = eventBus;
        this.sharedPreferences = sharedPreferences;

        inProgress = new ObservableField<>(false);
        userName = new ObservableField<>("");
        passwordError = new ObservableField<>("");
        passwordConfirmError = new ObservableField<>("");
        userNameError = new ObservableField<>("");
        passwordStrength = new ObservableInt();
        seekbarColor = new ObservableField<>(resources.getDrawable(R.drawable.progress));
        colorRes = new int[]{R.color.red, R.color.deep_orange, R.color.orange, R.color.light_green, R.color.green};

        clickListenerRegistration = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                attemptRegistration();
            }
        };
        errorResponseHandlerAdmin = new Action<ResponseAdminApiError>() {
            @Override
            public void call(ResponseAdminApiError responseAdminApiError) {
                inProgress.set(false);
                eventBus.post(new ShowMessageMessage(responseAdminApiError.getMessage()));
            }
        };
        errorResponseHandlerSdk = new Action<ResponseZerokitError>() {
            @Override
            public void call(ResponseZerokitError errorResponse) {
                inProgress.set(false);
                eventBus.post(new ShowMessageMessage(errorResponse.getDescription()));
            }
        };
        focusChangeListener = new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                passwordError.set("");
                userNameError.set("");
                passwordConfirmError.set("");
            }
        };

        passwordExporter = new PasswordEditText.PasswordExporter();
        passwordExporterConfirm = new PasswordEditText.PasswordExporter();

        passwordExporter.setOnChangeListener(new PasswordEditText.OnChangeListener() {
            @Override
            public void onChanged() {
                zerokit.getPasswordStrength(passwordExporter).enqueue(new Action<ResponseZerokitPasswordStrength>() {
                    @Override
                    public void call(ResponseZerokitPasswordStrength responseZerokitPasswordStrength) {
                        int score = responseZerokitPasswordStrength.getScore();
                        passwordStrength.set((score + 1) * 20);
                        ((LayerDrawable) seekbarColor.get()).findDrawableByLayerId(android.R.id.progress).setColorFilter(resources.getColor(colorRes[score]), PorterDuff.Mode.SRC_IN);
                    }
                });
            }
        });
    }

    @SuppressWarnings("WeakerAccess")
    void attemptRegistration() {
        if (TextUtils.isEmpty(userName.get())) userNameError.set("Required");
        else if (passwordExporter.isEmpty()) passwordError.set("Required");
        else if (passwordExporterConfirm.isEmpty()) passwordConfirmError.set("Required");
        else if (!passwordExporter.isContentEqual(passwordExporterConfirm))
            passwordConfirmError.set("Does not match");
        else registration(userName.get(), passwordExporter);
    }

    private void registration(final String alias, final PasswordEditText.PasswordExporter passwordExporter) {
        inProgress.set(true);
        adminApi.initReg(alias, new JSONObject()
                .put("autoValidate", true)
                .put("canCreateTresor", true)
                .put("alias", alias)
                .toString()).enqueue(new Action<ResponseAdminApiInitUserRegistration>() {