blob: 26b4652f4c7dde2a28ba0d97b77028106529d435 [file] [log] [blame]
Romain Guy3b748a42013-04-17 18:54:38 -07001/*
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.Collections;
50import java.util.Comparator;
51import java.util.List;
52import java.util.concurrent.CountDownLatch;
53import java.util.concurrent.TimeUnit;
54import 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 */
63public 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 */
Svetoslav Ganova0027152013-06-25 14:59:53 -0700189 public void systemRunning() {
Romain Guy3b748a42013-04-17 18:54:38 -0700190 }
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
Romain Guy80b12fc2013-05-29 15:54:25 -0700373 public boolean isCompatible(int ppid) {
374 return ppid == android.os.Process.myPpid();
375 }
376
377 @Override
Romain Guy3b748a42013-04-17 18:54:38 -0700378 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}