• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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