AssetAtlasService.java revision c1b33d665c8caf5760f68c45c6ca0baa649b832a
1/* 2 * Copyright (C) 2013 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 17package com.android.server; 18 19import android.content.Context; 20import android.content.pm.PackageInfo; 21import android.content.pm.PackageManager; 22import android.content.res.Resources; 23import android.graphics.Atlas; 24import android.graphics.Bitmap; 25import android.graphics.Canvas; 26import android.graphics.Paint; 27import android.graphics.PixelFormat; 28import android.graphics.PorterDuff; 29import android.graphics.PorterDuffXfermode; 30import android.graphics.drawable.Drawable; 31import android.os.Environment; 32import android.os.RemoteException; 33import android.os.SystemProperties; 34import android.util.Log; 35import android.util.LongSparseArray; 36import android.view.GraphicBuffer; 37import android.view.IAssetAtlas; 38 39import java.io.BufferedReader; 40import java.io.BufferedWriter; 41import java.io.File; 42import java.io.FileInputStream; 43import java.io.FileNotFoundException; 44import java.io.FileOutputStream; 45import java.io.IOException; 46import java.io.InputStreamReader; 47import java.io.OutputStreamWriter; 48import java.util.ArrayList; 49import java.util.Collection; 50import java.util.Collections; 51import java.util.Comparator; 52import java.util.HashSet; 53import java.util.List; 54import java.util.concurrent.CountDownLatch; 55import java.util.concurrent.TimeUnit; 56import java.util.concurrent.atomic.AtomicBoolean; 57 58/** 59 * This service is responsible for packing preloaded bitmaps into a single 60 * atlas texture. The resulting texture can be shared across processes to 61 * reduce overall memory usage. 62 * 63 * @hide 64 */ 65public class AssetAtlasService extends IAssetAtlas.Stub { 66 /** 67 * Name of the <code>AssetAtlasService</code>. 68 */ 69 public static final String ASSET_ATLAS_SERVICE = "assetatlas"; 70 71 private static final String LOG_TAG = "AssetAtlas"; 72 73 // Turns debug logs on/off. Debug logs are kept to a minimum and should 74 // remain on to diagnose issues 75 private static final boolean DEBUG_ATLAS = true; 76 77 // When set to true the content of the atlas will be saved to disk 78 // in /data/system/atlas.png. The shared GraphicBuffer may be empty 79 private static final boolean DEBUG_ATLAS_TEXTURE = false; 80 81 // Minimum size in pixels to consider for the resulting texture 82 private static final int MIN_SIZE = 768; 83 // Maximum size in pixels to consider for the resulting texture 84 private static final int MAX_SIZE = 2048; 85 // Increment in number of pixels between size variants when looking 86 // for the best texture dimensions 87 private static final int STEP = 64; 88 89 // This percentage of the total number of pixels represents the minimum 90 // number of pixels we want to be able to pack in the atlas 91 private static final float PACKING_THRESHOLD = 0.8f; 92 93 // Defines the number of int fields used to represent a single entry 94 // in the atlas map. This number defines the size of the array returned 95 // by the getMap(). See the mAtlasMap field for more information 96 private static final int ATLAS_MAP_ENTRY_FIELD_COUNT = 4; 97 98 // Specifies how our GraphicBuffer will be used. To get proper swizzling 99 // the buffer will be written to using OpenGL (from JNI) so we can leave 100 // the software flag set to "never" 101 private static final int GRAPHIC_BUFFER_USAGE = GraphicBuffer.USAGE_SW_READ_NEVER | 102 GraphicBuffer.USAGE_SW_WRITE_NEVER | GraphicBuffer.USAGE_HW_TEXTURE; 103 104 // This boolean is set to true if an atlas was successfully 105 // computed and rendered 106 private final AtomicBoolean mAtlasReady = new AtomicBoolean(false); 107 108 private final Context mContext; 109 110 // Version name of the current build, used to identify changes to assets list 111 private final String mVersionName; 112 113 // Holds the atlas' data. This buffer can be mapped to 114 // OpenGL using an EGLImage 115 private GraphicBuffer mBuffer; 116 117 // Describes how bitmaps are placed in the atlas. Each bitmap is 118 // represented by several entries in the array: 119 // long0: SkBitmap*, the native bitmap object 120 // long1: x position 121 // long2: y position 122 // long3: rotated, 1 if the bitmap must be rotated, 0 otherwise 123 private long[] mAtlasMap; 124 125 /** 126 * Creates a new service. Upon creating, the service will gather the list of 127 * assets to consider for packing into the atlas and spawn a new thread to 128 * start the packing work. 129 * 130 * @param context The context giving access to preloaded resources 131 */ 132 public AssetAtlasService(Context context) { 133 mContext = context; 134 mVersionName = queryVersionName(context); 135 136 Collection<Bitmap> bitmaps = new HashSet<Bitmap>(300); 137 int totalPixelCount = 0; 138 139 // We only care about drawables that hold bitmaps 140 final Resources resources = context.getResources(); 141 final LongSparseArray<Drawable.ConstantState> drawables = resources.getPreloadedDrawables(); 142 143 final int count = drawables.size(); 144 for (int i = 0; i < count; i++) { 145 try { 146 totalPixelCount += drawables.valueAt(i).addAtlasableBitmaps(bitmaps); 147 } catch (Throwable t) { 148 Log.e("AssetAtlas", "Failed to fetch preloaded drawable state", t); 149 throw t; 150 } 151 } 152 153 ArrayList<Bitmap> sortedBitmaps = new ArrayList<Bitmap>(bitmaps); 154 // Our algorithms perform better when the bitmaps are first sorted 155 // The comparator will sort the bitmap by width first, then by height 156 Collections.sort(sortedBitmaps, new Comparator<Bitmap>() { 157 @Override 158 public int compare(Bitmap b1, Bitmap b2) { 159 if (b1.getWidth() == b2.getWidth()) { 160 return b2.getHeight() - b1.getHeight(); 161 } 162 return b2.getWidth() - b1.getWidth(); 163 } 164 }); 165 166 // Kick off the packing work on a worker thread 167 new Thread(new Renderer(sortedBitmaps, totalPixelCount)).start(); 168 } 169 170 /** 171 * Queries the version name stored in framework's AndroidManifest. 172 * The version name can be used to identify possible changes to 173 * framework resources. 174 * 175 * @see #getBuildIdentifier(String) 176 */ 177 private static String queryVersionName(Context context) { 178 try { 179 String packageName = context.getPackageName(); 180 PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); 181 return info.versionName; 182 } catch (PackageManager.NameNotFoundException e) { 183 Log.w(LOG_TAG, "Could not get package info", e); 184 } 185 return null; 186 } 187 188 /** 189 * Callback invoked by the server thread to indicate we can now run 190 * 3rd party code. 191 */ 192 public void systemRunning() { 193 } 194 195 /** 196 * The renderer does all the work: 197 */ 198 private class Renderer implements Runnable { 199 private final ArrayList<Bitmap> mBitmaps; 200 private final int mPixelCount; 201 202 private Bitmap mAtlasBitmap; 203 204 Renderer(ArrayList<Bitmap> bitmaps, int pixelCount) { 205 mBitmaps = bitmaps; 206 mPixelCount = pixelCount; 207 } 208 209 /** 210 * 1. On first boot or after every update, brute-force through all the 211 * possible atlas configurations and look for the best one (maximimize 212 * number of packed assets and minimize texture size) 213 * a. If a best configuration was computed, write it out to disk for 214 * future use 215 * 2. Read best configuration from disk 216 * 3. Compute the packing using the best configuration 217 * 4. Allocate a GraphicBuffer 218 * 5. Render assets in the buffer 219 */ 220 @Override 221 public void run() { 222 Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName); 223 if (DEBUG_ATLAS) Log.d(LOG_TAG, "Loaded configuration: " + config); 224 225 if (config != null) { 226 mBuffer = GraphicBuffer.create(config.width, config.height, 227 PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE); 228 229 if (mBuffer != null) { 230 Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags); 231 if (renderAtlas(mBuffer, atlas, config.count)) { 232 mAtlasReady.set(true); 233 } 234 } 235 } 236 } 237 238 /** 239 * Renders a list of bitmaps into the atlas. The position of each bitmap 240 * was decided by the packing algorithm and will be honored by this 241 * method. If need be this method will also rotate bitmaps. 242 * 243 * @param buffer The buffer to render the atlas entries into 244 * @param atlas The atlas to pack the bitmaps into 245 * @param packCount The number of bitmaps that will be packed in the atlas 246 * 247 * @return true if the atlas was rendered, false otherwise 248 */ 249 @SuppressWarnings("MismatchedReadAndWriteOfArray") 250 private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) { 251 // Use a Source blend mode to improve performance, the target bitmap 252 // will be zero'd out so there's no need to waste time applying blending 253 final Paint paint = new Paint(); 254 paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 255 256 // We always render the atlas into a bitmap. This bitmap is then 257 // uploaded into the GraphicBuffer using OpenGL to swizzle the content 258 final Canvas canvas = acquireCanvas(buffer.getWidth(), buffer.getHeight()); 259 if (canvas == null) return false; 260 261 final Atlas.Entry entry = new Atlas.Entry(); 262 263 mAtlasMap = new long[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT]; 264 long[] atlasMap = mAtlasMap; 265 int mapIndex = 0; 266 267 boolean result = false; 268 try { 269 final long startRender = System.nanoTime(); 270 final int count = mBitmaps.size(); 271 272 for (int i = 0; i < count; i++) { 273 final Bitmap bitmap = mBitmaps.get(i); 274 if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { 275 // We have more bitmaps to pack than the current configuration 276 // says, we were most likely not able to detect a change in the 277 // list of preloaded drawables, abort and delete the configuration 278 if (mapIndex >= mAtlasMap.length) { 279 deleteDataFile(); 280 break; 281 } 282 283 canvas.save(); 284 canvas.translate(entry.x, entry.y); 285 if (entry.rotated) { 286 canvas.translate(bitmap.getHeight(), 0.0f); 287 canvas.rotate(90.0f); 288 } 289 canvas.drawBitmap(bitmap, 0.0f, 0.0f, null); 290 canvas.restore(); 291 atlasMap[mapIndex++] = bitmap.getSkBitmap(); 292 atlasMap[mapIndex++] = entry.x; 293 atlasMap[mapIndex++] = entry.y; 294 atlasMap[mapIndex++] = entry.rotated ? 1 : 0; 295 } 296 } 297 298 final long endRender = System.nanoTime(); 299 result = nUploadAtlas(buffer, mAtlasBitmap); 300 301 final long endUpload = System.nanoTime(); 302 if (DEBUG_ATLAS) { 303 float renderDuration = (endRender - startRender) / 1000.0f / 1000.0f; 304 float uploadDuration = (endUpload - endRender) / 1000.0f / 1000.0f; 305 Log.d(LOG_TAG, String.format("Rendered atlas in %.2fms (%.2f+%.2fms)", 306 renderDuration + uploadDuration, renderDuration, uploadDuration)); 307 } 308 309 } finally { 310 releaseCanvas(canvas); 311 } 312 313 return result; 314 } 315 316 /** 317 * Returns a Canvas for the specified buffer. If {@link #DEBUG_ATLAS_TEXTURE} 318 * is turned on, the returned Canvas will render into a local bitmap that 319 * will then be saved out to disk for debugging purposes. 320 * @param width 321 * @param height 322 */ 323 private Canvas acquireCanvas(int width, int height) { 324 mAtlasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 325 return new Canvas(mAtlasBitmap); 326 } 327 328 /** 329 * Releases the canvas used to render into the buffer. Calling this method 330 * will release any resource previously acquired. If {@link #DEBUG_ATLAS_TEXTURE} 331 * is turend on, calling this method will write the content of the atlas 332 * to disk in /data/system/atlas.png for debugging. 333 */ 334 private void releaseCanvas(Canvas canvas) { 335 canvas.setBitmap(null); 336 if (DEBUG_ATLAS_TEXTURE) { 337 338 File systemDirectory = new File(Environment.getDataDirectory(), "system"); 339 File dataFile = new File(systemDirectory, "atlas.png"); 340 341 try { 342 FileOutputStream out = new FileOutputStream(dataFile); 343 mAtlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); 344 out.close(); 345 } catch (FileNotFoundException e) { 346 // Ignore 347 } catch (IOException e) { 348 // Ignore 349 } 350 } 351 mAtlasBitmap.recycle(); 352 mAtlasBitmap = null; 353 } 354 } 355 356 private static native boolean nUploadAtlas(GraphicBuffer buffer, Bitmap bitmap); 357 358 @Override 359 public boolean isCompatible(int ppid) { 360 return ppid == android.os.Process.myPpid(); 361 } 362 363 @Override 364 public GraphicBuffer getBuffer() throws RemoteException { 365 return mAtlasReady.get() ? mBuffer : null; 366 } 367 368 @Override 369 public long[] getMap() throws RemoteException { 370 return mAtlasReady.get() ? mAtlasMap : null; 371 } 372 373 /** 374 * Finds the best atlas configuration to pack the list of supplied bitmaps. 375 * This method takes advantage of multi-core systems by spawning a number 376 * of threads equal to the number of available cores. 377 */ 378 private static Configuration computeBestConfiguration( 379 ArrayList<Bitmap> bitmaps, int pixelCount) { 380 if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration..."); 381 382 long begin = System.nanoTime(); 383 List<WorkerResult> results = Collections.synchronizedList(new ArrayList<WorkerResult>()); 384 385 // Don't bother with an extra thread if there's only one processor 386 int cpuCount = Runtime.getRuntime().availableProcessors(); 387 if (cpuCount == 1) { 388 new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run(); 389 } else { 390 int start = MIN_SIZE + (cpuCount - 1) * STEP; 391 int end = MAX_SIZE; 392 int step = STEP * cpuCount; 393 394 final CountDownLatch signal = new CountDownLatch(cpuCount); 395 396 for (int i = 0; i < cpuCount; i++, start -= STEP, end -= STEP) { 397 ComputeWorker worker = new ComputeWorker(start, end, step, 398 bitmaps, pixelCount, results, signal); 399 new Thread(worker, "Atlas Worker #" + (i + 1)).start(); 400 } 401 402 boolean isAllWorkerFinished; 403 try { 404 isAllWorkerFinished = signal.await(10, TimeUnit.SECONDS); 405 } catch (InterruptedException e) { 406 Log.w(LOG_TAG, "Could not complete configuration computation"); 407 return null; 408 } 409 410 if (!isAllWorkerFinished) { 411 // We have to abort here, otherwise the async updates on "results" would crash the 412 // sort later. 413 Log.w(LOG_TAG, "Could not complete configuration computation before timeout."); 414 return null; 415 } 416 } 417 418 // Maximize the number of packed bitmaps, minimize the texture size 419 Collections.sort(results, new Comparator<WorkerResult>() { 420 @Override 421 public int compare(WorkerResult r1, WorkerResult r2) { 422 int delta = r2.count - r1.count; 423 if (delta != 0) return delta; 424 return r1.width * r1.height - r2.width * r2.height; 425 } 426 }); 427 428 if (DEBUG_ATLAS) { 429 float delay = (System.nanoTime() - begin) / 1000.0f / 1000.0f / 1000.0f; 430 Log.d(LOG_TAG, String.format("Found best atlas configuration (out of %d) in %.2fs", 431 results.size(), delay)); 432 } 433 434 WorkerResult result = results.get(0); 435 return new Configuration(result.type, result.width, result.height, result.count); 436 } 437 438 /** 439 * Returns the path to the file containing the best computed 440 * atlas configuration. 441 */ 442 private static File getDataFile() { 443 File systemDirectory = new File(Environment.getDataDirectory(), "system"); 444 return new File(systemDirectory, "framework_atlas.config"); 445 } 446 447 private static void deleteDataFile() { 448 Log.w(LOG_TAG, "Current configuration inconsistent with assets list"); 449 if (!getDataFile().delete()) { 450 Log.w(LOG_TAG, "Could not delete the current configuration"); 451 } 452 } 453 454 private File getFrameworkResourcesFile() { 455 return new File(mContext.getApplicationInfo().sourceDir); 456 } 457 458 /** 459 * Returns the best known atlas configuration. This method will either 460 * read the configuration from disk or start a brute-force search 461 * and save the result out to disk. 462 */ 463 private Configuration chooseConfiguration(ArrayList<Bitmap> bitmaps, int pixelCount, 464 String versionName) { 465 Configuration config = null; 466 467 final File dataFile = getDataFile(); 468 if (dataFile.exists()) { 469 config = readConfiguration(dataFile, versionName); 470 } 471 472 if (config == null) { 473 config = computeBestConfiguration(bitmaps, pixelCount); 474 if (config != null) writeConfiguration(config, dataFile, versionName); 475 } 476 477 return config; 478 } 479 480 /** 481 * Writes the specified atlas configuration to the specified file. 482 */ 483 private void writeConfiguration(Configuration config, File file, String versionName) { 484 BufferedWriter writer = null; 485 try { 486 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); 487 writer.write(getBuildIdentifier(versionName)); 488 writer.newLine(); 489 writer.write(config.type.toString()); 490 writer.newLine(); 491 writer.write(String.valueOf(config.width)); 492 writer.newLine(); 493 writer.write(String.valueOf(config.height)); 494 writer.newLine(); 495 writer.write(String.valueOf(config.count)); 496 writer.newLine(); 497 writer.write(String.valueOf(config.flags)); 498 writer.newLine(); 499 } catch (FileNotFoundException e) { 500 Log.w(LOG_TAG, "Could not write " + file, e); 501 } catch (IOException e) { 502 Log.w(LOG_TAG, "Could not write " + file, e); 503 } finally { 504 if (writer != null) { 505 try { 506 writer.close(); 507 } catch (IOException e) { 508 // Ignore 509 } 510 } 511 } 512 } 513 514 /** 515 * Reads an atlas configuration from the specified file. This method 516 * returns null if an error occurs or if the configuration is invalid. 517 */ 518 private Configuration readConfiguration(File file, String versionName) { 519 BufferedReader reader = null; 520 Configuration config = null; 521 try { 522 reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); 523 524 if (checkBuildIdentifier(reader, versionName)) { 525 Atlas.Type type = Atlas.Type.valueOf(reader.readLine()); 526 int width = readInt(reader, MIN_SIZE, MAX_SIZE); 527 int height = readInt(reader, MIN_SIZE, MAX_SIZE); 528 int count = readInt(reader, 0, Integer.MAX_VALUE); 529 int flags = readInt(reader, Integer.MIN_VALUE, Integer.MAX_VALUE); 530 531 config = new Configuration(type, width, height, count, flags); 532 } 533 } catch (IllegalArgumentException e) { 534 Log.w(LOG_TAG, "Invalid parameter value in " + file, e); 535 } catch (FileNotFoundException e) { 536 Log.w(LOG_TAG, "Could not read " + file, e); 537 } catch (IOException e) { 538 Log.w(LOG_TAG, "Could not read " + file, e); 539 } finally { 540 if (reader != null) { 541 try { 542 reader.close(); 543 } catch (IOException e) { 544 // Ignore 545 } 546 } 547 } 548 return config; 549 } 550 551 private static int readInt(BufferedReader reader, int min, int max) throws IOException { 552 return Math.max(min, Math.min(max, Integer.parseInt(reader.readLine()))); 553 } 554 555 /** 556 * Compares the next line in the specified buffered reader to the current 557 * build identifier. Returns whether the two values are equal. 558 * 559 * @see #getBuildIdentifier(String) 560 */ 561 private boolean checkBuildIdentifier(BufferedReader reader, String versionName) 562 throws IOException { 563 String deviceBuildId = getBuildIdentifier(versionName); 564 String buildId = reader.readLine(); 565 return deviceBuildId.equals(buildId); 566 } 567 568 /** 569 * Returns an identifier for the current build that can be used to detect 570 * likely changes to framework resources. The build identifier is made of 571 * several distinct values: 572 * 573 * build fingerprint/framework version name/file size of framework resources apk 574 * 575 * Only the build fingerprint should be necessary on user builds but 576 * the other values are useful to detect changes on eng builds during 577 * development. 578 * 579 * This identifier does not attempt to be exact: a new identifier does not 580 * necessarily mean the preloaded drawables have changed. It is important 581 * however that whenever the list of preloaded drawables changes, this 582 * identifier changes as well. 583 * 584 * @see #checkBuildIdentifier(java.io.BufferedReader, String) 585 */ 586 private String getBuildIdentifier(String versionName) { 587 return SystemProperties.get("ro.build.fingerprint", "") + '/' + versionName + '/' + 588 String.valueOf(getFrameworkResourcesFile().length()); 589 } 590 591 /** 592 * Atlas configuration. Specifies the algorithm, dimensions and flags to use. 593 */ 594 private static class Configuration { 595 final Atlas.Type type; 596 final int width; 597 final int height; 598 final int count; 599 final int flags; 600 601 Configuration(Atlas.Type type, int width, int height, int count) { 602 this(type, width, height, count, Atlas.FLAG_DEFAULTS); 603 } 604 605 Configuration(Atlas.Type type, int width, int height, int count, int flags) { 606 this.type = type; 607 this.width = width; 608 this.height = height; 609 this.count = count; 610 this.flags = flags; 611 } 612 613 @Override 614 public String toString() { 615 return type.toString() + " (" + width + "x" + height + ") flags=0x" + 616 Integer.toHexString(flags) + " count=" + count; 617 } 618 } 619 620 /** 621 * Used during the brute-force search to gather information about each 622 * variant of the packing algorithm. 623 */ 624 private static class WorkerResult { 625 Atlas.Type type; 626 int width; 627 int height; 628 int count; 629 630 WorkerResult(Atlas.Type type, int width, int height, int count) { 631 this.type = type; 632 this.width = width; 633 this.height = height; 634 this.count = count; 635 } 636 637 @Override 638 public String toString() { 639 return String.format("%s %dx%d", type.toString(), width, height); 640 } 641 } 642 643 /** 644 * A compute worker will try a finite number of variations of the packing 645 * algorithms and save the results in a supplied list. 646 */ 647 private static class ComputeWorker implements Runnable { 648 private final int mStart; 649 private final int mEnd; 650 private final int mStep; 651 private final List<Bitmap> mBitmaps; 652 private final List<WorkerResult> mResults; 653 private final CountDownLatch mSignal; 654 private final int mThreshold; 655 656 /** 657 * Creates a new compute worker to brute-force through a range of 658 * packing algorithms variants. 659 * 660 * @param start The minimum texture width to try 661 * @param end The maximum texture width to try 662 * @param step The number of pixels to increment the texture width by at each step 663 * @param bitmaps The list of bitmaps to pack in the atlas 664 * @param pixelCount The total number of pixels occupied by the list of bitmaps 665 * @param results The list of results in which to save the brute-force search results 666 * @param signal Latch to decrement when this worker is done, may be null 667 */ 668 ComputeWorker(int start, int end, int step, List<Bitmap> bitmaps, int pixelCount, 669 List<WorkerResult> results, CountDownLatch signal) { 670 mStart = start; 671 mEnd = end; 672 mStep = step; 673 mBitmaps = bitmaps; 674 mResults = results; 675 mSignal = signal; 676 677 // Minimum number of pixels we want to be able to pack 678 int threshold = (int) (pixelCount * PACKING_THRESHOLD); 679 // Make sure we can find at least one configuration 680 while (threshold > MAX_SIZE * MAX_SIZE) { 681 threshold >>= 1; 682 } 683 mThreshold = threshold; 684 } 685 686 @Override 687 public void run() { 688 if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName()); 689 690 Atlas.Entry entry = new Atlas.Entry(); 691 for (Atlas.Type type : Atlas.Type.values()) { 692 for (int width = mEnd; width > mStart; width -= mStep) { 693 for (int height = MAX_SIZE; height > MIN_SIZE; height -= STEP) { 694 // If the atlas is not big enough, skip it 695 if (width * height <= mThreshold) continue; 696 697 final int count = packBitmaps(type, width, height, entry); 698 if (count > 0) { 699 mResults.add(new WorkerResult(type, width, height, count)); 700 // If we were able to pack everything let's stop here 701 // Increasing the height further won't make things better 702 if (count == mBitmaps.size()) { 703 break; 704 } 705 } 706 } 707 } 708 } 709 710 if (mSignal != null) { 711 mSignal.countDown(); 712 } 713 } 714 715 private int packBitmaps(Atlas.Type type, int width, int height, Atlas.Entry entry) { 716 int total = 0; 717 Atlas atlas = new Atlas(type, width, height); 718 719 final int count = mBitmaps.size(); 720 for (int i = 0; i < count; i++) { 721 final Bitmap bitmap = mBitmaps.get(i); 722 if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { 723 total++; 724 } 725 } 726 727 return total; 728 } 729 } 730} 731