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