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