Merge "Extracting VotesStorage to separate class with local locking" into udc-dev
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 2653b2a..fd94be9 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -103,10 +103,6 @@
     private static final int MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED = 7;
     private static final int MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED = 8;
 
-    // Special ID used to indicate that given vote is to be applied globally, rather than to a
-    // specific display.
-    private static final int GLOBAL_ID = -1;
-
     private static final float FLOAT_TOLERANCE = RefreshRateRange.FLOAT_TOLERANCE;
 
     private final Object mLock = new Object();
@@ -129,10 +125,6 @@
     @Nullable
     private DisplayDeviceConfig mDefaultDisplayDeviceConfig;
 
-    // A map from the display ID to the collection of votes and their priority. The latter takes
-    // the form of another map from the priority to the vote itself so that each priority is
-    // guaranteed to have exactly one vote, which is also easily and efficiently replaceable.
-    private SparseArray<SparseArray<Vote>> mVotesByDisplay;
     // A map from the display ID to the supported modes on that display.
     private SparseArray<Display.Mode[]> mSupportedModesByDisplay;
     // A map from the display ID to the default mode of that display.
@@ -146,6 +138,8 @@
 
     private final boolean mSupportsFrameRateOverride;
 
+    private final VotesStorage mVotesStorage;
+
     /**
      * The allowed refresh rate switching type. This is used by SurfaceFlinger.
      */
@@ -161,7 +155,6 @@
         mContext = context;
         mHandler = new DisplayModeDirectorHandler(handler.getLooper());
         mInjector = injector;
-        mVotesByDisplay = new SparseArray<>();
         mSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
         mAppRequestObserver = new AppRequestObserver();
@@ -171,15 +164,11 @@
         mBrightnessObserver = new BrightnessObserver(context, handler, injector);
         mDefaultDisplayDeviceConfig = null;
         mUdfpsObserver = new UdfpsObserver();
-        final BallotBox ballotBox = (displayId, priority, vote) -> {
-            synchronized (mLock) {
-                updateVoteLocked(displayId, priority, vote);
-            }
-        };
-        mDisplayObserver = new DisplayObserver(context, handler, ballotBox);
-        mSensorObserver = new SensorObserver(context, ballotBox, injector);
-        mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, ballotBox);
-        mHbmObserver = new HbmObserver(injector, ballotBox, BackgroundThread.getHandler(),
+        mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked);
+        mDisplayObserver = new DisplayObserver(context, handler, mVotesStorage);
+        mSensorObserver = new SensorObserver(context, mVotesStorage, injector);
+        mSkinThermalStatusObserver = new SkinThermalStatusObserver(injector, mVotesStorage);
+        mHbmObserver = new HbmObserver(injector, mVotesStorage, BackgroundThread.getHandler(),
                 mDeviceConfigDisplaySettings);
         mAlwaysRespectAppRequest = false;
         mSupportsFrameRateOverride = injector.supportsFrameRateOverride();
@@ -226,29 +215,7 @@
         mLoggingEnabled = loggingEnabled;
         mBrightnessObserver.setLoggingEnabled(loggingEnabled);
         mSkinThermalStatusObserver.setLoggingEnabled(loggingEnabled);
-    }
-
-    @NonNull
-    private SparseArray<Vote> getVotesLocked(int displayId) {
-        SparseArray<Vote> displayVotes = mVotesByDisplay.get(displayId);
-        final SparseArray<Vote> votes;
-        if (displayVotes != null) {
-            votes = displayVotes.clone();
-        } else {
-            votes = new SparseArray<>();
-        }
-
-        SparseArray<Vote> globalVotes = mVotesByDisplay.get(GLOBAL_ID);
-        if (globalVotes != null) {
-            for (int i = 0; i < globalVotes.size(); i++) {
-                int priority = globalVotes.keyAt(i);
-                if (votes.indexOfKey(priority) < 0) {
-                    votes.put(priority, globalVotes.valueAt(i));
-                }
-            }
-        }
-
-        return votes;
+        mVotesStorage.setLoggingEnabled(loggingEnabled);
     }
 
     private static final class VoteSummary {
@@ -407,7 +374,7 @@
     @NonNull
     public DesiredDisplayModeSpecs getDesiredDisplayModeSpecs(int displayId) {
         synchronized (mLock) {
-            SparseArray<Vote> votes = getVotesLocked(displayId);
+            SparseArray<Vote> votes = mVotesStorage.getVotes(displayId);
             Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
             Display.Mode defaultMode = mDefaultModeByDisplay.get(displayId);
             if (modes == null || defaultMode == null) {
@@ -780,10 +747,8 @@
     @VisibleForTesting
     @Nullable
     Vote getVote(int displayId, int priority) {
-        synchronized (mLock) {
-            SparseArray<Vote> votes = getVotesLocked(displayId);
-            return votes.get(priority);
-        }
+        SparseArray<Vote> votes = mVotesStorage.getVotes(displayId);
+        return votes.get(priority);
     }
 
     /**
@@ -806,18 +771,6 @@
                 final Display.Mode mode = mDefaultModeByDisplay.valueAt(i);
                 pw.println("    " + id + " -> " + mode);
             }
-            pw.println("  mVotesByDisplay:");
-            for (int i = 0; i < mVotesByDisplay.size(); i++) {
-                pw.println("    " + mVotesByDisplay.keyAt(i) + ":");
-                SparseArray<Vote> votes = mVotesByDisplay.valueAt(i);
-                for (int p = Vote.MAX_PRIORITY; p >= Vote.MIN_PRIORITY; p--) {
-                    Vote vote = votes.get(p);
-                    if (vote == null) {
-                        continue;
-                    }
-                    pw.println("      " + Vote.priorityToString(p) + " -> " + vote);
-                }
-            }
             pw.println("  mModeSwitchingType: " + switchingTypeToString(mModeSwitchingType));
             pw.println("  mAlwaysRespectAppRequest: " + mAlwaysRespectAppRequest);
             mSettingsObserver.dumpLocked(pw);
@@ -827,44 +780,10 @@
             mHbmObserver.dumpLocked(pw);
             mSkinThermalStatusObserver.dumpLocked(pw);
         }
-
+        mVotesStorage.dump(pw);
         mSensorObserver.dump(pw);
     }
 
-    private void updateVoteLocked(int priority, Vote vote) {
-        updateVoteLocked(GLOBAL_ID, priority, vote);
-    }
-
-    private void updateVoteLocked(int displayId, int priority, Vote vote) {
-        if (mLoggingEnabled) {
-            Slog.i(TAG, "updateVoteLocked(displayId=" + displayId
-                    + ", priority=" + Vote.priorityToString(priority)
-                    + ", vote=" + vote + ")");
-        }
-        if (priority < Vote.MIN_PRIORITY || priority > Vote.MAX_PRIORITY) {
-            Slog.w(TAG, "Received a vote with an invalid priority, ignoring:"
-                    + " priority=" + Vote.priorityToString(priority)
-                    + ", vote=" + vote, new Throwable());
-            return;
-        }
-        final SparseArray<Vote> votes = getOrCreateVotesByDisplay(displayId);
-
-        if (vote != null) {
-            votes.put(priority, vote);
-        } else {
-            votes.remove(priority);
-        }
-
-        if (votes.size() == 0) {
-            if (mLoggingEnabled) {
-                Slog.i(TAG, "No votes left for display " + displayId + ", removing.");
-            }
-            mVotesByDisplay.remove(displayId);
-        }
-
-        notifyDesiredDisplayModeSpecsChangedLocked();
-    }
-
     @GuardedBy("mLock")
     private float getMaxRefreshRateLocked(int displayId) {
         Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
@@ -890,16 +809,6 @@
         }
     }
 
-    private SparseArray<Vote> getOrCreateVotesByDisplay(int displayId) {
-        if (mVotesByDisplay.indexOfKey(displayId) >= 0) {
-            return mVotesByDisplay.get(displayId);
-        } else {
-            SparseArray<Vote> votes = new SparseArray<>();
-            mVotesByDisplay.put(displayId, votes);
-            return votes;
-        }
-    }
-
     private static String switchingTypeToString(@DisplayManager.SwitchingType int type) {
         switch (type) {
             case DisplayManager.SWITCHING_TYPE_NONE:
@@ -927,7 +836,7 @@
 
     @VisibleForTesting
     void injectVotesByDisplay(SparseArray<SparseArray<Vote>> votesByDisplay) {
-        mVotesByDisplay = votesByDisplay;
+        mVotesStorage.injectVotesByDisplay(votesByDisplay);
     }
 
     @VisibleForTesting
@@ -1157,225 +1066,6 @@
     }
 
     @VisibleForTesting
-    static final class Vote {
-        // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
-        // priority vote, it's overridden by all other considerations. It acts to set a default
-        // frame rate for a device.
-        public static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
-
-        // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
-        // null. It is used to set a preferred refresh rate value in case the higher priority votes
-        // result is a range.
-        public static final int PRIORITY_FLICKER_REFRESH_RATE = 1;
-
-        // High-brightness-mode may need a specific range of refresh-rates to function properly.
-        public static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
-
-        // SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
-        // It votes [minRefreshRate, Float.POSITIVE_INFINITY]
-        public static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
-
-        // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
-        // frame rate in certain cases, mostly to preserve power.
-        // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
-        // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
-        // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
-        public static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4;
-
-        // We split the app request into different priorities in case we can satisfy one desire
-        // without the other.
-
-        // Application can specify preferred refresh rate with below attrs.
-        // @see android.view.WindowManager.LayoutParams#preferredRefreshRate
-        // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
-        //
-        // When the app specifies a LayoutParams#preferredDisplayModeId, in addition to the
-        // refresh rate, it also chooses a preferred size (resolution) as part of the selected
-        // mode id. The app preference is then translated to APP_REQUEST_BASE_MODE_REFRESH_RATE and
-        // optionally to APP_REQUEST_SIZE as well, if a mode id was selected.
-        // The system also forces some apps like denylisted app to run at a lower refresh rate.
-        // @see android.R.array#config_highRefreshRateBlacklist
-        //
-        // When summarizing the votes and filtering the allowed display modes, these votes determine
-        // which mode id should be the base mode id to be sent to SurfaceFlinger:
-        // - APP_REQUEST_BASE_MODE_REFRESH_RATE is used to validate the vote summary. If a summary
-        //   includes a base mode refresh rate, but it is not in the refresh rate range, then the
-        //   summary is considered invalid so we could drop a lower priority vote and try again.
-        // - APP_REQUEST_SIZE is used to filter out display modes of a different size.
-        //
-        // The preferred refresh rate is set on the main surface of the app outside of
-        // DisplayModeDirector.
-        // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
-        public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5;
-        public static final int PRIORITY_APP_REQUEST_SIZE = 6;
-
-        // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
-        // rest of low priority voters. It votes [0, max(PEAK, MIN)]
-        public static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
-
-        // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
-        // rate to max value (same as for PRIORITY_UDFPS) on lock screen
-        public static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
-
-        // For concurrent displays we want to limit refresh rate on all displays
-        public static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9;
-
-        // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
-        // Settings.Global.LOW_POWER_MODE is on.
-        public static final int PRIORITY_LOW_POWER_MODE = 10;
-
-        // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
-        // higher priority voters' result is a range, it will fix the rate to a single choice.
-        // It's used to avoid refresh rate switches in certain conditions which may result in the
-        // user seeing the display flickering when the switches occur.
-        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11;
-
-        // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
-        public static final int PRIORITY_SKIN_TEMPERATURE = 12;
-
-        // The proximity sensor needs the refresh rate to be locked in order to function, so this is
-        // set to a high priority.
-        public static final int PRIORITY_PROXIMITY = 13;
-
-        // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
-        // to function, so this needs to be the highest priority of all votes.
-        public static final int PRIORITY_UDFPS = 14;
-
-        // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
-        // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
-
-        public static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
-        public static final int MAX_PRIORITY = PRIORITY_UDFPS;
-
-        // The cutoff for the app request refresh rate range. Votes with priorities lower than this
-        // value will not be considered when constructing the app request refresh rate range.
-        public static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
-                PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
-
-        /**
-         * A value signifying an invalid width or height in a vote.
-         */
-        public static final int INVALID_SIZE = -1;
-
-        /**
-         * The requested width of the display in pixels, or INVALID_SIZE;
-         */
-        public final int width;
-        /**
-         * The requested height of the display in pixels, or INVALID_SIZE;
-         */
-        public final int height;
-        /**
-         * Information about the refresh rate frame rate ranges DM would like to set the display to.
-         */
-        public final RefreshRateRanges refreshRateRanges;
-
-        /**
-         * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
-         * a single value).
-         */
-        public final boolean disableRefreshRateSwitching;
-
-        /**
-         * The preferred refresh rate selected by the app. It is used to validate that the summary
-         * refresh rate ranges include this value, and are not restricted by a lower priority vote.
-         */
-        public final float appRequestBaseModeRefreshRate;
-
-        public static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0,
-                    Float.POSITIVE_INFINITY,
-                    minRefreshRate == maxRefreshRate, 0f);
-        }
-
-        public static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate,
-                    maxFrameRate,
-                    false, 0f);
-        }
-
-        public static Vote forSize(int width, int height) {
-            return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY,
-                    false,
-                    0f);
-        }
-
-        public static Vote forDisableRefreshRateSwitching() {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
-                    Float.POSITIVE_INFINITY, true,
-                    0f);
-        }
-
-        public static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
-                    Float.POSITIVE_INFINITY, false,
-                    baseModeRefreshRate);
-        }
-
-        private Vote(int width, int height,
-                float minPhysicalRefreshRate,
-                float maxPhysicalRefreshRate,
-                float minRenderFrameRate,
-                float maxRenderFrameRate,
-                boolean disableRefreshRateSwitching,
-                float baseModeRefreshRate) {
-            this.width = width;
-            this.height = height;
-            this.refreshRateRanges = new RefreshRateRanges(
-                    new RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
-                    new RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
-            this.disableRefreshRateSwitching = disableRefreshRateSwitching;
-            this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
-        }
-
-        public static String priorityToString(int priority) {
-            switch (priority) {
-                case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
-                    return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
-                case PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE:
-                    return "PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE";
-                case PRIORITY_APP_REQUEST_SIZE:
-                    return "PRIORITY_APP_REQUEST_SIZE";
-                case PRIORITY_DEFAULT_RENDER_FRAME_RATE:
-                    return "PRIORITY_DEFAULT_REFRESH_RATE";
-                case PRIORITY_FLICKER_REFRESH_RATE:
-                    return "PRIORITY_FLICKER_REFRESH_RATE";
-                case PRIORITY_FLICKER_REFRESH_RATE_SWITCH:
-                    return "PRIORITY_FLICKER_REFRESH_RATE_SWITCH";
-                case PRIORITY_HIGH_BRIGHTNESS_MODE:
-                    return "PRIORITY_HIGH_BRIGHTNESS_MODE";
-                case PRIORITY_PROXIMITY:
-                    return "PRIORITY_PROXIMITY";
-                case PRIORITY_LOW_POWER_MODE:
-                    return "PRIORITY_LOW_POWER_MODE";
-                case PRIORITY_SKIN_TEMPERATURE:
-                    return "PRIORITY_SKIN_TEMPERATURE";
-                case PRIORITY_UDFPS:
-                    return "PRIORITY_UDFPS";
-                case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE:
-                    return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
-                case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
-                    return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
-                case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
-                    return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
-                case PRIORITY_LAYOUT_LIMITED_FRAME_RATE:
-                    return "PRIORITY_LAYOUT_LIMITED_FRAME_RATE";
-                default:
-                    return Integer.toString(priority);
-            }
-        }
-
-        @Override
-        public String toString() {
-            return "Vote{"
-                    + "width=" + width + ", height=" + height
-                    + ", refreshRateRanges=" + refreshRateRanges
-                    + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
-                    + ", appRequestBaseModeRefreshRate=" + appRequestBaseModeRefreshRate + "}";
-        }
-    }
-
-    @VisibleForTesting
     final class SettingsObserver extends ContentObserver {
         private final Uri mSmoothDisplaySetting =
                 Settings.System.getUriFor(Settings.System.SMOOTH_DISPLAY);
@@ -1510,7 +1200,7 @@
             } else {
                 vote = null;
             }
-            updateVoteLocked(Vote.PRIORITY_LOW_POWER_MODE, vote);
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_LOW_POWER_MODE, vote);
             mBrightnessObserver.onLowPowerModeEnabledLocked(inLowPowerMode);
         }
 
@@ -1529,13 +1219,14 @@
             Vote peakVote = peakRefreshRate == 0f
                     ? null
                     : Vote.forRenderFrameRates(0f, Math.max(minRefreshRate, peakRefreshRate));
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE, peakVote);
-            updateVoteLocked(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
+                    peakVote);
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
                     Vote.forRenderFrameRates(minRefreshRate, Float.POSITIVE_INFINITY));
             Vote defaultVote =
                     defaultRefreshRate == 0f
                             ? null : Vote.forRenderFrameRates(0f, defaultRefreshRate);
-            updateVoteLocked(Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE, defaultVote);
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_DEFAULT_RENDER_FRAME_RATE, defaultVote);
 
             float maxRefreshRate;
             if (peakRefreshRate == 0f && defaultRefreshRate == 0f) {
@@ -1619,9 +1310,9 @@
                 sizeVote = null;
             }
 
-            updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                     baseModeRefreshRateVote);
-            updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
         }
 
         private void setAppPreferredRefreshRateRangeLocked(int displayId,
@@ -1652,11 +1343,8 @@
                 mAppPreferredRefreshRateRangeByDisplay.remove(displayId);
                 vote = null;
             }
-            synchronized (mLock) {
-                updateVoteLocked(displayId,
-                        Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
-                        vote);
-            }
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE,
+                    vote);
         }
 
         private Display.Mode findModeByIdLocked(int displayId, int modeId) {
@@ -1696,12 +1384,12 @@
         // calling into us already holding its own lock.
         private final Context mContext;
         private final Handler mHandler;
-        private final BallotBox mBallotBox;
+        private final VotesStorage mVotesStorage;
 
-        DisplayObserver(Context context, Handler handler, BallotBox ballotBox) {
+        DisplayObserver(Context context, Handler handler, VotesStorage votesStorage) {
             mContext = context;
             mHandler = handler;
-            mBallotBox = ballotBox;
+            mVotesStorage = votesStorage;
         }
 
         public void observe() {
@@ -1761,7 +1449,7 @@
             Vote vote = info != null && info.layoutLimitedRefreshRate != null
                     ? Vote.forPhysicalRefreshRates(info.layoutLimitedRefreshRate.min,
                     info.layoutLimitedRefreshRate.max) : null;
-            mBallotBox.vote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_LAYOUT_LIMITED_FRAME_RATE, vote);
         }
 
         private void updateDisplayModes(int displayId, @Nullable DisplayInfo info) {
@@ -2083,8 +1771,8 @@
                 updateSensorStatus();
                 if (!changeable) {
                     // Revoke previous vote from BrightnessObserver
-                    updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE, null);
-                    updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, null);
+                    mVotesStorage.updateGlobalVote(Vote.PRIORITY_FLICKER_REFRESH_RATE, null);
+                    mVotesStorage.updateGlobalVote(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, null);
                 }
             }
         }
@@ -2409,8 +2097,9 @@
                 Slog.d(TAG, "Display brightness " + mBrightness + ", ambient lux " +  mAmbientLux
                         + ", Vote " + refreshRateVote);
             }
-            updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE, refreshRateVote);
-            updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, refreshRateSwitchingVote);
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_FLICKER_REFRESH_RATE, refreshRateVote);
+            mVotesStorage.updateGlobalVote(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH,
+                    refreshRateSwitchingVote);
         }
 
         private boolean hasValidLowZone() {
@@ -2678,7 +2367,7 @@
             } else {
                 vote = null;
             }
-            DisplayModeDirector.this.updateVoteLocked(displayId, votePriority, vote);
+            mVotesStorage.updateVote(displayId, votePriority, vote);
         }
 
         void dumpLocked(PrintWriter pw) {
@@ -2704,7 +2393,7 @@
         private final String mProximitySensorName = null;
         private final String mProximitySensorType = Sensor.STRING_TYPE_PROXIMITY;
 
-        private final BallotBox mBallotBox;
+        private final VotesStorage mVotesStorage;
         private final Context mContext;
         private final Injector mInjector;
         @GuardedBy("mSensorObserverLock")
@@ -2716,9 +2405,9 @@
         @GuardedBy("mSensorObserverLock")
         private boolean mIsProxActive = false;
 
-        SensorObserver(Context context, BallotBox ballotBox, Injector injector) {
+        SensorObserver(Context context, VotesStorage votesStorage, Injector injector) {
             mContext = context;
-            mBallotBox = ballotBox;
+            mVotesStorage = votesStorage;
             mInjector = injector;
         }
 
@@ -2764,7 +2453,7 @@
                         vote = Vote.forPhysicalRefreshRates(rate.min, rate.max);
                     }
                 }
-                mBallotBox.vote(displayId, Vote.PRIORITY_PROXIMITY, vote);
+                mVotesStorage.updateVote(displayId, Vote.PRIORITY_PROXIMITY, vote);
             }
         }
 
@@ -2817,7 +2506,7 @@
      * DisplayManagerInternal but originate in the display-device-config file.
      */
     public class HbmObserver implements DisplayManager.DisplayListener {
-        private final BallotBox mBallotBox;
+        private final VotesStorage mVotesStorage;
         private final Handler mHandler;
         private final SparseIntArray mHbmMode = new SparseIntArray();
         private final SparseBooleanArray mHbmActive = new SparseBooleanArray();
@@ -2828,10 +2517,10 @@
 
         private DisplayManagerInternal mDisplayManagerInternal;
 
-        HbmObserver(Injector injector, BallotBox ballotBox, Handler handler,
+        HbmObserver(Injector injector, VotesStorage votesStorage, Handler handler,
                 DeviceConfigDisplaySettings displaySettings) {
             mInjector = injector;
-            mBallotBox = ballotBox;
+            mVotesStorage = votesStorage;
             mHandler = handler;
             mDeviceConfigDisplaySettings = displaySettings;
         }
@@ -2901,7 +2590,7 @@
 
         @Override
         public void onDisplayRemoved(int displayId) {
-            mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, null);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, null);
             mHbmMode.delete(displayId);
             mHbmActive.delete(displayId);
         }
@@ -2968,7 +2657,7 @@
                 }
 
             }
-            mBallotBox.vote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, vote);
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_HIGH_BRIGHTNESS_MODE, vote);
         }
 
         void dumpLocked(PrintWriter pw) {
@@ -3299,8 +2988,4 @@
                     ServiceManager.getService(Context.THERMAL_SERVICE));
         }
     }
-
-    interface BallotBox {
-        void vote(int displayId, int priority, Vote vote);
-    }
 }
diff --git a/services/core/java/com/android/server/display/mode/SkinThermalStatusObserver.java b/services/core/java/com/android/server/display/mode/SkinThermalStatusObserver.java
index 8a3b329..58e1550 100644
--- a/services/core/java/com/android/server/display/mode/SkinThermalStatusObserver.java
+++ b/services/core/java/com/android/server/display/mode/SkinThermalStatusObserver.java
@@ -37,7 +37,7 @@
         DisplayManager.DisplayListener {
     private static final String TAG = "SkinThermalStatusObserver";
 
-    private final DisplayModeDirector.BallotBox mBallotBox;
+    private final VotesStorage mVotesStorage;
     private final DisplayModeDirector.Injector mInjector;
 
     private boolean mLoggingEnabled;
@@ -52,15 +52,15 @@
             mThermalThrottlingByDisplay = new SparseArray<>();
 
     SkinThermalStatusObserver(DisplayModeDirector.Injector injector,
-            DisplayModeDirector.BallotBox ballotBox) {
-        this(injector, ballotBox, BackgroundThread.getHandler());
+            VotesStorage votesStorage) {
+        this(injector, votesStorage, BackgroundThread.getHandler());
     }
 
     @VisibleForTesting
     SkinThermalStatusObserver(DisplayModeDirector.Injector injector,
-            DisplayModeDirector.BallotBox ballotBox, Handler handler) {
+            VotesStorage votesStorage, Handler handler) {
         mInjector = injector;
-        mBallotBox = ballotBox;
+        mVotesStorage = votesStorage;
         mHandler = handler;
     }
 
@@ -112,8 +112,8 @@
     public void onDisplayRemoved(int displayId) {
         synchronized (mThermalObserverLock) {
             mThermalThrottlingByDisplay.remove(displayId);
-            mHandler.post(() -> mBallotBox.vote(displayId,
-                    DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE, null));
+            mHandler.post(() -> mVotesStorage.updateVote(displayId,
+                    Vote.PRIORITY_SKIN_TEMPERATURE, null));
         }
         if (mLoggingEnabled) {
             Slog.d(TAG, "Display removed and voted: displayId=" + displayId);
@@ -218,11 +218,11 @@
         SurfaceControl.RefreshRateRange foundRange = findBestMatchingRefreshRateRange(currentStatus,
                 throttlingMap);
         // if status <= currentStatus not found in the map reset vote
-        DisplayModeDirector.Vote vote = null;
+        Vote vote = null;
         if (foundRange != null) { // otherwise vote with found range
-            vote = DisplayModeDirector.Vote.forRenderFrameRates(foundRange.min, foundRange.max);
+            vote = Vote.forRenderFrameRates(foundRange.min, foundRange.max);
         }
-        mBallotBox.vote(displayId, DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE, vote);
+        mVotesStorage.updateVote(displayId, Vote.PRIORITY_SKIN_TEMPERATURE, vote);
         if (mLoggingEnabled) {
             Slog.d(TAG, "Voted: vote=" + vote + ", display =" + displayId);
         }
@@ -244,11 +244,11 @@
 
     private void fallbackReportThrottlingIfNeeded(int displayId,
             @Temperature.ThrottlingStatus int currentStatus) {
-        DisplayModeDirector.Vote vote = null;
+        Vote vote = null;
         if (currentStatus >= Temperature.THROTTLING_CRITICAL) {
-            vote = DisplayModeDirector.Vote.forRenderFrameRates(0f, 60f);
+            vote = Vote.forRenderFrameRates(0f, 60f);
         }
-        mBallotBox.vote(displayId, DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE, vote);
+        mVotesStorage.updateVote(displayId, Vote.PRIORITY_SKIN_TEMPERATURE, vote);
         if (mLoggingEnabled) {
             Slog.d(TAG, "Voted(fallback): vote=" + vote + ", display =" + displayId);
         }
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
new file mode 100644
index 0000000..a42d8f2
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+import android.view.SurfaceControl;
+
+final class Vote {
+    // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
+    // priority vote, it's overridden by all other considerations. It acts to set a default
+    // frame rate for a device.
+    static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
+
+    // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
+    // null. It is used to set a preferred refresh rate value in case the higher priority votes
+    // result is a range.
+    static final int PRIORITY_FLICKER_REFRESH_RATE = 1;
+
+    // High-brightness-mode may need a specific range of refresh-rates to function properly.
+    static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
+
+    // SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
+    // It votes [minRefreshRate, Float.POSITIVE_INFINITY]
+    static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
+
+    // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
+    // frame rate in certain cases, mostly to preserve power.
+    // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
+    // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
+    // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
+    static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 4;
+
+    // We split the app request into different priorities in case we can satisfy one desire
+    // without the other.
+
+    // Application can specify preferred refresh rate with below attrs.
+    // @see android.view.WindowManager.LayoutParams#preferredRefreshRate
+    // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
+    //
+    // When the app specifies a LayoutParams#preferredDisplayModeId, in addition to the
+    // refresh rate, it also chooses a preferred size (resolution) as part of the selected
+    // mode id. The app preference is then translated to APP_REQUEST_BASE_MODE_REFRESH_RATE and
+    // optionally to APP_REQUEST_SIZE as well, if a mode id was selected.
+    // The system also forces some apps like denylisted app to run at a lower refresh rate.
+    // @see android.R.array#config_highRefreshRateBlacklist
+    //
+    // When summarizing the votes and filtering the allowed display modes, these votes determine
+    // which mode id should be the base mode id to be sent to SurfaceFlinger:
+    // - APP_REQUEST_BASE_MODE_REFRESH_RATE is used to validate the vote summary. If a summary
+    //   includes a base mode refresh rate, but it is not in the refresh rate range, then the
+    //   summary is considered invalid so we could drop a lower priority vote and try again.
+    // - APP_REQUEST_SIZE is used to filter out display modes of a different size.
+    //
+    // The preferred refresh rate is set on the main surface of the app outside of
+    // DisplayModeDirector.
+    // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
+    static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 5;
+    static final int PRIORITY_APP_REQUEST_SIZE = 6;
+
+    // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
+    // rest of low priority voters. It votes [0, max(PEAK, MIN)]
+    static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 7;
+
+    // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
+    // rate to max value (same as for PRIORITY_UDFPS) on lock screen
+    static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 8;
+
+    // For concurrent displays we want to limit refresh rate on all displays
+    static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 9;
+
+    // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
+    // Settings.Global.LOW_POWER_MODE is on.
+    static final int PRIORITY_LOW_POWER_MODE = 10;
+
+    // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
+    // higher priority voters' result is a range, it will fix the rate to a single choice.
+    // It's used to avoid refresh rate switches in certain conditions which may result in the
+    // user seeing the display flickering when the switches occur.
+    static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 11;
+
+    // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
+    static final int PRIORITY_SKIN_TEMPERATURE = 12;
+
+    // The proximity sensor needs the refresh rate to be locked in order to function, so this is
+    // set to a high priority.
+    static final int PRIORITY_PROXIMITY = 13;
+
+    // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
+    // to function, so this needs to be the highest priority of all votes.
+    static final int PRIORITY_UDFPS = 14;
+
+    // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
+    // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
+
+    static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
+    static final int MAX_PRIORITY = PRIORITY_UDFPS;
+
+    // The cutoff for the app request refresh rate range. Votes with priorities lower than this
+    // value will not be considered when constructing the app request refresh rate range.
+    static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
+            PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
+
+    /**
+     * A value signifying an invalid width or height in a vote.
+     */
+    static final int INVALID_SIZE = -1;
+
+    /**
+     * The requested width of the display in pixels, or INVALID_SIZE;
+     */
+    public final int width;
+    /**
+     * The requested height of the display in pixels, or INVALID_SIZE;
+     */
+    public final int height;
+    /**
+     * Information about the refresh rate frame rate ranges DM would like to set the display to.
+     */
+    public final SurfaceControl.RefreshRateRanges refreshRateRanges;
+
+    /**
+     * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
+     * a single value).
+     */
+    public final boolean disableRefreshRateSwitching;
+
+    /**
+     * The preferred refresh rate selected by the app. It is used to validate that the summary
+     * refresh rate ranges include this value, and are not restricted by a lower priority vote.
+     */
+    public final float appRequestBaseModeRefreshRate;
+
+    static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
+        return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate, 0,
+                Float.POSITIVE_INFINITY,
+                minRefreshRate == maxRefreshRate, 0f);
+    }
+
+    static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
+        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, minFrameRate,
+                maxFrameRate,
+                false, 0f);
+    }
+
+    static Vote forSize(int width, int height) {
+        return new Vote(width, height, 0, Float.POSITIVE_INFINITY, 0, Float.POSITIVE_INFINITY,
+                false,
+                0f);
+    }
+
+    static Vote forDisableRefreshRateSwitching() {
+        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                Float.POSITIVE_INFINITY, true,
+                0f);
+    }
+
+    static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
+        return new Vote(INVALID_SIZE, INVALID_SIZE, 0, Float.POSITIVE_INFINITY, 0,
+                Float.POSITIVE_INFINITY, false,
+                baseModeRefreshRate);
+    }
+
+    private Vote(int width, int height,
+            float minPhysicalRefreshRate,
+            float maxPhysicalRefreshRate,
+            float minRenderFrameRate,
+            float maxRenderFrameRate,
+            boolean disableRefreshRateSwitching,
+            float baseModeRefreshRate) {
+        this.width = width;
+        this.height = height;
+        this.refreshRateRanges = new SurfaceControl.RefreshRateRanges(
+                new SurfaceControl.RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
+                new SurfaceControl.RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
+        this.disableRefreshRateSwitching = disableRefreshRateSwitching;
+        this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
+    }
+
+    static String priorityToString(int priority) {
+        switch (priority) {
+            case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
+                return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
+            case PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE:
+                return "PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE";
+            case PRIORITY_APP_REQUEST_SIZE:
+                return "PRIORITY_APP_REQUEST_SIZE";
+            case PRIORITY_DEFAULT_RENDER_FRAME_RATE:
+                return "PRIORITY_DEFAULT_REFRESH_RATE";
+            case PRIORITY_FLICKER_REFRESH_RATE:
+                return "PRIORITY_FLICKER_REFRESH_RATE";
+            case PRIORITY_FLICKER_REFRESH_RATE_SWITCH:
+                return "PRIORITY_FLICKER_REFRESH_RATE_SWITCH";
+            case PRIORITY_HIGH_BRIGHTNESS_MODE:
+                return "PRIORITY_HIGH_BRIGHTNESS_MODE";
+            case PRIORITY_PROXIMITY:
+                return "PRIORITY_PROXIMITY";
+            case PRIORITY_LOW_POWER_MODE:
+                return "PRIORITY_LOW_POWER_MODE";
+            case PRIORITY_SKIN_TEMPERATURE:
+                return "PRIORITY_SKIN_TEMPERATURE";
+            case PRIORITY_UDFPS:
+                return "PRIORITY_UDFPS";
+            case PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE:
+                return "PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE";
+            case PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE:
+                return "PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE";
+            case PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE:
+                return "PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE";
+            case PRIORITY_LAYOUT_LIMITED_FRAME_RATE:
+                return "PRIORITY_LAYOUT_LIMITED_FRAME_RATE";
+            default:
+                return Integer.toString(priority);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Vote: {"
+                + "width: " + width + ", height: " + height
+                + ", refreshRateRanges: " + refreshRateRanges
+                + ", disableRefreshRateSwitching: " + disableRefreshRateSwitching
+                + ", appRequestBaseModeRefreshRate: "  + appRequestBaseModeRefreshRate + "}";
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
new file mode 100644
index 0000000..dadcebe
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
+
+class VotesStorage {
+    private static final String TAG = "VotesStorage";
+    // Special ID used to indicate that given vote is to be applied globally, rather than to a
+    // specific display.
+    private static final int GLOBAL_ID = -1;
+
+    private boolean mLoggingEnabled;
+
+    private final Listener mListener;
+
+    private final Object mStorageLock = new Object();
+    // A map from the display ID to the collection of votes and their priority. The latter takes
+    // the form of another map from the priority to the vote itself so that each priority is
+    // guaranteed to have exactly one vote, which is also easily and efficiently replaceable.
+    @GuardedBy("mStorageLock")
+    private final SparseArray<SparseArray<Vote>> mVotesByDisplay = new SparseArray<>();
+
+    VotesStorage(@NonNull Listener listener) {
+        mListener = listener;
+    }
+    /** sets logging enabled/disabled for this class */
+    void setLoggingEnabled(boolean loggingEnabled) {
+        mLoggingEnabled = loggingEnabled;
+    }
+    /**
+     * gets all votes for specific display, note that global display votes are also added to result
+     */
+    @NonNull
+    SparseArray<Vote> getVotes(int displayId) {
+        SparseArray<Vote> votesLocal;
+        SparseArray<Vote> globalVotesLocal;
+        synchronized (mStorageLock) {
+            SparseArray<Vote> displayVotes = mVotesByDisplay.get(displayId);
+            votesLocal = displayVotes != null ? displayVotes.clone() : new SparseArray<>();
+            SparseArray<Vote> globalVotes = mVotesByDisplay.get(GLOBAL_ID);
+            globalVotesLocal = globalVotes != null ? globalVotes.clone() : new SparseArray<>();
+        }
+        for (int i = 0; i < globalVotesLocal.size(); i++) {
+            int priority = globalVotesLocal.keyAt(i);
+            if (!votesLocal.contains(priority)) {
+                votesLocal.put(priority, globalVotesLocal.valueAt(i));
+            }
+        }
+        return votesLocal;
+    }
+
+    /** updates vote storage for all displays */
+    void updateGlobalVote(int priority, @Nullable Vote vote) {
+        updateVote(GLOBAL_ID, priority, vote);
+    }
+
+    /** updates vote storage */
+    void updateVote(int displayId, int priority, @Nullable Vote vote) {
+        if (mLoggingEnabled) {
+            Slog.i(TAG, "updateVoteLocked(displayId=" + displayId
+                    + ", priority=" + Vote.priorityToString(priority)
+                    + ", vote=" + vote + ")");
+        }
+        if (priority < Vote.MIN_PRIORITY || priority > Vote.MAX_PRIORITY) {
+            Slog.w(TAG, "Received a vote with an invalid priority, ignoring:"
+                    + " priority=" + Vote.priorityToString(priority)
+                    + ", vote=" + vote);
+            return;
+        }
+        SparseArray<Vote> votes;
+        synchronized (mStorageLock) {
+            if (mVotesByDisplay.contains(displayId)) {
+                votes = mVotesByDisplay.get(displayId);
+            } else {
+                votes = new SparseArray<>();
+                mVotesByDisplay.put(displayId, votes);
+            }
+            if (vote != null) {
+                votes.put(priority, vote);
+            } else {
+                votes.remove(priority);
+            }
+        }
+        if (mLoggingEnabled) {
+            Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
+        }
+        mListener.onChanged();
+    }
+
+    /** dump class values, for debugging */
+    void dump(@NonNull PrintWriter pw) {
+        SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>();
+        synchronized (mStorageLock) {
+            for (int i = 0; i < mVotesByDisplay.size(); i++) {
+                votesByDisplayLocal.put(mVotesByDisplay.keyAt(i),
+                        mVotesByDisplay.valueAt(i).clone());
+            }
+        }
+        pw.println("  mVotesByDisplay:");
+        for (int i = 0; i < votesByDisplayLocal.size(); i++) {
+            SparseArray<Vote> votes = votesByDisplayLocal.valueAt(i);
+            if (votes.size() == 0) {
+                continue;
+            }
+            pw.println("    " + votesByDisplayLocal.keyAt(i) + ":");
+            for (int p = Vote.MAX_PRIORITY; p >= Vote.MIN_PRIORITY; p--) {
+                Vote vote = votes.get(p);
+                if (vote == null) {
+                    continue;
+                }
+                pw.println("      " + Vote.priorityToString(p) + " -> " + vote);
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void injectVotesByDisplay(SparseArray<SparseArray<Vote>> votesByDisplay) {
+        synchronized (mStorageLock) {
+            mVotesByDisplay.clear();
+            for (int i = 0; i < votesByDisplay.size(); i++) {
+                mVotesByDisplay.put(votesByDisplay.keyAt(i), votesByDisplay.valueAt(i));
+            }
+        }
+    }
+
+    interface Listener {
+        void onChanged();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 1b393c4..e492252 100644
--- a/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -27,7 +27,7 @@
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE;
 
-import static com.android.server.display.mode.DisplayModeDirector.Vote.INVALID_SIZE;
+import static com.android.server.display.mode.Vote.INVALID_SIZE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -94,7 +94,6 @@
 import com.android.server.display.TestUtils;
 import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver;
 import com.android.server.display.mode.DisplayModeDirector.DesiredDisplayModeSpecs;
-import com.android.server.display.mode.DisplayModeDirector.Vote;
 import com.android.server.sensors.SensorManagerInternal;
 import com.android.server.sensors.SensorManagerInternal.ProximityActiveListener;
 import com.android.server.statusbar.StatusBarManagerInternal;
@@ -224,8 +223,7 @@
         assertThat(modeSpecs.appRequest.render.min).isEqualTo(0f);
         assertThat(modeSpecs.appRequest.render.max).isEqualTo(Float.POSITIVE_INFINITY);
 
-        int numPriorities =
-                DisplayModeDirector.Vote.MAX_PRIORITY - DisplayModeDirector.Vote.MIN_PRIORITY + 1;
+        int numPriorities =  Vote.MAX_PRIORITY - Vote.MIN_PRIORITY + 1;
 
         // Ensure vote priority works as expected. As we add new votes with higher priority, they
         // should take precedence over lower priority votes.
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java b/services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
index 13540d6..9ab6ee5 100644
--- a/services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
@@ -18,7 +18,6 @@
 
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import android.hardware.display.DisplayManager;
@@ -57,7 +56,7 @@
     private RegisteringFakesInjector mInjector = new RegisteringFakesInjector();
 
     private final TestHandler mHandler = new TestHandler(null);
-    private final FakeVoteStorage mStorage = new FakeVoteStorage();
+    private final VotesStorage mStorage = new VotesStorage(() -> {});
 
     @Before
     public void setUp() {
@@ -92,28 +91,26 @@
     public void testNotifyWithDefaultVotesForCritical() {
         // GIVEN 2 displays with no thermalThrottling config
         mObserver.observe();
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
 
         // WHEN thermal sensor notifies CRITICAL
         mObserver.notifyThrottling(createTemperature(Temperature.THROTTLING_CRITICAL));
         mHandler.flush();
 
         // THEN 2 votes are added to storage with (0,60) render refresh rate(default behaviour)
-        assertEquals(2, mStorage.mVoteRegistry.size());
-
-        SparseArray<DisplayModeDirector.Vote> displayVotes = mStorage.mVoteRegistry.get(DISPLAY_ID);
+        SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
         assertEquals(1, displayVotes.size());
 
-        DisplayModeDirector.Vote vote = displayVotes.get(
-                DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE);
+        Vote vote = displayVotes.get(
+                Vote.PRIORITY_SKIN_TEMPERATURE);
         assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
         assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
 
-        SparseArray<DisplayModeDirector.Vote> otherDisplayVotes = mStorage.mVoteRegistry.get(
-                DISPLAY_ID_OTHER);
+        SparseArray<Vote> otherDisplayVotes = mStorage.getVotes(DISPLAY_ID_OTHER);
         assertEquals(1, otherDisplayVotes.size());
 
-        vote = otherDisplayVotes.get(DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE);
+        vote = otherDisplayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
         assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
         assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
     }
@@ -122,25 +119,29 @@
     public void testNotifyWithDefaultVotesChangeFromCriticalToSevere() {
         // GIVEN 2 displays with no thermalThrottling config AND temperature level CRITICAL
         mObserver.observe();
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
         mObserver.notifyThrottling(createTemperature(Temperature.THROTTLING_CRITICAL));
         // WHEN thermal sensor notifies SEVERE
         mObserver.notifyThrottling(createTemperature(Temperature.THROTTLING_SEVERE));
         mHandler.flush();
         // THEN all votes with PRIORITY_SKIN_TEMPERATURE are removed from the storage
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
     }
 
     @Test
     public void testNotifyWithDefaultVotesForSevere() {
         // GIVEN 2 displays with no thermalThrottling config
         mObserver.observe();
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
         // WHEN thermal sensor notifies CRITICAL
         mObserver.notifyThrottling(createTemperature(Temperature.THROTTLING_SEVERE));
         mHandler.flush();
         // THEN nothing is added to the storage
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
     }
 
     @Test
@@ -155,18 +156,20 @@
         mObserver = new SkinThermalStatusObserver(mInjector, mStorage, mHandler);
         mObserver.observe();
         mObserver.onDisplayChanged(DISPLAY_ID);
-        assertEquals(0, mStorage.mVoteRegistry.size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
         // WHEN thermal sensor notifies temperature above configured
         mObserver.notifyThrottling(createTemperature(Temperature.THROTTLING_SEVERE));
         mHandler.flush();
         // THEN vote with refreshRate from config is added to the storage
-        assertEquals(1, mStorage.mVoteRegistry.size());
-        SparseArray<DisplayModeDirector.Vote> displayVotes = mStorage.mVoteRegistry.get(DISPLAY_ID);
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
+
+        SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
         assertEquals(1, displayVotes.size());
-        DisplayModeDirector.Vote vote = displayVotes.get(
-                DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE);
+        Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
         assertEquals(90, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
         assertEquals(120, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
     }
 
     @Test
@@ -178,14 +181,13 @@
         mObserver.onDisplayAdded(DISPLAY_ID_ADDED);
         mHandler.flush();
         // THEN 3rd vote is added to storage with (0,60) render refresh rate(default behaviour)
-        assertEquals(3, mStorage.mVoteRegistry.size());
+        assertEquals(1, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(1, mStorage.getVotes(DISPLAY_ID_OTHER).size());
+        assertEquals(1, mStorage.getVotes(DISPLAY_ID_ADDED).size());
 
-        SparseArray<DisplayModeDirector.Vote> displayVotes = mStorage.mVoteRegistry.get(
-                DISPLAY_ID_ADDED);
-        assertEquals(1, displayVotes.size());
+        SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID_ADDED);
 
-        DisplayModeDirector.Vote vote = displayVotes.get(
-                DisplayModeDirector.Vote.PRIORITY_SKIN_TEMPERATURE);
+        Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
         assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
         assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
     }
@@ -200,9 +202,9 @@
         mObserver.onDisplayRemoved(DISPLAY_ID_ADDED);
         mHandler.flush();
         // THEN there are 2 votes in registry
-        assertEquals(2, mStorage.mVoteRegistry.size());
-        assertNotNull(mStorage.mVoteRegistry.get(DISPLAY_ID));
-        assertNotNull(mStorage.mVoteRegistry.get(DISPLAY_ID_OTHER));
+        assertEquals(1, mStorage.getVotes(DISPLAY_ID).size());
+        assertEquals(1, mStorage.getVotes(DISPLAY_ID_OTHER).size());
+        assertEquals(0, mStorage.getVotes(DISPLAY_ID_ADDED).size());
     }
 
     private static Temperature createTemperature(@Temperature.ThrottlingStatus int status) {
@@ -259,27 +261,4 @@
             return false;
         }
     }
-
-
-    private static class FakeVoteStorage implements DisplayModeDirector.BallotBox {
-        private final SparseArray<SparseArray<DisplayModeDirector.Vote>> mVoteRegistry =
-                new SparseArray<>();
-
-        @Override
-        public void vote(int displayId, int priority, DisplayModeDirector.Vote vote) {
-            SparseArray<DisplayModeDirector.Vote> votesPerDisplay = mVoteRegistry.get(displayId);
-            if (votesPerDisplay == null) {
-                votesPerDisplay = new SparseArray<>();
-                mVoteRegistry.put(displayId, votesPerDisplay);
-            }
-            if (vote == null) {
-                votesPerDisplay.remove(priority);
-            } else {
-                votesPerDisplay.put(priority, vote);
-            }
-            if (votesPerDisplay.size() == 0) {
-                mVoteRegistry.remove(displayId);
-            }
-        }
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/display/mode/VotesStorageTest.java b/services/tests/servicestests/src/com/android/server/display/mode/VotesStorageTest.java
new file mode 100644
index 0000000..287fdd5
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/display/mode/VotesStorageTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class VotesStorageTest {
+    private static final int DISPLAY_ID = 100;
+    private static final int PRIORITY = Vote.PRIORITY_APP_REQUEST_SIZE;
+    private static final Vote VOTE = Vote.forDisableRefreshRateSwitching();
+    private static final int DISPLAY_ID_OTHER = 101;
+    private static final int PRIORITY_OTHER = Vote.PRIORITY_FLICKER_REFRESH_RATE;
+    private static final Vote VOTE_OTHER = Vote.forBaseModeRefreshRate(10f);
+
+    @Mock
+    public VotesStorage.Listener mVotesListener;
+
+    private VotesStorage mVotesStorage;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mVotesStorage = new VotesStorage(mVotesListener);
+    }
+
+    @Test
+    public void addsVoteToStorage() {
+        // WHEN updateVote is called
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        // THEN vote is added to the storage
+        SparseArray<Vote> votes = mVotesStorage.getVotes(DISPLAY_ID);
+        assertThat(votes.size()).isEqualTo(1);
+        assertThat(votes.get(PRIORITY)).isEqualTo(VOTE);
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID_OTHER).size()).isEqualTo(0);
+    }
+
+    @Test
+    public void notifiesVoteListenerIfVoteAdded() {
+        // WHEN updateVote is called
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        // THEN listener is notified
+        verify(mVotesListener).onChanged();
+    }
+
+    @Test
+    public void addsAnotherVoteToStorageWithDifferentPriority() {
+        // GIVEN vote storage with one vote
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        // WHEN updateVote is called with other priority
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY_OTHER, VOTE_OTHER);
+        // THEN another vote is added to storage
+        SparseArray<Vote> votes = mVotesStorage.getVotes(DISPLAY_ID);
+        assertThat(votes.size()).isEqualTo(2);
+        assertThat(votes.get(PRIORITY)).isEqualTo(VOTE);
+        assertThat(votes.get(PRIORITY_OTHER)).isEqualTo(VOTE_OTHER);
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID_OTHER).size()).isEqualTo(0);
+    }
+
+    @Test
+    public void replacesVoteInStorageForSamePriority() {
+        // GIVEN vote storage with one vote
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        // WHEN updateVote is called with same priority
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE_OTHER);
+        // THEN vote is replaced by other vote
+        SparseArray<Vote> votes = mVotesStorage.getVotes(DISPLAY_ID);
+        assertThat(votes.size()).isEqualTo(1);
+        assertThat(votes.get(PRIORITY)).isEqualTo(VOTE_OTHER);
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID_OTHER).size()).isEqualTo(0);
+    }
+
+    @Test
+    public void removesVoteInStorageForSamePriority() {
+        // GIVEN vote storage with one vote
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, VOTE);
+        // WHEN update is called with same priority and null vote
+        mVotesStorage.updateVote(DISPLAY_ID, PRIORITY, null);
+        // THEN vote removed from the storage
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID).size()).isEqualTo(0);
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID_OTHER).size()).isEqualTo(0);
+    }
+
+    @Test
+    public void addsGlobalDisplayVoteToStorage() {
+        // WHEN updateGlobalVote is called
+        mVotesStorage.updateGlobalVote(PRIORITY, VOTE);
+        // THEN it is added to the storage for every display
+        SparseArray<Vote> votes = mVotesStorage.getVotes(DISPLAY_ID);
+        assertThat(votes.size()).isEqualTo(1);
+        assertThat(votes.get(PRIORITY)).isEqualTo(VOTE);
+        votes = mVotesStorage.getVotes(DISPLAY_ID_OTHER);
+        assertThat(votes.size()).isEqualTo(1);
+        assertThat(votes.get(PRIORITY)).isEqualTo(VOTE);
+    }
+
+    @Test
+    public void ignoresVotesWithLowerThanMinPriority() {
+        // WHEN updateVote is called with invalid (lower than min) priority
+        mVotesStorage.updateVote(DISPLAY_ID, Vote.MIN_PRIORITY - 1, VOTE);
+        // THEN vote is not added to the storage AND listener not notified
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID).size()).isEqualTo(0);
+        verify(mVotesListener, never()).onChanged();
+    }
+
+    @Test
+    public void ignoresVotesWithGreaterThanMaxPriority() {
+        // WHEN updateVote is called with invalid (greater than max) priority
+        mVotesStorage.updateVote(DISPLAY_ID, Vote.MAX_PRIORITY + 1, VOTE);
+        // THEN vote is not added to the storage AND listener not notified
+        assertThat(mVotesStorage.getVotes(DISPLAY_ID).size()).isEqualTo(0);
+        verify(mVotesListener, never()).onChanged();
+    }
+}