AssetAtlasService.java revision 17ab38f8a87bc90eab11373f878f220ce3031de6
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.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    // 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