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