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