1 /* 2 * Copyright (C) 2022 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.celllayout; 17 18 import android.graphics.Point; 19 import android.graphics.Rect; 20 21 import java.util.ArrayDeque; 22 import java.util.ArrayList; 23 import java.util.HashMap; 24 import java.util.HashSet; 25 import java.util.List; 26 import java.util.Map; 27 import java.util.Queue; 28 import java.util.Set; 29 import java.util.stream.Collectors; 30 31 32 public class CellLayoutBoard implements Comparable<CellLayoutBoard> { 33 intersects(Rect r1, Rect r2)34 private boolean intersects(Rect r1, Rect r2) { 35 // If one rectangle is on left side of other 36 if (r1.left > r2.right || r2.left > r1.right) { 37 return false; 38 } 39 40 // If one rectangle is above other 41 if (r1.bottom > r2.top || r2.bottom > r1.top) { 42 return false; 43 } 44 45 return true; 46 } 47 overlapsWithIgnored(Set<Rect> ignoredRectangles, Rect rect)48 private boolean overlapsWithIgnored(Set<Rect> ignoredRectangles, Rect rect) { 49 for (Rect ignoredRect : ignoredRectangles) { 50 // Using the built in intersects doesn't work because it doesn't account for area 0 51 if (intersects(ignoredRect, rect)) { 52 return true; 53 } 54 } 55 return false; 56 } 57 58 @Override compareTo(CellLayoutBoard cellLayoutBoard)59 public int compareTo(CellLayoutBoard cellLayoutBoard) { 60 // to be equal they need to have the same number of widgets and the same dimensions 61 // their order can be different 62 Set<Rect> widgetsSet = new HashSet<>(); 63 Set<Rect> ignoredRectangles = new HashSet<>(); 64 for (WidgetRect rect : mWidgetsRects) { 65 if (rect.shouldIgnore()) { 66 ignoredRectangles.add(rect.mBounds); 67 } else { 68 widgetsSet.add(rect.mBounds); 69 } 70 } 71 for (WidgetRect rect : cellLayoutBoard.mWidgetsRects) { 72 // ignore rectangles overlapping with the area marked by x 73 if (overlapsWithIgnored(ignoredRectangles, rect.mBounds)) { 74 continue; 75 } 76 if (!widgetsSet.contains(rect.mBounds)) { 77 return -1; 78 } 79 widgetsSet.remove(rect.mBounds); 80 } 81 if (!widgetsSet.isEmpty()) { 82 return 1; 83 } 84 85 // to be equal they need to have the same number of icons their order can be different 86 Set<Point> iconsSet = new HashSet<>(); 87 mIconPoints.forEach(icon -> iconsSet.add(icon.getCoord())); 88 for (IconPoint icon : cellLayoutBoard.mIconPoints) { 89 if (!iconsSet.contains(icon.getCoord())) { 90 return -1; 91 } 92 iconsSet.remove(icon.getCoord()); 93 } 94 if (!iconsSet.isEmpty()) { 95 return 1; 96 } 97 return 0; 98 } 99 100 public static class CellType { 101 // The cells marked by this will be filled by 1x1 widgets and will be ignored when 102 // validating 103 public static final char IGNORE = 'x'; 104 // The cells marked by this will be filled by app icons 105 public static final char ICON = 'i'; 106 // The cells marked by FOLDER will be filled by folders with 27 app icons inside 107 public static final char FOLDER = 'Z'; 108 // Empty space 109 public static final char EMPTY = '-'; 110 // Widget that will be saved as "main widget" for easier retrieval 111 public static final char MAIN_WIDGET = 'm'; 112 // Everything else will be consider a widget 113 } 114 115 public static class WidgetRect { 116 public char mType; 117 public Rect mBounds; 118 WidgetRect(char type, Rect bounds)119 WidgetRect(char type, Rect bounds) { 120 this.mType = type; 121 this.mBounds = bounds; 122 } 123 getSpanX()124 int getSpanX() { 125 return mBounds.right - mBounds.left + 1; 126 } 127 getSpanY()128 int getSpanY() { 129 return mBounds.top - mBounds.bottom + 1; 130 } 131 getCellX()132 int getCellX() { 133 return mBounds.left; 134 } 135 getCellY()136 int getCellY() { 137 return mBounds.bottom; 138 } 139 shouldIgnore()140 boolean shouldIgnore() { 141 return this.mType == CellType.IGNORE; 142 } 143 contains(int x, int y)144 boolean contains(int x, int y) { 145 return mBounds.contains(x, y); 146 } 147 148 @Override toString()149 public String toString() { 150 return "WidgetRect type = " + mType + " x = " + getCellX() + " | y " + getCellY() 151 + " xs = " + getSpanX() + " ys = " + getSpanY(); 152 } 153 } 154 155 public static class IconPoint { 156 public Point coord; 157 public char mType; 158 IconPoint(Point coord, char type)159 public IconPoint(Point coord, char type) { 160 this.coord = coord; 161 mType = type; 162 } 163 getType()164 public char getType() { 165 return mType; 166 } 167 setType(char type)168 public void setType(char type) { 169 mType = type; 170 } 171 getCoord()172 public Point getCoord() { 173 return coord; 174 } 175 setCoord(Point coord)176 public void setCoord(Point coord) { 177 this.coord = coord; 178 } 179 } 180 181 public static class FolderPoint { 182 public Point coord; 183 public char mType; 184 FolderPoint(Point coord, char type)185 public FolderPoint(Point coord, char type) { 186 this.coord = coord; 187 mType = type; 188 } 189 190 /** 191 * [A-Z]: Represents a folder and number of icons in the folder is represented by 192 * the order of letter in the alphabet, A=2, B=3, C=4 ... etc. 193 */ getNumberIconsInside()194 public int getNumberIconsInside() { 195 return (mType - 'A') + 2; 196 } 197 } 198 199 200 private HashSet<Character> mUsedWidgetTypes = new HashSet<>(); 201 202 static final int INFINITE = 99999; 203 204 char[][] mWidget = new char[30][30]; 205 206 List<WidgetRect> mWidgetsRects = new ArrayList<>(); 207 Map<Character, WidgetRect> mWidgetsMap = new HashMap<>(); 208 209 List<IconPoint> mIconPoints = new ArrayList<>(); 210 List<FolderPoint> mFolderPoints = new ArrayList<>(); 211 212 WidgetRect mMain = null; 213 214 int mWidth, mHeight; 215 CellLayoutBoard()216 CellLayoutBoard() { 217 for (int x = 0; x < mWidget.length; x++) { 218 for (int y = 0; y < mWidget[0].length; y++) { 219 mWidget[x][y] = CellType.EMPTY; 220 } 221 } 222 } 223 CellLayoutBoard(int width, int height)224 CellLayoutBoard(int width, int height) { 225 mWidget = new char[width][height]; 226 this.mWidth = width; 227 this.mHeight = height; 228 for (int x = 0; x < mWidget.length; x++) { 229 for (int y = 0; y < mWidget[0].length; y++) { 230 mWidget[x][y] = CellType.EMPTY; 231 } 232 } 233 } 234 pointInsideRect(int x, int y, WidgetRect rect)235 public boolean pointInsideRect(int x, int y, WidgetRect rect) { 236 Boolean isXInRect = x >= rect.getCellX() && x < rect.getCellX() + rect.getSpanX(); 237 Boolean isYInRect = y >= rect.getCellY() && y < rect.getCellY() + rect.getSpanY(); 238 return isXInRect && isYInRect; 239 } 240 241 public WidgetRect getWidgetAt(int x, int y) { 242 return mWidgetsRects.stream() 243 .filter(widgetRect -> pointInsideRect(x, y, widgetRect)).findFirst().orElse(null); 244 } 245 getWidgets()246 public List<WidgetRect> getWidgets() { 247 return mWidgetsRects; 248 } 249 getIcons()250 public List<IconPoint> getIcons() { 251 return mIconPoints; 252 } 253 getFolders()254 public List<FolderPoint> getFolders() { 255 return mFolderPoints; 256 } 257 getMain()258 public WidgetRect getMain() { 259 return mMain; 260 } 261 getWidgetRect(char c)262 public WidgetRect getWidgetRect(char c) { 263 return mWidgetsMap.get(c); 264 } 265 removeWidgetFromBoard(WidgetRect widget)266 private void removeWidgetFromBoard(WidgetRect widget) { 267 for (int xi = widget.mBounds.left; xi < widget.mBounds.right; xi++) { 268 for (int yi = widget.mBounds.top; yi < widget.mBounds.bottom; yi++) { 269 mWidget[xi][yi] = '-'; 270 } 271 } 272 } 273 removeOverlappingItems(Rect rect)274 private void removeOverlappingItems(Rect rect) { 275 // Remove overlapping widgets and remove them from the board 276 mWidgetsRects = mWidgetsRects.stream().filter(widget -> { 277 if (rect.intersect(widget.mBounds)) { 278 removeWidgetFromBoard(widget); 279 return false; 280 } 281 return true; 282 }).collect(Collectors.toList()); 283 // Remove overlapping icons and remove them from the board 284 mIconPoints = mIconPoints.stream().filter(iconPoint -> { 285 int x = iconPoint.coord.x; 286 int y = iconPoint.coord.y; 287 if (rect.contains(x, y)) { 288 mWidget[x][y] = '-'; 289 return false; 290 } 291 return true; 292 }).collect(Collectors.toList()); 293 294 // Remove overlapping folders and remove them from the board 295 mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { 296 int x = folderPoint.coord.x; 297 int y = folderPoint.coord.y; 298 if (rect.contains(x, y)) { 299 mWidget[x][y] = '-'; 300 return false; 301 } 302 return true; 303 }).collect(Collectors.toList()); 304 } 305 removeOverlappingItems(Point p)306 private void removeOverlappingItems(Point p) { 307 // Remove overlapping widgets and remove them from the board 308 mWidgetsRects = mWidgetsRects.stream().filter(widget -> { 309 if (widget.mBounds.contains(p.x, p.y)) { 310 removeWidgetFromBoard(widget); 311 return false; 312 } 313 return true; 314 }).collect(Collectors.toList()); 315 // Remove overlapping icons and remove them from the board 316 mIconPoints = mIconPoints.stream().filter(iconPoint -> { 317 int x = iconPoint.coord.x; 318 int y = iconPoint.coord.y; 319 if (p.x == x && p.y == y) { 320 mWidget[x][y] = '-'; 321 return false; 322 } 323 return true; 324 }).collect(Collectors.toList()); 325 326 // Remove overlapping folders and remove them from the board 327 mFolderPoints = mFolderPoints.stream().filter(folderPoint -> { 328 int x = folderPoint.coord.x; 329 int y = folderPoint.coord.y; 330 if (p.x == x && p.y == y) { 331 mWidget[x][y] = '-'; 332 return false; 333 } 334 return true; 335 }).collect(Collectors.toList()); 336 } 337 getNextWidgetType()338 private char getNextWidgetType() { 339 for (char type = 'a'; type <= 'z'; type++) { 340 if (type == 'i') continue; 341 if (mUsedWidgetTypes.contains(type)) continue; 342 mUsedWidgetTypes.add(type); 343 return type; 344 } 345 return 'z'; 346 } 347 addWidget(int x, int y, int spanX, int spanY, char type)348 public void addWidget(int x, int y, int spanX, int spanY, char type) { 349 Rect rect = new Rect(x, y + spanY - 1, x + spanX - 1, y); 350 removeOverlappingItems(rect); 351 WidgetRect widgetRect = new WidgetRect(type, rect); 352 mWidgetsRects.add(widgetRect); 353 for (int xi = rect.left; xi < rect.right + 1; xi++) { 354 for (int yi = rect.bottom; yi < rect.top + 1; yi++) { 355 mWidget[xi][yi] = type; 356 } 357 } 358 } 359 addWidget(int x, int y, int spanX, int spanY)360 public void addWidget(int x, int y, int spanX, int spanY) { 361 addWidget(x, y, spanX, spanY, getNextWidgetType()); 362 } 363 addIcon(int x, int y)364 public void addIcon(int x, int y) { 365 Point iconCoord = new Point(x, y); 366 removeOverlappingItems(iconCoord); 367 mIconPoints.add(new IconPoint(iconCoord, CellType.ICON)); 368 mWidget[x][y] = 'i'; 369 } 370 getWidgetRect(int x, int y, Set<Point> used, char[][] board)371 public static WidgetRect getWidgetRect(int x, int y, Set<Point> used, char[][] board) { 372 char type = board[x][y]; 373 Queue<Point> search = new ArrayDeque<Point>(); 374 Point current = new Point(x, y); 375 search.add(current); 376 used.add(current); 377 List<Point> neighbors = new ArrayList<>(List.of( 378 new Point(-1, 0), 379 new Point(0, -1), 380 new Point(1, 0), 381 new Point(0, 1)) 382 ); 383 Rect widgetRect = new Rect(INFINITE, -INFINITE, -INFINITE, INFINITE); 384 while (!search.isEmpty()) { 385 current = search.poll(); 386 widgetRect.top = Math.max(widgetRect.top, current.y); 387 widgetRect.right = Math.max(widgetRect.right, current.x); 388 widgetRect.bottom = Math.min(widgetRect.bottom, current.y); 389 widgetRect.left = Math.min(widgetRect.left, current.x); 390 for (Point p : neighbors) { 391 Point next = new Point(current.x + p.x, current.y + p.y); 392 if (next.x < 0 || next.x >= board.length) continue; 393 if (next.y < 0 || next.y >= board[0].length) continue; 394 if (board[next.x][next.y] == type && !used.contains(next)) { 395 used.add(next); 396 search.add(next); 397 } 398 } 399 } 400 return new WidgetRect(type, widgetRect); 401 } 402 isFolder(char type)403 public static boolean isFolder(char type) { 404 return type >= 'A' && type <= 'Z'; 405 } 406 isWidget(char type)407 public static boolean isWidget(char type) { 408 return type != CellType.ICON && type != CellType.EMPTY && (type >= 'a' && type <= 'z'); 409 } 410 isIcon(char type)411 public static boolean isIcon(char type) { 412 return type == CellType.ICON; 413 } 414 getRects(char[][] board)415 private static List<WidgetRect> getRects(char[][] board) { 416 Set<Point> used = new HashSet<>(); 417 List<WidgetRect> widgetsRects = new ArrayList<>(); 418 for (int x = 0; x < board.length; x++) { 419 for (int y = 0; y < board[0].length; y++) { 420 if (!used.contains(new Point(x, y)) && isWidget(board[x][y])) { 421 widgetsRects.add(getWidgetRect(x, y, used, board)); 422 } 423 } 424 } 425 return widgetsRects; 426 } 427 getIconPoints(char[][] board)428 private static List<IconPoint> getIconPoints(char[][] board) { 429 List<IconPoint> iconPoints = new ArrayList<>(); 430 for (int x = 0; x < board.length; x++) { 431 for (int y = 0; y < board[0].length; y++) { 432 if (isIcon(board[x][y])) { 433 iconPoints.add(new IconPoint(new Point(x, y), board[x][y])); 434 } 435 } 436 } 437 return iconPoints; 438 } 439 getFolderPoints(char[][] board)440 private static List<FolderPoint> getFolderPoints(char[][] board) { 441 List<FolderPoint> folderPoints = new ArrayList<>(); 442 for (int x = 0; x < board.length; x++) { 443 for (int y = 0; y < board[0].length; y++) { 444 if (isFolder(board[x][y])) { 445 folderPoints.add(new FolderPoint(new Point(x, y), board[x][y])); 446 } 447 } 448 } 449 return folderPoints; 450 } 451 getMainFromList(List<CellLayoutBoard> boards)452 public static WidgetRect getMainFromList(List<CellLayoutBoard> boards) { 453 for (CellLayoutBoard board : boards) { 454 WidgetRect main = board.getMain(); 455 if (main != null) { 456 return main; 457 } 458 } 459 return null; 460 } 461 getWidgetIn(List<CellLayoutBoard> boards, int x, int y)462 public static WidgetRect getWidgetIn(List<CellLayoutBoard> boards, int x, int y) { 463 for (CellLayoutBoard board : boards) { 464 WidgetRect main = board.getWidgetAt(x, y); 465 if (main != null) { 466 return main; 467 } 468 x -= board.mWidth; 469 } 470 return null; 471 } 472 boardFromString(String boardStr)473 public static CellLayoutBoard boardFromString(String boardStr) { 474 String[] lines = boardStr.split("\n"); 475 CellLayoutBoard board = new CellLayoutBoard(); 476 477 for (int y = 0; y < lines.length; y++) { 478 String line = lines[y]; 479 for (int x = 0; x < line.length(); x++) { 480 char c = line.charAt(x); 481 if (c != CellType.EMPTY) { 482 board.mWidget[x][y] = line.charAt(x); 483 } 484 } 485 } 486 board.mHeight = lines.length; 487 board.mWidth = lines[0].length(); 488 board.mWidgetsRects = getRects(board.mWidget); 489 board.mWidgetsRects.forEach(widgetRect -> { 490 if (widgetRect.mType == CellType.MAIN_WIDGET) { 491 board.mMain = widgetRect; 492 } 493 board.mWidgetsMap.put(widgetRect.mType, widgetRect); 494 }); 495 board.mIconPoints = getIconPoints(board.mWidget); 496 board.mFolderPoints = getFolderPoints(board.mWidget); 497 return board; 498 } 499 toString(int maxX, int maxY)500 public String toString(int maxX, int maxY) { 501 StringBuilder s = new StringBuilder(); 502 s.append("board: "); 503 s.append(maxX); 504 s.append("x"); 505 s.append(maxY); 506 s.append("\n"); 507 maxX = Math.min(maxX, mWidget.length); 508 maxY = Math.min(maxY, mWidget[0].length); 509 for (int y = 0; y < maxY; y++) { 510 for (int x = 0; x < maxX; x++) { 511 s.append(mWidget[x][y]); 512 } 513 s.append('\n'); 514 } 515 return s.toString(); 516 } 517 518 @Override toString()519 public String toString() { 520 return toString(mWidth, mHeight); 521 } 522 boardListFromString(String boardsStr)523 public static List<CellLayoutBoard> boardListFromString(String boardsStr) { 524 String[] lines = boardsStr.split("\n"); 525 ArrayList<String> individualBoards = new ArrayList<>(); 526 ArrayList<CellLayoutBoard> boards = new ArrayList<>(); 527 for (String line : lines) { 528 String[] boardSegment = line.split("\\|"); 529 for (int i = 0; i < boardSegment.length; i++) { 530 if (i >= individualBoards.size()) { 531 individualBoards.add(boardSegment[i]); 532 } else { 533 individualBoards.set(i, individualBoards.get(i) + "\n" + boardSegment[i]); 534 } 535 } 536 } 537 for (String board : individualBoards) { 538 boards.add(CellLayoutBoard.boardFromString(board)); 539 } 540 return boards; 541 } 542 getWidth()543 public int getWidth() { 544 return mWidth; 545 } 546 getHeight()547 public int getHeight() { 548 return mHeight; 549 } 550 } 551