1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.input; 18 19 import static android.view.PointerIcon.DEFAULT_POINTER_SCALE; 20 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK; 21 import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE; 22 23 import android.annotation.NonNull; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.hardware.display.DisplayManager; 27 import android.os.Handler; 28 import android.util.Slog; 29 import android.util.SparseArray; 30 import android.util.SparseDoubleArray; 31 import android.util.SparseIntArray; 32 import android.view.ContextThemeWrapper; 33 import android.view.Display; 34 import android.view.DisplayInfo; 35 import android.view.PointerIcon; 36 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.server.UiThread; 40 41 import java.util.Objects; 42 43 /** 44 * A thread-safe component of {@link InputManagerService} responsible for caching loaded 45 * {@link PointerIcon}s and triggering reloading of the icons. 46 */ 47 final class PointerIconCache { 48 private static final String TAG = PointerIconCache.class.getSimpleName(); 49 50 private final Context mContext; 51 52 // Do not hold the lock when calling into native code. 53 private final NativeInputManagerService mNative; 54 55 // We use the UI thread for loading pointer icons. 56 private final Handler mUiThreadHandler; 57 58 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 59 private final SparseArray<SparseArray<PointerIcon>> mLoadedPointerIconsByDisplayAndType = 60 new SparseArray<>(); 61 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 62 private boolean mUseLargePointerIcons = false; 63 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 64 private final SparseArray<Context> mDisplayContexts = new SparseArray<>(); 65 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 66 private final SparseIntArray mDisplayDensities = new SparseIntArray(); 67 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 68 private @PointerIcon.PointerIconVectorStyleFill int mPointerIconFillStyle = 69 POINTER_ICON_VECTOR_STYLE_FILL_BLACK; 70 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 71 private @PointerIcon.PointerIconVectorStyleStroke int mPointerIconStrokeStyle = 72 POINTER_ICON_VECTOR_STYLE_STROKE_WHITE; 73 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 74 private float mPointerIconScale = DEFAULT_POINTER_SCALE; 75 // Note that android doesn't have SparseFloatArray, so this falls back to use double instead. 76 @GuardedBy("mLoadedPointerIconsByDisplayAndType") 77 private final SparseDoubleArray mAccessibilityScaleFactorPerDisplay = new SparseDoubleArray(); 78 79 private final DisplayManager.DisplayListener mDisplayListener = 80 new DisplayManager.DisplayListener() { 81 @Override 82 public void onDisplayAdded(int displayId) { 83 synchronized (mLoadedPointerIconsByDisplayAndType) { 84 updateDisplayDensityLocked(displayId); 85 } 86 } 87 88 @Override 89 public void onDisplayRemoved(int displayId) { 90 synchronized (mLoadedPointerIconsByDisplayAndType) { 91 mLoadedPointerIconsByDisplayAndType.remove(displayId); 92 mDisplayContexts.remove(displayId); 93 mDisplayDensities.delete(displayId); 94 mAccessibilityScaleFactorPerDisplay.delete(displayId); 95 } 96 } 97 98 @Override 99 public void onDisplayChanged(int displayId) { 100 handleDisplayChanged(displayId); 101 } 102 }; 103 PointerIconCache(Context context, NativeInputManagerService nativeService)104 /* package */ PointerIconCache(Context context, NativeInputManagerService nativeService) { 105 this(context, nativeService, UiThread.getHandler()); 106 } 107 108 @VisibleForTesting PointerIconCache(Context context, NativeInputManagerService nativeService, Handler handler)109 /* package */ PointerIconCache(Context context, NativeInputManagerService nativeService, 110 Handler handler) { 111 mContext = context; 112 mNative = nativeService; 113 mUiThreadHandler = handler; 114 } 115 systemRunning()116 public void systemRunning() { 117 final DisplayManager displayManager = Objects.requireNonNull( 118 mContext.getSystemService(DisplayManager.class)); 119 displayManager.registerDisplayListener(mDisplayListener, mUiThreadHandler); 120 final Display[] displays = displayManager.getDisplays(); 121 for (int i = 0; i < displays.length; i++) { 122 mDisplayListener.onDisplayAdded(displays[i].getDisplayId()); 123 } 124 } 125 monitor()126 public void monitor() { 127 synchronized (mLoadedPointerIconsByDisplayAndType) { /* Test if blocked by lock */} 128 } 129 130 /** Set whether the large pointer icons should be used for accessibility. */ setUseLargePointerIcons(boolean useLargeIcons)131 public void setUseLargePointerIcons(boolean useLargeIcons) { 132 mUiThreadHandler.post(() -> handleSetUseLargePointerIcons(useLargeIcons)); 133 } 134 135 /** Set the fill style for vector pointer icons. */ setPointerFillStyle(@ointerIcon.PointerIconVectorStyleFill int fillStyle)136 public void setPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill int fillStyle) { 137 mUiThreadHandler.post(() -> handleSetPointerFillStyle(fillStyle)); 138 } 139 140 /** Set the stroke style for vector pointer icons. */ setPointerStrokeStyle(@ointerIcon.PointerIconVectorStyleStroke int strokeStyle)141 public void setPointerStrokeStyle(@PointerIcon.PointerIconVectorStyleStroke int strokeStyle) { 142 mUiThreadHandler.post(() -> handleSetPointerStrokeStyle(strokeStyle)); 143 } 144 145 /** Set the scale for vector pointer icons. */ setPointerScale(float scale)146 public void setPointerScale(float scale) { 147 mUiThreadHandler.post(() -> handleSetPointerScale(scale)); 148 } 149 150 /** Set the scale for accessibility (magnification) for vector pointer icons. */ setAccessibilityScaleFactor(int displayId, float scaleFactor)151 public void setAccessibilityScaleFactor(int displayId, float scaleFactor) { 152 mUiThreadHandler.post(() -> handleAccessibilityScaleFactor(displayId, scaleFactor)); 153 } 154 155 /** 156 * Get a loaded system pointer icon. This will fetch the icon from the cache, or load it if 157 * it isn't already cached. 158 */ getLoadedPointerIcon(int displayId, int type)159 public @NonNull PointerIcon getLoadedPointerIcon(int displayId, int type) { 160 synchronized (mLoadedPointerIconsByDisplayAndType) { 161 SparseArray<PointerIcon> iconsByType = mLoadedPointerIconsByDisplayAndType.get( 162 displayId); 163 if (iconsByType == null) { 164 iconsByType = new SparseArray<>(); 165 mLoadedPointerIconsByDisplayAndType.put(displayId, iconsByType); 166 } 167 PointerIcon icon = iconsByType.get(type); 168 if (icon == null) { 169 Context context = getContextForDisplayLocked(displayId); 170 Resources.Theme theme = context.getResources().newTheme(); 171 theme.setTo(context.getTheme()); 172 theme.applyStyle(PointerIcon.vectorFillStyleToResource(mPointerIconFillStyle), 173 /* force= */ true); 174 theme.applyStyle(PointerIcon.vectorStrokeStyleToResource(mPointerIconStrokeStyle), 175 /* force= */ true); 176 final float scale = mPointerIconScale 177 * (float) mAccessibilityScaleFactorPerDisplay.get(displayId, 1f); 178 icon = PointerIcon.getLoadedSystemIcon(new ContextThemeWrapper(context, theme), 179 type, mUseLargePointerIcons, scale); 180 iconsByType.put(type, icon); 181 } 182 return Objects.requireNonNull(icon); 183 } 184 } 185 186 @GuardedBy("mLoadedPointerIconsByDisplayAndType") getContextForDisplayLocked(int displayId)187 private @NonNull Context getContextForDisplayLocked(int displayId) { 188 if (displayId == Display.INVALID_DISPLAY) { 189 // Fallback to using the default context. 190 return mContext; 191 } 192 if (displayId == mContext.getDisplay().getDisplayId()) { 193 return mContext; 194 } 195 196 Context displayContext = mDisplayContexts.get(displayId); 197 if (displayContext == null) { 198 final DisplayManager displayManager = Objects.requireNonNull( 199 mContext.getSystemService(DisplayManager.class)); 200 final Display display = displayManager.getDisplay(displayId); 201 if (display == null) { 202 // Fallback to using the default context. 203 return mContext; 204 } 205 206 displayContext = mContext.createDisplayContext(display); 207 mDisplayContexts.put(displayId, displayContext); 208 } 209 return displayContext; 210 } 211 212 @android.annotation.UiThread handleDisplayChanged(int displayId)213 private void handleDisplayChanged(int displayId) { 214 synchronized (mLoadedPointerIconsByDisplayAndType) { 215 if (!updateDisplayDensityLocked(displayId)) { 216 return; 217 } 218 // The display density changed, so force all cached pointer icons to be 219 // reloaded for the display. 220 Slog.i(TAG, "Reloading pointer icons due to density change on display: " + displayId); 221 var iconsByType = mLoadedPointerIconsByDisplayAndType.get(displayId); 222 if (iconsByType == null) { 223 return; 224 } 225 iconsByType.clear(); 226 mDisplayContexts.remove(displayId); 227 } 228 mNative.reloadPointerIcons(); 229 } 230 231 @android.annotation.UiThread handleSetUseLargePointerIcons(boolean useLargeIcons)232 private void handleSetUseLargePointerIcons(boolean useLargeIcons) { 233 synchronized (mLoadedPointerIconsByDisplayAndType) { 234 if (mUseLargePointerIcons == useLargeIcons) { 235 return; 236 } 237 mUseLargePointerIcons = useLargeIcons; 238 // Clear all cached icons on all displays. 239 mLoadedPointerIconsByDisplayAndType.clear(); 240 } 241 mNative.reloadPointerIcons(); 242 } 243 244 @android.annotation.UiThread handleSetPointerFillStyle(@ointerIcon.PointerIconVectorStyleFill int fillStyle)245 private void handleSetPointerFillStyle(@PointerIcon.PointerIconVectorStyleFill int fillStyle) { 246 synchronized (mLoadedPointerIconsByDisplayAndType) { 247 if (mPointerIconFillStyle == fillStyle) { 248 return; 249 } 250 mPointerIconFillStyle = fillStyle; 251 // Clear all cached icons on all displays. 252 mLoadedPointerIconsByDisplayAndType.clear(); 253 } 254 mNative.reloadPointerIcons(); 255 } 256 257 @android.annotation.UiThread handleSetPointerStrokeStyle( @ointerIcon.PointerIconVectorStyleStroke int strokeStyle)258 private void handleSetPointerStrokeStyle( 259 @PointerIcon.PointerIconVectorStyleStroke int strokeStyle) { 260 synchronized (mLoadedPointerIconsByDisplayAndType) { 261 if (mPointerIconStrokeStyle == strokeStyle) { 262 return; 263 } 264 mPointerIconStrokeStyle = strokeStyle; 265 // Clear all cached icons on all displays. 266 mLoadedPointerIconsByDisplayAndType.clear(); 267 } 268 mNative.reloadPointerIcons(); 269 } 270 271 @android.annotation.UiThread handleSetPointerScale(float scale)272 private void handleSetPointerScale(float scale) { 273 synchronized (mLoadedPointerIconsByDisplayAndType) { 274 if (mPointerIconScale == scale) { 275 return; 276 } 277 mPointerIconScale = scale; 278 // Clear all cached icons on all displays. 279 mLoadedPointerIconsByDisplayAndType.clear(); 280 } 281 mNative.reloadPointerIcons(); 282 } 283 284 @android.annotation.UiThread handleAccessibilityScaleFactor(int displayId, float scale)285 private void handleAccessibilityScaleFactor(int displayId, float scale) { 286 synchronized (mLoadedPointerIconsByDisplayAndType) { 287 if (mAccessibilityScaleFactorPerDisplay.get(displayId, 1f) == scale) { 288 return; 289 } 290 mAccessibilityScaleFactorPerDisplay.put(displayId, scale); 291 // Clear cached icons on the display. 292 mLoadedPointerIconsByDisplayAndType.remove(displayId); 293 } 294 mNative.reloadPointerIcons(); 295 } 296 297 // Updates the cached display density for the given displayId, and returns true if 298 // the cached density changed. 299 @GuardedBy("mLoadedPointerIconsByDisplayAndType") updateDisplayDensityLocked(int displayId)300 private boolean updateDisplayDensityLocked(int displayId) { 301 final DisplayManager displayManager = Objects.requireNonNull( 302 mContext.getSystemService(DisplayManager.class)); 303 final Display display = displayManager.getDisplay(displayId); 304 if (display == null) { 305 return false; 306 } 307 DisplayInfo info = new DisplayInfo(); 308 display.getDisplayInfo(info); 309 final int oldDensity = mDisplayDensities.get(displayId, 0 /* default */); 310 if (oldDensity == info.logicalDensityDpi) { 311 return false; 312 } 313 mDisplayDensities.put(displayId, info.logicalDensityDpi); 314 return true; 315 } 316 } 317