All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.launchdarkly.sdk.android.SharedPrefsFlagStoreManager Maven / Gradle / Ivy

package com.launchdarkly.sdk.android;

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;

import androidx.annotation.NonNull;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

class SharedPrefsFlagStoreManager implements FlagStoreManager, StoreUpdatedListener {

    private static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-";

    @NonNull
    private final FlagStoreFactory flagStoreFactory;
    @NonNull
    private final String mobileKey;
    private final int maxCachedUsers;

    private FlagStore currentFlagStore;
    private final SharedPreferences usersSharedPrefs;
    private final ConcurrentHashMap> listeners;
    private final CopyOnWriteArrayList allFlagsListeners;

    SharedPrefsFlagStoreManager(@NonNull Application application,
                                @NonNull String mobileKey,
                                @NonNull FlagStoreFactory flagStoreFactory,
                                int maxCachedUsers) {
        this.mobileKey = mobileKey;
        this.flagStoreFactory = flagStoreFactory;
        this.maxCachedUsers = maxCachedUsers;
        this.usersSharedPrefs = application.getSharedPreferences(SHARED_PREFS_BASE_KEY + mobileKey + "-users", Context.MODE_PRIVATE);
        this.listeners = new ConcurrentHashMap<>();
        this.allFlagsListeners = new CopyOnWriteArrayList<>();
    }

    @Override
    public void switchToUser(String userKey) {
        String storeId = storeIdentifierForUser(userKey);
        if (currentFlagStore != null) {
            currentFlagStore.unregisterOnStoreUpdatedListener();
        }
        currentFlagStore = flagStoreFactory.createFlagStore(storeId);
        currentFlagStore.registerOnStoreUpdatedListener(this);

        // Store the user's key and the current time in usersSharedPrefs so it can be removed when
        // MAX_USERS is exceeded.
        usersSharedPrefs.edit()
                .putLong(userKey, System.currentTimeMillis())
                .apply();

        int usersStored = usersSharedPrefs.getAll().size();
        // Negative numbers represent an unlimited number of cached users. The active user is not
        // considered a cached user, so we subtract one.
        int usersToRemove = maxCachedUsers >= 0 ? usersStored - maxCachedUsers - 1 : 0;
        if (usersToRemove > 0) {
            Iterator oldestFirstUsers = getCachedUsers(storeId).iterator();
            // Remove oldest users until we are at MAX_USERS.
            for (int i = 0; i < usersToRemove; i++) {
                String removed = oldestFirstUsers.next();
                LDConfig.LOG.d("Exceeded max # of users: [%s] Removing user: [%s]", maxCachedUsers, removed);
                // Load FlagStore for oldest user and delete it.
                flagStoreFactory.createFlagStore(storeIdentifierForUser(removed)).delete();
                // Remove entry from usersSharedPrefs.
                usersSharedPrefs.edit().remove(removed).apply();
            }
        }
    }

    private String storeIdentifierForUser(String userKey) {
        return mobileKey + userKey;
    }

    @Override
    public FlagStore getCurrentUserStore() {
        return currentFlagStore;
    }

    @Override
    public void registerListener(String key, FeatureFlagChangeListener listener) {
        Map backingMap = new ConcurrentHashMap<>();
        Set newSet = Collections.newSetFromMap(backingMap);
        newSet.add(listener);
        Set oldSet = listeners.putIfAbsent(key, newSet);
        if (oldSet != null) {
            oldSet.add(listener);
            LDConfig.LOG.d("Added listener. Total count: [%s]", oldSet.size());
        } else {
            LDConfig.LOG.d("Added listener. Total count: 1");
        }
    }

    @Override
    public void unRegisterListener(String key, FeatureFlagChangeListener listener) {
        Set keySet = listeners.get(key);
        if (keySet != null) {
            boolean removed = keySet.remove(listener);
            if (removed) {
                LDConfig.LOG.d("Removing listener for key: [%s]", key);
            }
        }
    }

    @Override
    public void registerAllFlagsListener(LDAllFlagsListener listener) {
        allFlagsListeners.add(listener);
    }

    @Override
    public void unregisterAllFlagsListener(LDAllFlagsListener listener) {
        allFlagsListeners.remove(listener);
    }

    // Gets cached users (does not include the active user) sorted by creation time (oldest first)
    private Collection getCachedUsers(String activeUser) {
        Map all = usersSharedPrefs.getAll();
        all.remove(activeUser);
        TreeMap sortedMap = new TreeMap<>();
        //get typed versions of the users' timestamps and insert into sorted TreeMap
        for (String k : all.keySet()) {
            try {
                sortedMap.put((Long) all.get(k), k);
                LDConfig.LOG.d("Found user: %s", userAndTimeStampToHumanReadableString(k, (Long) all.get(k)));
            } catch (ClassCastException cce) {
                LDConfig.LOG.e(cce, "Unexpected type! This is not good");
            }
        }
        return sortedMap.values();
    }

    private static String userAndTimeStampToHumanReadableString(String userSharedPrefsKey, Long timestamp) {
        return userSharedPrefsKey + " [" + userSharedPrefsKey + "] timestamp: [" + timestamp + "]" + " [" + new Date(timestamp) + "]";
    }

    private void dispatchStoreUpdateCallback(final String flagKey, final FlagStoreUpdateType flagStoreUpdateType) {
        // We make sure to call listener callbacks on the main thread, as we consistently did so in
        // the past by virtue of using SharedPreferences to implement the callbacks.
        if (Looper.myLooper() == Looper.getMainLooper()) {
            // Get the listeners for the specific key
            Set keySet = listeners.get(flagKey);
            // If there are any listeners for this key
            if (keySet != null) {
                // We only call the listener if the flag is a new flag or updated.
                if (flagStoreUpdateType != FlagStoreUpdateType.FLAG_DELETED) {
                    for (FeatureFlagChangeListener listener : keySet) {
                        listener.onFeatureFlagChange(flagKey);
                    }
                } else {
                    // When flag is deleted we remove the corresponding listeners
                    listeners.remove(flagKey);
                }
            }
        } else {
            // Call ourselves on the main thread
            new Handler(Looper.getMainLooper()).post(() -> dispatchStoreUpdateCallback(flagKey, flagStoreUpdateType));
        }
    }

    @Override
    public void onStoreUpdate(List> updates) {
        List flagKeys = new ArrayList<>();
        for (Pair update : updates) {
            flagKeys.add(update.first);
            dispatchStoreUpdateCallback(update.first, update.second);
        }
        for (LDAllFlagsListener allFlagsListener : allFlagsListeners) {
            allFlagsListener.onChange(flagKeys);
        }
    }

    public Collection getListenersByKey(String key) {
        Set res = listeners.get(key);
        return res == null ? new HashSet<>() : res;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy