Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5d2049f2 authored by Varun Shah's avatar Varun Shah
Browse files

Updated onSharedPreferenceChanged listener behavior.

The onSharedPreferenceChanged listener will now also be called on
Editor#clear with a null key.
This change is gated behind a flag using the new app compat framework.

Removed the OnSharedPreferencesClear listener interface.

Bug: 119147584
Bug: 138293946
Test: atest android.content.cts.SharedPreferencesTest
Change-Id: Ieea168eb40afb8f1b5830f1541be20d93d6f94b5
parent 57872964
Loading
Loading
Loading
Loading
+0 −6
Original line number Diff line number Diff line
@@ -10865,9 +10865,7 @@ package android.content {
    method @Nullable public String getString(String, @Nullable String);
    method @Nullable public java.util.Set<java.lang.String> getStringSet(String, @Nullable java.util.Set<java.lang.String>);
    method public void registerOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
    method public default void registerOnSharedPreferencesClearListener(@NonNull android.content.SharedPreferences.OnSharedPreferencesClearListener);
    method public void unregisterOnSharedPreferenceChangeListener(android.content.SharedPreferences.OnSharedPreferenceChangeListener);
    method public default void unregisterOnSharedPreferencesClearListener(@NonNull android.content.SharedPreferences.OnSharedPreferencesClearListener);
  }
  public static interface SharedPreferences.Editor {
@@ -10887,10 +10885,6 @@ package android.content {
    method public void onSharedPreferenceChanged(android.content.SharedPreferences, String);
  }
  public static interface SharedPreferences.OnSharedPreferencesClearListener {
    method public void onSharedPreferencesClear(@NonNull android.content.SharedPreferences, @NonNull java.util.Set<java.lang.String>);
  }
  public class SyncAdapterType implements android.os.Parcelable {
    ctor public SyncAdapterType(String, String, boolean, boolean);
    ctor public SyncAdapterType(android.os.Parcel);
+29 −69
Original line number Diff line number Diff line
@@ -16,17 +16,19 @@

package android.app;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.FileUtils;
import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.system.StructTimespec;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
@@ -62,6 +64,15 @@ final class SharedPreferencesImpl implements SharedPreferences {
    /** If a fsync takes more than {@value #MAX_FSYNC_DURATION_MILLIS} ms, warn */
    private static final long MAX_FSYNC_DURATION_MILLIS = 256;

    /**
     * There will now be a callback to {@link
     * OnSharedPreferenceChangeListener#onSharedPreferenceChanged(SharedPreferences, String)} with
     * a {@code null} key on {@link Editor#clear()}.
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
    private static final long CALLBACK_ON_CLEAR_CHANGE = 119147584L;

    // Lock ordering rules:
    //  - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
    //  - acquire mWritingToDiskLock before EditorImpl.mLock
@@ -94,10 +105,6 @@ final class SharedPreferencesImpl implements SharedPreferences {
    private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
            new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

    @GuardedBy("mLock")
    private final WeakHashMap<OnSharedPreferencesClearListener, Object> mClearListeners =
            new WeakHashMap<>();

    /** Current memory state (always increasing) */
    @GuardedBy("this")
    private long mCurrentMemoryStateGeneration;
@@ -258,28 +265,6 @@ final class SharedPreferencesImpl implements SharedPreferences {
        }
    }

    @Override
    public void registerOnSharedPreferencesClearListener(
            @NonNull OnSharedPreferencesClearListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener cannot be null.");
        }
        synchronized (mLock) {
            mClearListeners.put(listener, CONTENT);
        }
    }

    @Override
    public void unregisterOnSharedPreferencesClearListener(
            @NonNull OnSharedPreferencesClearListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener cannot be null.");
        }
        synchronized (mLock) {
            mClearListeners.remove(listener);
        }
    }

    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
@@ -388,10 +373,9 @@ final class SharedPreferencesImpl implements SharedPreferences {
    // Return value from EditorImpl#commitToMemory()
    private static class MemoryCommitResult {
        final long memoryStateGeneration;
        final boolean keysCleared;
        @Nullable final List<String> keysModified;
        @Nullable final Set<String> keysCleared;
        @Nullable final Set<OnSharedPreferenceChangeListener> listeners;
        @Nullable final Set<OnSharedPreferencesClearListener> clearListeners;
        final Map<String, Object> mapToWriteToDisk;
        final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

@@ -399,16 +383,14 @@ final class SharedPreferencesImpl implements SharedPreferences {
        volatile boolean writeToDiskResult = false;
        boolean wasWritten = false;

        private MemoryCommitResult(long memoryStateGeneration, @Nullable List<String> keysModified,
        private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
                @Nullable List<String> keysModified,
                @Nullable Set<OnSharedPreferenceChangeListener> listeners,
                @Nullable Set<String> keysCleared,
                @Nullable Set<OnSharedPreferencesClearListener> clearListeners,
                Map<String, Object> mapToWriteToDisk) {
            this.memoryStateGeneration = memoryStateGeneration;
            this.keysCleared = keysCleared;
            this.keysModified = keysModified;
            this.listeners = listeners;
            this.keysCleared = keysCleared;
            this.clearListeners = clearListeners;
            this.mapToWriteToDisk = mapToWriteToDisk;
        }

@@ -526,16 +508,14 @@ final class SharedPreferencesImpl implements SharedPreferences {
            // SharedPreferences instance back, which has the
            // changes reflected in memory.
            notifyListeners(mcr);
            notifyClearListeners(mcr);
        }

        // Returns true if any changes were made
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false;
            List<String> keysModified = null;
            Set<String> keysCleared = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Set<OnSharedPreferencesClearListener> clearListeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
@@ -557,23 +537,16 @@ final class SharedPreferencesImpl implements SharedPreferences {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }
                boolean hasClearListeners = !mClearListeners.isEmpty();
                if (hasClearListeners) {
                    keysCleared = new ArraySet<>();
                    clearListeners = new HashSet<>(mClearListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            if (hasClearListeners) {
                                keysCleared.addAll(mapToWriteToDisk.keySet());
                            }
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }

@@ -613,8 +586,8 @@ final class SharedPreferencesImpl implements SharedPreferences {
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    keysCleared, clearListeners, mapToWriteToDisk);
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                    listeners, mapToWriteToDisk);
        }

        @Override
@@ -641,16 +614,21 @@ final class SharedPreferencesImpl implements SharedPreferences {
                }
            }
            notifyListeners(mcr);
            notifyClearListeners(mcr);
            return mcr.writeToDiskResult;
        }

        private void notifyListeners(final MemoryCommitResult mcr) {
            if (mcr.listeners == null || mcr.keysModified == null ||
                mcr.keysModified.size() == 0) {
            if (mcr.listeners == null || (mcr.keysModified == null && !mcr.keysCleared)) {
                return;
            }
            if (Looper.myLooper() == Looper.getMainLooper()) {
                if (mcr.keysCleared && Compatibility.isChangeEnabled(CALLBACK_ON_CLEAR_CHANGE)) {
                    for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                        if (listener != null) {
                            listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, null);
                        }
                    }
                }
                for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
                    final String key = mcr.keysModified.get(i);
                    for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
@@ -664,24 +642,6 @@ final class SharedPreferencesImpl implements SharedPreferences {
                ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr));
            }
        }

        private void notifyClearListeners(final MemoryCommitResult mcr) {
            if (mcr.clearListeners == null || mcr.keysCleared == null
                    || mcr.keysCleared.isEmpty()) {
                return;
            }
            if (Looper.myLooper() == Looper.getMainLooper()) {
                for (OnSharedPreferencesClearListener listener : mcr.clearListeners) {
                    if (listener != null) {
                        listener.onSharedPreferencesClear(SharedPreferencesImpl.this,
                                mcr.keysCleared);
                    }
                }
            } else {
                // Run this function on the main thread.
                ActivityThread.sMainThreadHandler.post(() -> notifyClearListeners(mcr));
            }
        }
    }

    /**
+9 −56
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package android.content;

import android.annotation.NonNull;
import android.annotation.Nullable;

import java.util.Map;
@@ -58,33 +57,18 @@ public interface SharedPreferences {
         *
         * <p>This callback will be run on your main thread.
         *
         * <p><em>Note: This callback will not be triggered when preferences are cleared via
         * {@link Editor#clear()}. However, from {@link android.os.Build.VERSION_CODES#R Android R}
         * onwards, you can use {@link OnSharedPreferencesClearListener} to register for
         * {@link Editor#clear()} callbacks.</em>
         *
         * @param sharedPreferences The {@link SharedPreferences} that received
         *            the change.
         * @param key The key of the preference that was changed, added, or
         *            removed.
         */
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    /**
     * Interface definition for a callback to be invoked when shared preferences are cleared.
     */
    public interface OnSharedPreferencesClearListener {
        /**
         * Called when shared preferences are cleared via {@link Editor#clear()}.
         *
         * <p>This callback will be run on your main thread.
         * <p><em>Note: This callback will not be triggered when preferences are cleared
         * via {@link Editor#clear()}, unless targeting {@link android.os.Build.VERSION_CODES#R}
         * on devices running OS versions {@link android.os.Build.VERSION_CODES#R Android R}
         * or later.</em>
         *
         * @param sharedPreferences The {@link SharedPreferences} that received the change.
         * @param keys The set of keys that were cleared.
         * @param key The key of the preference that was changed, added, or removed. Apps targeting
         *            {@link android.os.Build.VERSION_CODES#R} on devices running OS versions
         *            {@link android.os.Build.VERSION_CODES#R Android R} or later, will receive
         *            a {@code null} value when preferences are cleared.
         */
        void onSharedPreferencesClear(@NonNull SharedPreferences sharedPreferences,
                @NonNull Set<String> keys);
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    /**
@@ -405,35 +389,4 @@ public interface SharedPreferences {
     * @see #registerOnSharedPreferenceChangeListener
     */
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);

    /**
     * Registers a callback to be invoked when preferences are cleared via {@link Editor#clear()}.
     *
     * <p class="caution"><strong>Caution:</strong> The preference manager does
     * not currently store a strong reference to the listener. You must store a
     * strong reference to the listener, or it will be susceptible to garbage
     * collection. We recommend you keep a reference to the listener in the
     * instance data of an object that will exist as long as you need the
     * listener.</p>
     *
     * @param listener The callback that will run.
     * @see #unregisterOnSharedPreferencesClearListener
     */
    default void registerOnSharedPreferencesClearListener(
            @NonNull OnSharedPreferencesClearListener listener) {
        throw new UnsupportedOperationException(
                "registerOnSharedPreferencesClearListener not implemented");
    }

    /**
     * Unregisters a previous callback for {@link Editor#clear()}.
     *
     * @param listener The callback that should be unregistered.
     * @see #registerOnSharedPreferencesClearListener
     */
    default void unregisterOnSharedPreferencesClearListener(
            @NonNull OnSharedPreferencesClearListener listener) {
        throw new UnsupportedOperationException(
                "unregisterOnSharedPreferencesClearListener not implemented");
    }
}