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