diff --git a/device.mk b/device.mk index 5b890942e7da217a846e15abe55129964978e8d9..0e7c3bc31d299c87f5a4b7844cd158c941cb2cac 100644 --- a/device.mk +++ b/device.mk @@ -23,6 +23,9 @@ PRODUCT_PACKAGES += \ checkpoint_gc \ otapreopt_script +PRODUCT_PACKAGES += \ + RefreshRateController + PRODUCT_PACKAGES += \ update_engine \ update_engine_client \ diff --git a/display/Android.bp b/display/Android.bp new file mode 100644 index 0000000000000000000000000000000000000000..326704303037037b09d0cf746ba3fd9bf91aa71d --- /dev/null +++ b/display/Android.bp @@ -0,0 +1,13 @@ +// +// Copyright (C) 2025 E FOUNDATION +// SPDX-License-Identifier: Apache-2.0 +// + +android_app { + name: "RefreshRateController", + srcs: ["src/**/*.java"], + certificate: "platform", + platform_apis: true, + privileged: true, + system_ext_specific: true, +} diff --git a/display/AndroidManifest.xml b/display/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..bbda4a1529d57a6d3b8c166bc6cc61558684d027 --- /dev/null +++ b/display/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateBroadcastReceiver.java b/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateBroadcastReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..10af290dc209a2f8b48a281f996019a2956ed1f0 --- /dev/null +++ b/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateBroadcastReceiver.java @@ -0,0 +1,27 @@ +package foundation.e.refreshratecontroller; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class RefreshRateBroadcastReceiver extends BroadcastReceiver { + + private static final String TAG = "RefreshRateBroadcastReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action == null) { + return; + } + + Log.d(TAG, "Received action: " + action); + + if (action.equals(Intent.ACTION_LOCKED_BOOT_COMPLETED)) { + Log.d(TAG, "Starting RefreshRateMonitoringService..."); + Intent serviceIntent = new Intent(context, RefreshRateMonitoringService.class); + context.startService(serviceIntent); + } + } +} diff --git a/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateMonitoringService.java b/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateMonitoringService.java new file mode 100644 index 0000000000000000000000000000000000000000..172b1e81a4065f89310d29171deb4bb3b15168fe --- /dev/null +++ b/display/src/main/java/foundation/e/refreshratecontroller/RefreshRateMonitoringService.java @@ -0,0 +1,233 @@ +package foundation.e.refreshratecontroller; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.IBinder; +import android.provider.Settings; +import android.util.Log; +import android.view.Display; + +public class RefreshRateMonitoringService extends Service implements DisplayManager.DisplayListener { + + private static final String TAG = "RefreshRateMonitoringService"; + private static final String MIN_REFRESH_RATE_KEY = Settings.System.MIN_REFRESH_RATE; + private static final long APPLY_DELAY_MILLIS = 250; + private static final float MIN_FPS = 0f; + private static final float MAX_FPS = 90.0f; + + private int currentDisplayState = Display.STATE_UNKNOWN; + private int currentDisplayModeId = Display.Mode.INVALID_MODE_ID; + private float preferredMinFPS = MIN_FPS; + private boolean isRefreshRateSwitchInProgress = false; + + private DisplayManager displayManager; + private Handler handler; + + // RefreshRateMonitoringService class methods + + private void setSystemSettingFloat(String key, float value) { + try { + boolean setResult = Settings.System.putFloat(getContentResolver(), key, value); + if (setResult) { + Log.d(TAG, "Successfully set '" + key + "' to " + value); + } else { + Log.e(TAG, "Failed to set '" + key + "'."); + } + } catch (SecurityException e) { + Log.e(TAG, "Permission denied for key '" + key + "'.", e); + } catch (Exception e) { + Log.e(TAG, "Unexpected error while modifying '" + key + "'.", e); + } + } + private void resetRefreshRateOverrides() { + Log.d(TAG, "Setting MIN_REFRESH_RATE_KEY to " + MIN_FPS); + setSystemSettingFloat(MIN_REFRESH_RATE_KEY, MIN_FPS); + } + + private void applyDesiredRefreshRateOverrides() { + Log.d(TAG, "Setting MIN_REFRESH_RATE_KEY to " + preferredMinFPS); + setSystemSettingFloat(MIN_REFRESH_RATE_KEY, preferredMinFPS); + } + + private Runnable applyDesiredRefreshRatesRunnable = new Runnable() { + @Override + public void run() { + Log.d(TAG, "Executing delayed refresh rate application..."); + applyDesiredRefreshRateOverrides(); + isRefreshRateSwitchInProgress = false; + } + }; + + private String displayStateToString(int state) { + switch (state) { + case Display.STATE_OFF: return "STATE_OFF"; + case Display.STATE_ON: return "STATE_ON"; + case Display.STATE_DOZE: return "STATE_DOZE"; + case Display.STATE_DOZE_SUSPEND: return "STATE_DOZE_SUSPEND"; + case Display.STATE_UNKNOWN: return "STATE_UNKNOWN"; + default: return "UNKNOWN_STATE(" + state + ")"; + } + } + + private void logDisplayState(Display display) { + if (display != null) { + Log.d(TAG, "Display " + display.getDisplayId() + " state: " + displayStateToString(display.getState())); + } + } + + private void logDisplayInfo(Display display) { + if (display != null) { + Display.Mode currentMode = display.getMode(); + Log.d(TAG, "Display ID=" + display.getDisplayId() + + ", Mode=" + currentMode.getModeId() + + ", Width=" + currentMode.getPhysicalWidth() + + ", Height=" + currentMode.getPhysicalHeight() + + ", RefreshRate=" + currentMode.getRefreshRate()); + } + } + + private void initializeDisplay() { + Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); + if (defaultDisplay != null) { + currentDisplayState = defaultDisplay.getState(); + currentDisplayModeId = defaultDisplay.getMode().getModeId(); + + preferredMinFPS = getMinRefreshRateFromKey("onCreate"); + if (preferredMinFPS == Float.POSITIVE_INFINITY) { + preferredMinFPS = MAX_FPS; + } else if (preferredMinFPS == MIN_FPS) { + preferredMinFPS = defaultDisplay.getMode().getRefreshRate(); + } + + logDisplayState(defaultDisplay); + logDisplayInfo(defaultDisplay); + + isRefreshRateSwitchInProgress = true; + resetRefreshRateOverrides(); + handler.postDelayed(applyDesiredRefreshRatesRunnable, APPLY_DELAY_MILLIS); + } + } + + private void handleDisplayChange(Display display) { + int newState = display.getState(); + int newModeId = display.getMode().getModeId(); + logDisplayState(display); + + if (newState != currentDisplayState) { + Log.d(TAG, "Display state changed from " + displayStateToString(currentDisplayState) + " to " + displayStateToString(newState)); + currentDisplayState = newState; + + if (newState == Display.STATE_ON) { + Log.d(TAG, "Display now ON. Scheduling refresh rate change."); + isRefreshRateSwitchInProgress = true; + resetRefreshRateOverrides(); + handler.postDelayed(applyDesiredRefreshRatesRunnable, APPLY_DELAY_MILLIS); + } + } + + if (newModeId != currentDisplayModeId) { + Log.d(TAG, "Display mode changed from " + currentDisplayModeId + " to " + newModeId); + currentDisplayModeId = newModeId; + + if (newState == Display.STATE_ON && !isRefreshRateSwitchInProgress) { + preferredMinFPS = getMinRefreshRateFromKey("onDisplayChanged"); + if (preferredMinFPS == Float.POSITIVE_INFINITY) { + preferredMinFPS = MAX_FPS; + } + } + } + + logDisplayInfo(display); + } + + private void resetDisplayState() { + currentDisplayState = Display.STATE_UNKNOWN; + currentDisplayModeId = Display.Mode.INVALID_MODE_ID; + preferredMinFPS = MIN_FPS; + handler.removeCallbacks(applyDesiredRefreshRatesRunnable); + } + + private float getMinRefreshRateFromKey(String stage) { + float currentMinRefreshRateFromKey = Settings.System.getFloat(getContentResolver(), MIN_REFRESH_RATE_KEY, MIN_FPS); + if (Math.round(currentMinRefreshRateFromKey) == 0) { + Log.d(TAG, "Stage = " + stage + ": currentMinRefreshRateFromKey not set"); + return MIN_FPS; + } else { + Log.d(TAG, "Stage = " + stage + ": currentMinRefreshRateFromKey = " + currentMinRefreshRateFromKey); + return currentMinRefreshRateFromKey; + } + } + + // Service class overrides + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "Service created"); + handler = new Handler(getMainLooper()); + displayManager = (DisplayManager) getSystemService(Context.DISPLAY_SERVICE); + if (displayManager != null) { + displayManager.registerDisplayListener(this, handler); + Log.d(TAG, "DisplayListener registered."); + initializeDisplay(); + } else { + Log.e(TAG, "Could not get DisplayManager service."); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "Service started"); + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "Service destroyed"); + handler.removeCallbacks(applyDesiredRefreshRatesRunnable); + if (displayManager != null) { + displayManager.unregisterDisplayListener(this); + Log.d(TAG, "DisplayListener unregistered."); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + // DisplayManager.DisplayListener overrides + + @Override + public void onDisplayAdded(int displayId) { + Log.d(TAG, "Display added: displayId = " + displayId); + if (displayId == Display.DEFAULT_DISPLAY) { + initializeDisplay(); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + Log.d(TAG, "Display removed: displayId = " + displayId); + if (displayId == Display.DEFAULT_DISPLAY) { + resetDisplayState(); + } + } + @Override + public void onDisplayChanged(int displayId) { + Log.d(TAG, "Display changed: displayId = " + displayId); + if (displayId != Display.DEFAULT_DISPLAY) { + return; + } + if (displayManager != null) { + Display display = displayManager.getDisplay(displayId); + if (display != null) { + handleDisplayChange(display); + } + } + } +}