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