| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server; |
| |
| import android.content.Context; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.graphics.Atlas; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.PixelFormat; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.drawable.Drawable; |
| import android.os.Environment; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| import android.util.LongSparseArray; |
| import android.view.GraphicBuffer; |
| import android.view.IAssetAtlas; |
| |
| import java.io.BufferedReader; |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.OutputStreamWriter; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| /** |
| * This service is responsible for packing preloaded bitmaps into a single |
| * atlas texture. The resulting texture can be shared across processes to |
| * reduce overall memory usage. |
| * |
| * @hide |
| */ |
| public class AssetAtlasService extends IAssetAtlas.Stub { |
| /** |
| * Name of the <code>AssetAtlasService</code>. |
| */ |
| public static final String ASSET_ATLAS_SERVICE = "assetatlas"; |
| |
| private static final String LOG_TAG = "Atlas"; |
| |
| // Turns debug logs on/off. Debug logs are kept to a minimum and should |
| // remain on to diagnose issues |
| private static final boolean DEBUG_ATLAS = true; |
| |
| // When set to true the content of the atlas will be saved to disk |
| // in /data/system/atlas.png. The shared GraphicBuffer may be empty |
| private static final boolean DEBUG_ATLAS_TEXTURE = false; |
| |
| // Minimum size in pixels to consider for the resulting texture |
| private static final int MIN_SIZE = 768; |
| // Maximum size in pixels to consider for the resulting texture |
| private static final int MAX_SIZE = 2048; |
| // Increment in number of pixels between size variants when looking |
| // for the best texture dimensions |
| private static final int STEP = 64; |
| |
| // This percentage of the total number of pixels represents the minimum |
| // number of pixels we want to be able to pack in the atlas |
| private static final float PACKING_THRESHOLD = 0.8f; |
| |
| // Defines the number of int fields used to represent a single entry |
| // in the atlas map. This number defines the size of the array returned |
| // by the getMap(). See the mAtlasMap field for more information |
| private static final int ATLAS_MAP_ENTRY_FIELD_COUNT = 4; |
| |
| // Specifies how our GraphicBuffer will be used. To get proper swizzling |
| // the buffer will be written to using OpenGL (from JNI) so we can leave |
| // the software flag set to "never" |
| private static final int GRAPHIC_BUFFER_USAGE = GraphicBuffer.USAGE_SW_READ_NEVER | |
| GraphicBuffer.USAGE_SW_WRITE_NEVER | GraphicBuffer.USAGE_HW_TEXTURE; |
| |
| // This boolean is set to true if an atlas was successfully |
| // computed and rendered |
| private final AtomicBoolean mAtlasReady = new AtomicBoolean(false); |
| |
| private final Context mContext; |
| |
| // Version name of the current build, used to identify changes to assets list |
| private final String mVersionName; |
| |
| // Holds the atlas' data. This buffer can be mapped to |
| // OpenGL using an EGLImage |
| private GraphicBuffer mBuffer; |
| |
| // Describes how bitmaps are placed in the atlas. Each bitmap is |
| // represented by several entries in the array: |
| // int0: SkBitmap*, the native bitmap object |
| // int1: x position |
| // int2: y position |
| // int3: rotated, 1 if the bitmap must be rotated, 0 otherwise |
| // NOTE: This will need to be handled differently to support 64 bit pointers |
| private int[] mAtlasMap; |
| |
| /** |
| * Creates a new service. Upon creating, the service will gather the list of |
| * assets to consider for packing into the atlas and spawn a new thread to |
| * start the packing work. |
| * |
| * @param context The context giving access to preloaded resources |
| */ |
| public AssetAtlasService(Context context) { |
| mContext = context; |
| mVersionName = queryVersionName(context); |
| |
| ArrayList<Bitmap> bitmaps = new ArrayList<Bitmap>(300); |
| int totalPixelCount = 0; |
| |
| // We only care about drawables that hold bitmaps |
| final Resources resources = context.getResources(); |
| final LongSparseArray<Drawable.ConstantState> drawables = resources.getPreloadedDrawables(); |
| |
| final int count = drawables.size(); |
| for (int i = 0; i < count; i++) { |
| final Bitmap bitmap = drawables.valueAt(i).getBitmap(); |
| if (bitmap != null && bitmap.getConfig() == Bitmap.Config.ARGB_8888) { |
| bitmaps.add(bitmap); |
| totalPixelCount += bitmap.getWidth() * bitmap.getHeight(); |
| } |
| } |
| |
| // Our algorithms perform better when the bitmaps are first sorted |
| // The comparator will sort the bitmap by width first, then by height |
| Collections.sort(bitmaps, new Comparator<Bitmap>() { |
| @Override |
| public int compare(Bitmap b1, Bitmap b2) { |
| if (b1.getWidth() == b2.getWidth()) { |
| return b2.getHeight() - b1.getHeight(); |
| } |
| return b2.getWidth() - b1.getWidth(); |
| } |
| }); |
| |
| // Kick off the packing work on a worker thread |
| new Thread(new Renderer(bitmaps, totalPixelCount)).start(); |
| } |
| |
| /** |
| * Queries the version name stored in framework's AndroidManifest. |
| * The version name can be used to identify possible changes to |
| * framework resources. |
| * |
| * @see #getBuildIdentifier(String) |
| */ |
| private static String queryVersionName(Context context) { |
| try { |
| String packageName = context.getPackageName(); |
| PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); |
| return info.versionName; |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.w(LOG_TAG, "Could not get package info", e); |
| } |
| return null; |
| } |
| |
| /** |
| * Callback invoked by the server thread to indicate we can now run |
| * 3rd party code. |
| */ |
| public void systemRunning() { |
| } |
| |
| /** |
| * The renderer does all the work: |
| */ |
| private class Renderer implements Runnable { |
| private final ArrayList<Bitmap> mBitmaps; |
| private final int mPixelCount; |
| |
| private int mNativeBitmap; |
| |
| // Used for debugging only |
| private Bitmap mAtlasBitmap; |
| |
| Renderer(ArrayList<Bitmap> bitmaps, int pixelCount) { |
| mBitmaps = bitmaps; |
| mPixelCount = pixelCount; |
| } |
| |
| /** |
| * 1. On first boot or after every update, brute-force through all the |
| * possible atlas configurations and look for the best one (maximimize |
| * number of packed assets and minimize texture size) |
| * a. If a best configuration was computed, write it out to disk for |
| * future use |
| * 2. Read best configuration from disk |
| * 3. Compute the packing using the best configuration |
| * 4. Allocate a GraphicBuffer |
| * 5. Render assets in the buffer |
| */ |
| @Override |
| public void run() { |
| Configuration config = chooseConfiguration(mBitmaps, mPixelCount, mVersionName); |
| if (DEBUG_ATLAS) Log.d(LOG_TAG, "Loaded configuration: " + config); |
| |
| if (config != null) { |
| mBuffer = GraphicBuffer.create(config.width, config.height, |
| PixelFormat.RGBA_8888, GRAPHIC_BUFFER_USAGE); |
| |
| if (mBuffer != null) { |
| Atlas atlas = new Atlas(config.type, config.width, config.height, config.flags); |
| if (renderAtlas(mBuffer, atlas, config.count)) { |
| mAtlasReady.set(true); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Renders a list of bitmaps into the atlas. The position of each bitmap |
| * was decided by the packing algorithm and will be honored by this |
| * method. If need be this method will also rotate bitmaps. |
| * |
| * @param buffer The buffer to render the atlas entries into |
| * @param atlas The atlas to pack the bitmaps into |
| * @param packCount The number of bitmaps that will be packed in the atlas |
| * |
| * @return true if the atlas was rendered, false otherwise |
| */ |
| @SuppressWarnings("MismatchedReadAndWriteOfArray") |
| private boolean renderAtlas(GraphicBuffer buffer, Atlas atlas, int packCount) { |
| // Use a Source blend mode to improve performance, the target bitmap |
| // will be zero'd out so there's no need to waste time applying blending |
| final Paint paint = new Paint(); |
| paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); |
| |
| // We always render the atlas into a bitmap. This bitmap is then |
| // uploaded into the GraphicBuffer using OpenGL to swizzle the content |
| final Canvas canvas = acquireCanvas(buffer.getWidth(), buffer.getHeight()); |
| if (canvas == null) return false; |
| |
| final Atlas.Entry entry = new Atlas.Entry(); |
| |
| mAtlasMap = new int[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT]; |
| int[] atlasMap = mAtlasMap; |
| int mapIndex = 0; |
| |
| boolean result = false; |
| try { |
| final long startRender = System.nanoTime(); |
| final int count = mBitmaps.size(); |
| |
| for (int i = 0; i < count; i++) { |
| final Bitmap bitmap = mBitmaps.get(i); |
| if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { |
| // We have more bitmaps to pack than the current configuration |
| // says, we were most likely not able to detect a change in the |
| // list of preloaded drawables, abort and delete the configuration |
| if (mapIndex >= mAtlasMap.length) { |
| deleteDataFile(); |
| break; |
| } |
| |
| canvas.save(); |
| canvas.translate(entry.x, entry.y); |
| if (entry.rotated) { |
| canvas.translate(bitmap.getHeight(), 0.0f); |
| canvas.rotate(90.0f); |
| } |
| canvas.drawBitmap(bitmap, 0.0f, 0.0f, null); |
| canvas.restore(); |
| |
| atlasMap[mapIndex++] = bitmap.mNativeBitmap; |
| atlasMap[mapIndex++] = entry.x; |
| atlasMap[mapIndex++] = entry.y; |
| atlasMap[mapIndex++] = entry.rotated ? 1 : 0; |
| } |
| } |
| |
| final long endRender = System.nanoTime(); |
| if (mNativeBitmap != 0) { |
| result = nUploadAtlas(buffer, mNativeBitmap); |
| } |
| |
| final long endUpload = System.nanoTime(); |
| if (DEBUG_ATLAS) { |
| float renderDuration = (endRender - startRender) / 1000.0f / 1000.0f; |
| float uploadDuration = (endUpload - endRender) / 1000.0f / 1000.0f; |
| Log.d(LOG_TAG, String.format("Rendered atlas in %.2fms (%.2f+%.2fms)", |
| renderDuration + uploadDuration, renderDuration, uploadDuration)); |
| } |
| |
| } finally { |
| releaseCanvas(canvas); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Returns a Canvas for the specified buffer. If {@link #DEBUG_ATLAS_TEXTURE} |
| * is turned on, the returned Canvas will render into a local bitmap that |
| * will then be saved out to disk for debugging purposes. |
| * @param width |
| * @param height |
| */ |
| private Canvas acquireCanvas(int width, int height) { |
| if (DEBUG_ATLAS_TEXTURE) { |
| mAtlasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); |
| return new Canvas(mAtlasBitmap); |
| } else { |
| Canvas canvas = new Canvas(); |
| mNativeBitmap = nAcquireAtlasCanvas(canvas, width, height); |
| return canvas; |
| } |
| } |
| |
| /** |
| * Releases the canvas used to render into the buffer. Calling this method |
| * will release any resource previously acquired. If {@link #DEBUG_ATLAS_TEXTURE} |
| * is turend on, calling this method will write the content of the atlas |
| * to disk in /data/system/atlas.png for debugging. |
| */ |
| private void releaseCanvas(Canvas canvas) { |
| if (DEBUG_ATLAS_TEXTURE) { |
| canvas.setBitmap(null); |
| |
| File systemDirectory = new File(Environment.getDataDirectory(), "system"); |
| File dataFile = new File(systemDirectory, "atlas.png"); |
| |
| try { |
| FileOutputStream out = new FileOutputStream(dataFile); |
| mAtlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); |
| out.close(); |
| } catch (FileNotFoundException e) { |
| // Ignore |
| } catch (IOException e) { |
| // Ignore |
| } |
| |
| mAtlasBitmap.recycle(); |
| mAtlasBitmap = null; |
| } else { |
| nReleaseAtlasCanvas(canvas, mNativeBitmap); |
| } |
| } |
| } |
| |
| private static native int nAcquireAtlasCanvas(Canvas canvas, int width, int height); |
| private static native void nReleaseAtlasCanvas(Canvas canvas, int bitmap); |
| private static native boolean nUploadAtlas(GraphicBuffer buffer, int bitmap); |
| |
| @Override |
| public boolean isCompatible(int ppid) { |
| return ppid == android.os.Process.myPpid(); |
| } |
| |
| @Override |
| public GraphicBuffer getBuffer() throws RemoteException { |
| return mAtlasReady.get() ? mBuffer : null; |
| } |
| |
| @Override |
| public int[] getMap() throws RemoteException { |
| return mAtlasReady.get() ? mAtlasMap : null; |
| } |
| |
| /** |
| * Finds the best atlas configuration to pack the list of supplied bitmaps. |
| * This method takes advantage of multi-core systems by spawning a number |
| * of threads equal to the number of available cores. |
| */ |
| private static Configuration computeBestConfiguration( |
| ArrayList<Bitmap> bitmaps, int pixelCount) { |
| if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration..."); |
| |
| long begin = System.nanoTime(); |
| List<WorkerResult> results = Collections.synchronizedList(new ArrayList<WorkerResult>()); |
| |
| // Don't bother with an extra thread if there's only one processor |
| int cpuCount = Runtime.getRuntime().availableProcessors(); |
| if (cpuCount == 1) { |
| new ComputeWorker(MIN_SIZE, MAX_SIZE, STEP, bitmaps, pixelCount, results, null).run(); |
| } else { |
| int start = MIN_SIZE; |
| int end = MAX_SIZE - (cpuCount - 1) * STEP; |
| int step = STEP * cpuCount; |
| |
| final CountDownLatch signal = new CountDownLatch(cpuCount); |
| |
| for (int i = 0; i < cpuCount; i++, start += STEP, end += STEP) { |
| ComputeWorker worker = new ComputeWorker(start, end, step, |
| bitmaps, pixelCount, results, signal); |
| new Thread(worker, "Atlas Worker #" + (i + 1)).start(); |
| } |
| |
| try { |
| signal.await(10, TimeUnit.SECONDS); |
| } catch (InterruptedException e) { |
| Log.w(LOG_TAG, "Could not complete configuration computation"); |
| return null; |
| } |
| } |
| |
| // Maximize the number of packed bitmaps, minimize the texture size |
| Collections.sort(results, new Comparator<WorkerResult>() { |
| @Override |
| public int compare(WorkerResult r1, WorkerResult r2) { |
| int delta = r2.count - r1.count; |
| if (delta != 0) return delta; |
| return r1.width * r1.height - r2.width * r2.height; |
| } |
| }); |
| |
| if (DEBUG_ATLAS) { |
| float delay = (System.nanoTime() - begin) / 1000.0f / 1000.0f / 1000.0f; |
| Log.d(LOG_TAG, String.format("Found best atlas configuration in %.2fs", delay)); |
| } |
| |
| WorkerResult result = results.get(0); |
| return new Configuration(result.type, result.width, result.height, result.count); |
| } |
| |
| /** |
| * Returns the path to the file containing the best computed |
| * atlas configuration. |
| */ |
| private static File getDataFile() { |
| File systemDirectory = new File(Environment.getDataDirectory(), "system"); |
| return new File(systemDirectory, "framework_atlas.config"); |
| } |
| |
| private static void deleteDataFile() { |
| Log.w(LOG_TAG, "Current configuration inconsistent with assets list"); |
| if (!getDataFile().delete()) { |
| Log.w(LOG_TAG, "Could not delete the current configuration"); |
| } |
| } |
| |
| private File getFrameworkResourcesFile() { |
| return new File(mContext.getApplicationInfo().sourceDir); |
| } |
| |
| /** |
| * Returns the best known atlas configuration. This method will either |
| * read the configuration from disk or start a brute-force search |
| * and save the result out to disk. |
| */ |
| private Configuration chooseConfiguration(ArrayList<Bitmap> bitmaps, int pixelCount, |
| String versionName) { |
| Configuration config = null; |
| |
| final File dataFile = getDataFile(); |
| if (dataFile.exists()) { |
| config = readConfiguration(dataFile, versionName); |
| } |
| |
| if (config == null) { |
| config = computeBestConfiguration(bitmaps, pixelCount); |
| if (config != null) writeConfiguration(config, dataFile, versionName); |
| } |
| |
| return config; |
| } |
| |
| /** |
| * Writes the specified atlas configuration to the specified file. |
| */ |
| private void writeConfiguration(Configuration config, File file, String versionName) { |
| BufferedWriter writer = null; |
| try { |
| writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); |
| writer.write(getBuildIdentifier(versionName)); |
| writer.newLine(); |
| writer.write(config.type.toString()); |
| writer.newLine(); |
| writer.write(String.valueOf(config.width)); |
| writer.newLine(); |
| writer.write(String.valueOf(config.height)); |
| writer.newLine(); |
| writer.write(String.valueOf(config.count)); |
| writer.newLine(); |
| writer.write(String.valueOf(config.flags)); |
| writer.newLine(); |
| } catch (FileNotFoundException e) { |
| Log.w(LOG_TAG, "Could not write " + file, e); |
| } catch (IOException e) { |
| Log.w(LOG_TAG, "Could not write " + file, e); |
| } finally { |
| if (writer != null) { |
| try { |
| writer.close(); |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| } |
| } |
| |
| /** |
| * Reads an atlas configuration from the specified file. This method |
| * returns null if an error occurs or if the configuration is invalid. |
| */ |
| private Configuration readConfiguration(File file, String versionName) { |
| BufferedReader reader = null; |
| Configuration config = null; |
| try { |
| reader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); |
| |
| if (checkBuildIdentifier(reader, versionName)) { |
| Atlas.Type type = Atlas.Type.valueOf(reader.readLine()); |
| int width = readInt(reader, MIN_SIZE, MAX_SIZE); |
| int height = readInt(reader, MIN_SIZE, MAX_SIZE); |
| int count = readInt(reader, 0, Integer.MAX_VALUE); |
| int flags = readInt(reader, Integer.MIN_VALUE, Integer.MAX_VALUE); |
| |
| config = new Configuration(type, width, height, count, flags); |
| } |
| } catch (IllegalArgumentException e) { |
| Log.w(LOG_TAG, "Invalid parameter value in " + file, e); |
| } catch (FileNotFoundException e) { |
| Log.w(LOG_TAG, "Could not read " + file, e); |
| } catch (IOException e) { |
| Log.w(LOG_TAG, "Could not read " + file, e); |
| } finally { |
| if (reader != null) { |
| try { |
| reader.close(); |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| } |
| return config; |
| } |
| |
| private static int readInt(BufferedReader reader, int min, int max) throws IOException { |
| return Math.max(min, Math.min(max, Integer.parseInt(reader.readLine()))); |
| } |
| |
| /** |
| * Compares the next line in the specified buffered reader to the current |
| * build identifier. Returns whether the two values are equal. |
| * |
| * @see #getBuildIdentifier(String) |
| */ |
| private boolean checkBuildIdentifier(BufferedReader reader, String versionName) |
| throws IOException { |
| String deviceBuildId = getBuildIdentifier(versionName); |
| String buildId = reader.readLine(); |
| return deviceBuildId.equals(buildId); |
| } |
| |
| /** |
| * Returns an identifier for the current build that can be used to detect |
| * likely changes to framework resources. The build identifier is made of |
| * several distinct values: |
| * |
| * build fingerprint/framework version name/file size of framework resources apk |
| * |
| * Only the build fingerprint should be necessary on user builds but |
| * the other values are useful to detect changes on eng builds during |
| * development. |
| * |
| * This identifier does not attempt to be exact: a new identifier does not |
| * necessarily mean the preloaded drawables have changed. It is important |
| * however that whenever the list of preloaded drawables changes, this |
| * identifier changes as well. |
| * |
| * @see #checkBuildIdentifier(java.io.BufferedReader, String) |
| */ |
| private String getBuildIdentifier(String versionName) { |
| return SystemProperties.get("ro.build.fingerprint", "") + '/' + versionName + '/' + |
| String.valueOf(getFrameworkResourcesFile().length()); |
| } |
| |
| /** |
| * Atlas configuration. Specifies the algorithm, dimensions and flags to use. |
| */ |
| private static class Configuration { |
| final Atlas.Type type; |
| final int width; |
| final int height; |
| final int count; |
| final int flags; |
| |
| Configuration(Atlas.Type type, int width, int height, int count) { |
| this(type, width, height, count, Atlas.FLAG_DEFAULTS); |
| } |
| |
| Configuration(Atlas.Type type, int width, int height, int count, int flags) { |
| this.type = type; |
| this.width = width; |
| this.height = height; |
| this.count = count; |
| this.flags = flags; |
| } |
| |
| @Override |
| public String toString() { |
| return type.toString() + " (" + width + "x" + height + ") flags=0x" + |
| Integer.toHexString(flags) + " count=" + count; |
| } |
| } |
| |
| /** |
| * Used during the brute-force search to gather information about each |
| * variant of the packing algorithm. |
| */ |
| private static class WorkerResult { |
| Atlas.Type type; |
| int width; |
| int height; |
| int count; |
| |
| WorkerResult(Atlas.Type type, int width, int height, int count) { |
| this.type = type; |
| this.width = width; |
| this.height = height; |
| this.count = count; |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("%s %dx%d", type.toString(), width, height); |
| } |
| } |
| |
| /** |
| * A compute worker will try a finite number of variations of the packing |
| * algorithms and save the results in a supplied list. |
| */ |
| private static class ComputeWorker implements Runnable { |
| private final int mStart; |
| private final int mEnd; |
| private final int mStep; |
| private final List<Bitmap> mBitmaps; |
| private final List<WorkerResult> mResults; |
| private final CountDownLatch mSignal; |
| private final int mThreshold; |
| |
| /** |
| * Creates a new compute worker to brute-force through a range of |
| * packing algorithms variants. |
| * |
| * @param start The minimum texture width to try |
| * @param end The maximum texture width to try |
| * @param step The number of pixels to increment the texture width by at each step |
| * @param bitmaps The list of bitmaps to pack in the atlas |
| * @param pixelCount The total number of pixels occupied by the list of bitmaps |
| * @param results The list of results in which to save the brute-force search results |
| * @param signal Latch to decrement when this worker is done, may be null |
| */ |
| ComputeWorker(int start, int end, int step, List<Bitmap> bitmaps, int pixelCount, |
| List<WorkerResult> results, CountDownLatch signal) { |
| mStart = start; |
| mEnd = end; |
| mStep = step; |
| mBitmaps = bitmaps; |
| mResults = results; |
| mSignal = signal; |
| |
| // Minimum number of pixels we want to be able to pack |
| int threshold = (int) (pixelCount * PACKING_THRESHOLD); |
| // Make sure we can find at least one configuration |
| while (threshold > MAX_SIZE * MAX_SIZE) { |
| threshold >>= 1; |
| } |
| mThreshold = threshold; |
| } |
| |
| @Override |
| public void run() { |
| if (DEBUG_ATLAS) Log.d(LOG_TAG, "Running " + Thread.currentThread().getName()); |
| |
| Atlas.Entry entry = new Atlas.Entry(); |
| for (Atlas.Type type : Atlas.Type.values()) { |
| for (int width = mStart; width < mEnd; width += mStep) { |
| for (int height = MIN_SIZE; height < MAX_SIZE; height += STEP) { |
| // If the atlas is not big enough, skip it |
| if (width * height <= mThreshold) continue; |
| |
| final int count = packBitmaps(type, width, height, entry); |
| if (count > 0) { |
| mResults.add(new WorkerResult(type, width, height, count)); |
| // If we were able to pack everything let's stop here |
| // Increasing the height further won't make things better |
| if (count == mBitmaps.size()) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| if (mSignal != null) { |
| mSignal.countDown(); |
| } |
| } |
| |
| private int packBitmaps(Atlas.Type type, int width, int height, Atlas.Entry entry) { |
| int total = 0; |
| Atlas atlas = new Atlas(type, width, height); |
| |
| final int count = mBitmaps.size(); |
| for (int i = 0; i < count; i++) { |
| final Bitmap bitmap = mBitmaps.get(i); |
| if (atlas.pack(bitmap.getWidth(), bitmap.getHeight(), entry) != null) { |
| total++; |
| } |
| } |
| |
| return total; |
| } |
| } |
| } |