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