Home | History | Annotate | Download | only in server
      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 
     17 package com.android.server;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageInfo;
     21 import android.content.pm.PackageManager;
     22 import android.content.res.Resources;
     23 import android.graphics.Atlas;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.PixelFormat;
     28 import android.graphics.PorterDuff;
     29 import android.graphics.PorterDuffXfermode;
     30 import android.graphics.drawable.Drawable;
     31 import android.os.Environment;
     32 import android.os.RemoteException;
     33 import android.os.SystemProperties;
     34 import android.util.Log;
     35 import android.util.LongSparseArray;
     36 import android.view.GraphicBuffer;
     37 import android.view.IAssetAtlas;
     38 
     39 import java.io.BufferedReader;
     40 import java.io.BufferedWriter;
     41 import java.io.File;
     42 import java.io.FileInputStream;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.IOException;
     46 import java.io.InputStreamReader;
     47 import java.io.OutputStreamWriter;
     48 import java.util.ArrayList;
     49 import java.util.Collections;
     50 import java.util.Comparator;
     51 import java.util.List;
     52 import java.util.concurrent.CountDownLatch;
     53 import java.util.concurrent.TimeUnit;
     54 import 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  */
     63 public 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