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 import static com.android.launcher3.LauncherPrefs.THEMED_ICONS; 19 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 20 import static com.android.launcher3.util.Themes.isThemedIconEnabled; 21 22 import android.content.ContentProvider; 23 import android.content.ContentValues; 24 import android.content.pm.PackageManager; 25 import android.database.Cursor; 26 import android.database.MatrixCursor; 27 import android.net.Uri; 28 import android.os.Binder; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.os.IBinder; 32 import android.os.IBinder.DeathRecipient; 33 import android.os.Message; 34 import android.os.Messenger; 35 import android.util.ArrayMap; 36 import android.util.Log; 37 import android.util.Pair; 38 39 import com.android.launcher3.InvariantDeviceProfile; 40 import com.android.launcher3.InvariantDeviceProfile.GridOption; 41 import com.android.launcher3.LauncherPrefs; 42 import com.android.launcher3.util.Executors; 43 44 /** 45 * Exposes various launcher grid options and allows the caller to change them. 46 * APIs: 47 * /list_options: List the various available grip options, has following columns 48 * name: name of the grid 49 * rows: number of rows in the grid 50 * cols: number of columns in the grid 51 * preview_count: number of previews available for this grid option. The preview uri 52 * looks like /preview/<grid-name>/<preview index starting with 0> 53 * is_default: true if this grid is currently active 54 * 55 * /preview: Opens a file stream for the grid preview 56 * 57 * /default_grid: Call update to set the current grid, with values 58 * name: name of the grid to apply 59 */ 60 public class GridCustomizationsProvider extends ContentProvider { 61 62 private static final String TAG = "GridCustomizationsProvider"; 63 64 private static final String KEY_NAME = "name"; 65 private static final String KEY_ROWS = "rows"; 66 private static final String KEY_COLS = "cols"; 67 private static final String KEY_PREVIEW_COUNT = "preview_count"; 68 private static final String KEY_IS_DEFAULT = "is_default"; 69 70 private static final String KEY_LIST_OPTIONS = "/list_options"; 71 private static final String KEY_DEFAULT_GRID = "/default_grid"; 72 73 private static final String METHOD_GET_PREVIEW = "get_preview"; 74 75 private static final String GET_ICON_THEMED = "/get_icon_themed"; 76 private static final String SET_ICON_THEMED = "/set_icon_themed"; 77 private static final String ICON_THEMED = "/icon_themed"; 78 private static final String BOOLEAN_VALUE = "boolean_value"; 79 80 private static final String KEY_SURFACE_PACKAGE = "surface_package"; 81 private static final String KEY_CALLBACK = "callback"; 82 public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row"; 83 84 private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337; 85 86 /** 87 * Here we use the IBinder and the screen ID as the key of the active previews. 88 */ 89 private final ArrayMap<Pair<IBinder, Integer>, PreviewLifecycleObserver> mActivePreviews = 90 new ArrayMap<>(); 91 92 @Override onCreate()93 public boolean onCreate() { 94 return true; 95 } 96 97 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)98 public Cursor query(Uri uri, String[] projection, String selection, 99 String[] selectionArgs, String sortOrder) { 100 switch (uri.getPath()) { 101 case KEY_LIST_OPTIONS: { 102 MatrixCursor cursor = new MatrixCursor(new String[]{ 103 KEY_NAME, KEY_ROWS, KEY_COLS, KEY_PREVIEW_COUNT, KEY_IS_DEFAULT}); 104 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext()); 105 for (GridOption gridOption : idp.parseAllGridOptions(getContext())) { 106 cursor.newRow() 107 .add(KEY_NAME, gridOption.name) 108 .add(KEY_ROWS, gridOption.numRows) 109 .add(KEY_COLS, gridOption.numColumns) 110 .add(KEY_PREVIEW_COUNT, 1) 111 .add(KEY_IS_DEFAULT, idp.numColumns == gridOption.numColumns 112 && idp.numRows == gridOption.numRows); 113 } 114 return cursor; 115 } 116 case GET_ICON_THEMED: 117 case ICON_THEMED: { 118 MatrixCursor cursor = new MatrixCursor(new String[]{BOOLEAN_VALUE}); 119 cursor.newRow().add(BOOLEAN_VALUE, isThemedIconEnabled(getContext()) ? 1 : 0); 120 return cursor; 121 } 122 default: 123 return null; 124 } 125 } 126 127 @Override getType(Uri uri)128 public String getType(Uri uri) { 129 return "vnd.android.cursor.dir/launcher_grid"; 130 } 131 132 @Override insert(Uri uri, ContentValues initialValues)133 public Uri insert(Uri uri, ContentValues initialValues) { 134 return null; 135 } 136 137 @Override delete(Uri uri, String selection, String[] selectionArgs)138 public int delete(Uri uri, String selection, String[] selectionArgs) { 139 return 0; 140 } 141 142 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)143 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 144 switch (uri.getPath()) { 145 case KEY_DEFAULT_GRID: { 146 String gridName = values.getAsString(KEY_NAME); 147 InvariantDeviceProfile idp = InvariantDeviceProfile.INSTANCE.get(getContext()); 148 // Verify that this is a valid grid option 149 GridOption match = null; 150 for (GridOption option : idp.parseAllGridOptions(getContext())) { 151 if (option.name.equals(gridName)) { 152 match = option; 153 break; 154 } 155 } 156 if (match == null) { 157 return 0; 158 } 159 160 idp.setCurrentGrid(getContext(), gridName); 161 getContext().getContentResolver().notifyChange(uri, null); 162 return 1; 163 } 164 case ICON_THEMED: 165 case SET_ICON_THEMED: { 166 LauncherPrefs.get(getContext()) 167 .put(THEMED_ICONS, values.getAsBoolean(BOOLEAN_VALUE)); 168 getContext().getContentResolver().notifyChange(uri, null); 169 return 1; 170 } 171 default: 172 return 0; 173 } 174 } 175 176 @Override call(String method, String arg, Bundle extras)177 public Bundle call(String method, String arg, Bundle extras) { 178 if (getContext().checkPermission("android.permission.BIND_WALLPAPER", 179 Binder.getCallingPid(), Binder.getCallingUid()) 180 != PackageManager.PERMISSION_GRANTED) { 181 return null; 182 } 183 184 if (!METHOD_GET_PREVIEW.equals(method)) { 185 return null; 186 } 187 return getPreview(extras); 188 } 189 getPreview(Bundle request)190 private synchronized Bundle getPreview(Bundle request) { 191 PreviewLifecycleObserver observer = null; 192 try { 193 PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(getContext(), request); 194 195 observer = new PreviewLifecycleObserver(renderer); 196 // Destroy previous 197 destroyObserver(mActivePreviews.get(observer.getIdentifier())); 198 mActivePreviews.put(observer.getIdentifier(), observer); 199 200 renderer.loadAsync(); 201 renderer.getHostToken().linkToDeath(observer, 0); 202 203 Bundle result = new Bundle(); 204 result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage()); 205 206 Messenger messenger = 207 new Messenger(new Handler(UI_HELPER_EXECUTOR.getLooper(), observer)); 208 Message msg = Message.obtain(); 209 msg.replyTo = messenger; 210 result.putParcelable(KEY_CALLBACK, msg); 211 return result; 212 } catch (Exception e) { 213 Log.e(TAG, "Unable to generate preview", e); 214 if (observer != null) { 215 destroyObserver(observer); 216 } 217 return null; 218 } 219 } 220 destroyObserver(PreviewLifecycleObserver observer)221 private synchronized void destroyObserver(PreviewLifecycleObserver observer) { 222 if (observer == null || observer.destroyed) { 223 return; 224 } 225 observer.destroyed = true; 226 observer.renderer.getHostToken().unlinkToDeath(observer, 0); 227 Executors.MAIN_EXECUTOR.execute(observer.renderer::destroy); 228 PreviewLifecycleObserver cached = mActivePreviews.get(observer.getIdentifier()); 229 if (cached == observer) { 230 mActivePreviews.remove(observer.getIdentifier()); 231 } 232 } 233 234 private class PreviewLifecycleObserver implements Handler.Callback, DeathRecipient { 235 236 public final PreviewSurfaceRenderer renderer; 237 public boolean destroyed = false; 238 PreviewLifecycleObserver(PreviewSurfaceRenderer renderer)239 PreviewLifecycleObserver(PreviewSurfaceRenderer renderer) { 240 this.renderer = renderer; 241 } 242 243 @Override handleMessage(Message message)244 public boolean handleMessage(Message message) { 245 if (destroyed) { 246 return true; 247 } 248 if (message.what == MESSAGE_ID_UPDATE_PREVIEW) { 249 renderer.hideBottomRow(message.getData().getBoolean(KEY_HIDE_BOTTOM_ROW)); 250 } else { 251 destroyObserver(this); 252 } 253 return true; 254 } 255 256 @Override binderDied()257 public void binderDied() { 258 destroyObserver(this); 259 } 260 261 /** 262 * Returns a key that should make the PreviewSurfaceRenderer unique and if two of them have 263 * the same key they will be treated as the same PreviewSurfaceRenderer. Primary this is 264 * used to prevent memory leaks by removing the old PreviewSurfaceRenderer. 265 */ getIdentifier()266 public Pair<IBinder, Integer> getIdentifier() { 267 return new Pair<>(renderer.getHostToken(), renderer.getDisplayId()); 268 } 269 } 270 } 271