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