1 /* 2 * Copyright (C) 2023 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 package com.android.launcher3.graphics; 17 18 19 import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE; 20 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 21 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 22 23 import static java.util.Objects.requireNonNullElse; 24 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.database.MatrixCursor; 29 import android.net.Uri; 30 import android.os.Binder; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.IBinder.DeathRecipient; 34 import android.os.Message; 35 import android.os.Messenger; 36 import android.text.TextUtils; 37 import android.util.Log; 38 39 import androidx.annotation.NonNull; 40 41 import com.android.launcher3.InvariantDeviceProfile; 42 import com.android.launcher3.InvariantDeviceProfile.GridOption; 43 import com.android.launcher3.LauncherAppState; 44 import com.android.launcher3.LauncherModel; 45 import com.android.launcher3.LauncherPrefs; 46 import com.android.launcher3.dagger.ApplicationContext; 47 import com.android.launcher3.dagger.LauncherAppSingleton; 48 import com.android.launcher3.model.BgDataModel; 49 import com.android.launcher3.shapes.IconShapeModel; 50 import com.android.launcher3.shapes.ShapesProvider; 51 import com.android.launcher3.util.ContentProviderProxy.ProxyProvider; 52 import com.android.launcher3.util.DaggerSingletonTracker; 53 import com.android.launcher3.util.Executors; 54 import com.android.launcher3.util.Preconditions; 55 import com.android.launcher3.util.RunnableList; 56 import com.android.systemui.shared.Flags; 57 58 import java.lang.ref.WeakReference; 59 import java.util.Arrays; 60 import java.util.Collections; 61 import java.util.Comparator; 62 import java.util.List; 63 import java.util.Optional; 64 import java.util.Set; 65 import java.util.concurrent.ConcurrentHashMap; 66 import java.util.concurrent.ExecutionException; 67 68 import javax.inject.Inject; 69 70 /** 71 * Exposes various launcher grid options and allows the caller to change them. 72 * APIs: 73 * /shape_options: List of various available shape options, where each has following fields 74 * shape_key: key of the shape option 75 * title: translated title of the shape option 76 * path: path of the shape, assuming drawn on 100x100 view port 77 * is_default: true if this shape option is currently set to the system 78 * 79 * /list_options: List the various available grid options, where each has following fields 80 * name: key of the grid option 81 * rows: number of rows in the grid 82 * cols: number of columns in the grid 83 * preview_count: number of previews available for this grid option. The preview uri 84 * looks like /preview/[grid-name]/[preview index starting with 0] 85 * is_default: true if this grid option is currently set to the system 86 * 87 * /get_preview: Open a file stream for the grid preview 88 * 89 * /default_grid: Call update to set the current shape and grid, with values 90 * shape_key: key of the shape to apply 91 * name: key of the grid to apply 92 */ 93 @LauncherAppSingleton 94 public class GridCustomizationsProxy implements ProxyProvider { 95 96 private static final String TAG = "GridCustomizationsProvider"; 97 98 // KEY_NAME is the name of the grid used internally while the KEY_GRID_TITLE is the translated 99 // string title of the grid. 100 private static final String KEY_NAME = "name"; 101 private static final String KEY_GRID_TITLE = "grid_title"; 102 private static final String KEY_ROWS = "rows"; 103 private static final String KEY_COLS = "cols"; 104 private static final String KEY_GRID_ICON_ID = "grid_icon_id"; 105 private static final String KEY_PREVIEW_COUNT = "preview_count"; 106 // is_default means if a certain option is currently set to the system 107 private static final String KEY_IS_DEFAULT = "is_default"; 108 private static final String KEY_SHAPE_KEY = "shape_key"; 109 private static final String KEY_SHAPE_TITLE = "shape_title"; 110 private static final String KEY_PATH = "path"; 111 112 // list_options is the key for grid option list 113 private static final String KEY_LIST_OPTIONS = "/list_options"; 114 private static final String KEY_SHAPE_OPTIONS = "/shape_options"; 115 // default_grid is for setting grid and shape to system settings 116 private static final String KEY_DEFAULT_GRID = "/default_grid"; 117 private static final String SET_SHAPE = "/shape"; 118 119 private static final String METHOD_GET_PREVIEW = "get_preview"; 120 121 private static final String GET_ICON_THEMED = "/get_icon_themed"; 122 private static final String SET_ICON_THEMED = "/set_icon_themed"; 123 private static final String ICON_THEMED = "/icon_themed"; 124 private static final String BOOLEAN_VALUE = "boolean_value"; 125 126 private static final String KEY_SURFACE_PACKAGE = "surface_package"; 127 private static final String KEY_CALLBACK = "callback"; 128 public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row"; 129 public static final String KEY_GRID_NAME = "grid_name"; 130 131 private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337; 132 private static final int MESSAGE_ID_UPDATE_SHAPE = 2586; 133 private static final int MESSAGE_ID_UPDATE_GRID = 7414; 134 private static final int MESSAGE_ID_UPDATE_COLOR = 856; 135 private static final int MESSAGE_ID_UPDATE_ICON_THEMED = 311; 136 137 // Set of all active previews used to track duplicate memory allocations 138 private final Set<PreviewLifecycleObserver> mActivePreviews = 139 Collections.newSetFromMap(new ConcurrentHashMap<>()); 140 141 private final Context mContext; 142 private final ThemeManager mThemeManager; 143 private final LauncherPrefs mPrefs; 144 private final InvariantDeviceProfile mIdp; 145 146 @Inject GridCustomizationsProxy( @pplicationContext Context context, ThemeManager themeManager, LauncherPrefs prefs, InvariantDeviceProfile idp, DaggerSingletonTracker lifeCycle )147 protected GridCustomizationsProxy( 148 @ApplicationContext Context context, 149 ThemeManager themeManager, 150 LauncherPrefs prefs, 151 InvariantDeviceProfile idp, 152 DaggerSingletonTracker lifeCycle 153 ) { 154 mContext = context; 155 mThemeManager = themeManager; 156 mPrefs = prefs; 157 mIdp = idp; 158 lifeCycle.addCloseable(() -> mActivePreviews.forEach(PreviewLifecycleObserver::binderDied)); 159 } 160 161 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)162 public Cursor query(Uri uri, String[] projection, String selection, 163 String[] selectionArgs, String sortOrder) { 164 String path = uri.getPath(); 165 if (path == null) { 166 return null; 167 } 168 169 switch (path) { 170 case KEY_SHAPE_OPTIONS: { 171 if (Flags.newCustomizationPickerUi()) { 172 MatrixCursor cursor = new MatrixCursor(new String[]{ 173 KEY_SHAPE_KEY, KEY_SHAPE_TITLE, KEY_PATH, KEY_IS_DEFAULT}); 174 String currentShapePath = mThemeManager.getIconState().getIconMask(); 175 Optional<IconShapeModel> selectedShape = Arrays.stream( 176 ShapesProvider.INSTANCE.getIconShapes()).filter( 177 shape -> shape.getPathString().equals(currentShapePath) 178 ).findFirst(); 179 // Handle default for when current shape doesn't match new shapes. 180 if (selectedShape.isEmpty()) { 181 selectedShape = Optional.of(Arrays.stream( 182 ShapesProvider.INSTANCE.getIconShapes() 183 ).findFirst().get()); 184 } 185 186 for (IconShapeModel shape : ShapesProvider.INSTANCE.getIconShapes()) { 187 cursor.newRow() 188 .add(KEY_SHAPE_KEY, shape.getKey()) 189 .add(KEY_SHAPE_TITLE, shape.getTitle()) 190 .add(KEY_PATH, shape.getPathString()) 191 .add(KEY_IS_DEFAULT, shape.equals(selectedShape.get())); 192 } 193 return cursor; 194 } else { 195 return null; 196 } 197 } 198 case KEY_LIST_OPTIONS: { 199 MatrixCursor cursor = new MatrixCursor(new String[]{ 200 KEY_NAME, KEY_GRID_TITLE, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, 201 KEY_IS_DEFAULT, KEY_GRID_ICON_ID}); 202 List<GridOption> gridOptionList = mIdp.parseAllGridOptions(mContext); 203 if (com.android.launcher3.Flags.oneGridSpecs()) { 204 gridOptionList.sort(Comparator 205 .comparingInt((GridOption option) -> option.numColumns) 206 .reversed()); 207 } 208 for (GridOption gridOption : gridOptionList) { 209 cursor.newRow() 210 .add(KEY_NAME, gridOption.name) 211 .add(KEY_GRID_TITLE, gridOption.gridTitle) 212 .add(KEY_ROWS, gridOption.numRows) 213 .add(KEY_COLS, gridOption.numColumns) 214 .add(KEY_PREVIEW_COUNT, 1) 215 .add(KEY_IS_DEFAULT, mIdp.numColumns == gridOption.numColumns 216 && mIdp.numRows == gridOption.numRows) 217 .add(KEY_GRID_ICON_ID, gridOption.gridIconId); 218 } 219 return cursor; 220 } 221 case GET_ICON_THEMED: 222 case ICON_THEMED: { 223 MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE}); 224 cursor.newRow().add(BOOLEAN_VALUE, mThemeManager.isMonoThemeEnabled() ? 1 : 0); 225 return cursor; 226 } 227 default: 228 return null; 229 } 230 } 231 232 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)233 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 234 String path = uri.getPath(); 235 if (path == null) { 236 return 0; 237 } 238 switch (path) { 239 case KEY_DEFAULT_GRID: { 240 if (Flags.newCustomizationPickerUi()) { 241 mPrefs.put(PREF_ICON_SHAPE, 242 requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), "")); 243 } 244 String gridName = values.getAsString(KEY_NAME); 245 // Verify that this is a valid grid option 246 GridOption match = null; 247 for (GridOption option : mIdp.parseAllGridOptions(mContext)) { 248 String name = option.name; 249 if (name != null && name.equals(gridName)) { 250 match = option; 251 break; 252 } 253 } 254 if (match == null) { 255 return 0; 256 } 257 258 mIdp.setCurrentGrid(mContext, gridName); 259 if (Flags.newCustomizationPickerUi()) { 260 try { 261 // Wait for device profile to be fully reloaded and applied to the launcher 262 loadModelSync(mContext); 263 } catch (ExecutionException | InterruptedException e) { 264 Log.e(TAG, "Fail to load model", e); 265 } 266 } 267 mContext.getContentResolver().notifyChange(uri, null); 268 return 1; 269 } 270 case SET_SHAPE: 271 if (Flags.newCustomizationPickerUi()) { 272 mPrefs.put(PREF_ICON_SHAPE, 273 requireNonNullElse(values.getAsString(KEY_SHAPE_KEY), "")); 274 } 275 return 1; 276 case ICON_THEMED: 277 case SET_ICON_THEMED: { 278 mThemeManager.setMonoThemeEnabled(values.getAsBoolean(BOOLEAN_VALUE)); 279 mContext.getContentResolver().notifyChange(uri, null); 280 return 1; 281 } 282 default: 283 return 0; 284 } 285 } 286 287 /** 288 * Loads the model in memory synchronously 289 */ loadModelSync(Context context)290 private void loadModelSync(Context context) throws ExecutionException, InterruptedException { 291 Preconditions.assertNonUiThread(); 292 BgDataModel.Callbacks emptyCallbacks = new BgDataModel.Callbacks() { }; 293 LauncherModel launcherModel = LauncherAppState.getInstance(context).getModel(); 294 MAIN_EXECUTOR.submit( 295 () -> launcherModel.addCallbacksAndLoad(emptyCallbacks) 296 ).get(); 297 298 Executors.MODEL_EXECUTOR.submit(() -> { }).get(); 299 MAIN_EXECUTOR.submit( 300 () -> launcherModel.removeCallbacks(emptyCallbacks) 301 ).get(); 302 } 303 304 @Override call(@onNull String method, String arg, Bundle extras)305 public Bundle call(@NonNull String method, String arg, Bundle extras) { 306 if (METHOD_GET_PREVIEW.equals(method)) { 307 return getPreview(extras); 308 } else { 309 return null; 310 } 311 } 312 getPreview(Bundle request)313 private synchronized Bundle getPreview(Bundle request) { 314 RunnableList lifeCycleTracker = new RunnableList(); 315 try { 316 PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer( 317 mContext, lifeCycleTracker, request, Binder.getCallingPid()); 318 PreviewLifecycleObserver observer = 319 new PreviewLifecycleObserver(lifeCycleTracker, renderer); 320 321 // Destroy previous renderers to avoid any duplicate memory 322 mActivePreviews.stream().filter(observer::isSameRenderer).forEach(o -> 323 MAIN_EXECUTOR.execute(o.lifeCycleTracker::executeAllAndDestroy)); 324 325 renderer.loadAsync(); 326 lifeCycleTracker.add(() -> renderer.getHostToken().unlinkToDeath(observer, 0)); 327 renderer.getHostToken().linkToDeath(observer, 0); 328 329 Bundle result = new Bundle(); 330 result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage()); 331 332 mActivePreviews.add(observer); 333 lifeCycleTracker.add(() -> mActivePreviews.remove(observer)); 334 335 // Wrap the callback in a weak reference. This ensures that the callback is not kept 336 // alive due to the Messenger's IBinder 337 Messenger messenger = new Messenger(new Handler( 338 UI_HELPER_EXECUTOR.getLooper(), 339 new WeakCallbackWrapper(observer))); 340 341 Message msg = Message.obtain(); 342 msg.replyTo = messenger; 343 result.putParcelable(KEY_CALLBACK, msg); 344 return result; 345 } catch (Exception e) { 346 Log.e(TAG, "Unable to generate preview", e); 347 MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy); 348 return null; 349 } 350 } 351 352 private static class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient { 353 354 public final RunnableList lifeCycleTracker; 355 public final PreviewSurfaceRenderer renderer; 356 public boolean destroyed = false; 357 PreviewLifecycleObserver( RunnableList lifeCycleTracker, PreviewSurfaceRenderer renderer)358 PreviewLifecycleObserver( 359 RunnableList lifeCycleTracker, 360 PreviewSurfaceRenderer renderer) { 361 this.lifeCycleTracker = lifeCycleTracker; 362 this.renderer = renderer; 363 lifeCycleTracker.add(() -> destroyed = true); 364 } 365 366 @Override handleMessage(Message message)367 public boolean handleMessage(Message message) { 368 if (destroyed) { 369 return true; 370 } 371 372 switch (message.what) { 373 case MESSAGE_ID_UPDATE_PREVIEW: 374 renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW)); 375 break; 376 case MESSAGE_ID_UPDATE_SHAPE: 377 if (Flags.newCustomizationPickerUi() 378 && com.android.launcher3.Flags.enableLauncherIconShapes()) { 379 String shapeKey = message.getData().getString(KEY_SHAPE_KEY); 380 if (!TextUtils.isEmpty(shapeKey)) { 381 renderer.updateShape(shapeKey); 382 } 383 } 384 break; 385 case MESSAGE_ID_UPDATE_GRID: 386 String gridName = message.getData().getString(KEY_GRID_NAME); 387 if (!TextUtils.isEmpty(gridName)) { 388 renderer.updateGrid(gridName); 389 } 390 break; 391 case MESSAGE_ID_UPDATE_COLOR: 392 if (Flags.newCustomizationPickerUi()) { 393 renderer.previewColor(message.getData()); 394 } 395 break; 396 case MESSAGE_ID_UPDATE_ICON_THEMED: 397 if (Flags.newCustomizationPickerUi()) { 398 Boolean iconThemed = message.getData().getBoolean(BOOLEAN_VALUE); 399 // TODO Update icon themed in the preview 400 } 401 break; 402 default: 403 // Unknown command, destroy lifecycle 404 Log.d(TAG, "Unknown preview command: " + message.what + ", destroying preview"); 405 MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy); 406 break; 407 } 408 409 return true; 410 } 411 412 @Override binderDied()413 public void binderDied() { 414 MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy); 415 } 416 417 /** 418 * Two renderers are considered same if they have the same host token and display Id 419 */ isSameRenderer(PreviewLifecycleObserver plo)420 public boolean isSameRenderer(PreviewLifecycleObserver plo) { 421 return plo != null 422 && plo.renderer.getHostToken().equals(renderer.getHostToken()) 423 && plo.renderer.getDisplayId() == renderer.getDisplayId(); 424 } 425 } 426 427 /** 428 * A WeakReference wrapper around Handler.Callback to avoid passing hard-reference over IPC 429 * when using a Messenger 430 */ 431 private static class WeakCallbackWrapper implements Handler.Callback { 432 433 private final WeakReference<Handler.Callback> mActual; 434 private final Message mCleanupMessage; 435 WeakCallbackWrapper(Handler.Callback actual)436 WeakCallbackWrapper(Handler.Callback actual) { 437 mActual = new WeakReference<>(actual); 438 mCleanupMessage = new Message(); 439 } 440 441 @Override handleMessage(Message message)442 public boolean handleMessage(Message message) { 443 Handler.Callback actual = mActual.get(); 444 return actual != null && actual.handleMessage(message); 445 } 446 447 @Override finalize()448 protected void finalize() throws Throwable { 449 super.finalize(); 450 Handler.Callback actual = mActual.get(); 451 if (actual != null) { 452 actual.handleMessage(mCleanupMessage); 453 } 454 } 455 } 456 } 457