/* * 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.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; 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 AssetAtlasService. */ public static final String ASSET_ATLAS_SERVICE = "assetatlas"; private static final String LOG_TAG = "AssetAtlas"; // 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 = 512; // 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 = 3; // 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: // long0: SkBitmap*, the native bitmap object // long1: x position // long2: y position private long[] 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); Collection bitmaps = new HashSet(300); int totalPixelCount = 0; // We only care about drawables that hold bitmaps final Resources resources = context.getResources(); final LongSparseArray drawables = resources.getPreloadedDrawables(); final int count = drawables.size(); for (int i = 0; i < count; i++) { try { totalPixelCount += drawables.valueAt(i).addAtlasableBitmaps(bitmaps); } catch (Throwable t) { Log.e("AssetAtlas", "Failed to fetch preloaded drawable state", t); throw t; } } ArrayList sortedBitmaps = new ArrayList(bitmaps); // Our algorithms perform better when the bitmaps are first sorted // The comparator will sort the bitmap by width first, then by height Collections.sort(sortedBitmaps, new Comparator() { @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(sortedBitmaps, 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, PackageManager.MATCH_DEBUG_TRIAGED_MISSING); 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 mBitmaps; private final int mPixelCount; Renderer(ArrayList 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. * * @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 Bitmap atlasBitmap = Bitmap.createBitmap( buffer.getWidth(), buffer.getHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(atlasBitmap); final Atlas.Entry entry = new Atlas.Entry(); mAtlasMap = new long[packCount * ATLAS_MAP_ENTRY_FIELD_COUNT]; long[] atlasMap = mAtlasMap; int mapIndex = 0; boolean result = false; 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); canvas.drawBitmap(bitmap, 0.0f, 0.0f, null); canvas.restore(); atlasMap[mapIndex++] = bitmap.refSkPixelRef(); atlasMap[mapIndex++] = entry.x; atlasMap[mapIndex++] = entry.y; } } final long endRender = System.nanoTime(); releaseCanvas(canvas, atlasBitmap); result = nUploadAtlas(buffer, atlasBitmap); atlasBitmap.recycle(); 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)); } return result; } /** * 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, Bitmap atlasBitmap) { canvas.setBitmap(null); if (DEBUG_ATLAS_TEXTURE) { File systemDirectory = new File(Environment.getDataDirectory(), "system"); File dataFile = new File(systemDirectory, "atlas.png"); try { FileOutputStream out = new FileOutputStream(dataFile); atlasBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); out.close(); } catch (FileNotFoundException e) { // Ignore } catch (IOException e) { // Ignore } } } } private static native boolean nUploadAtlas(GraphicBuffer buffer, Bitmap 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 long[] 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 bitmaps, int pixelCount) { if (DEBUG_ATLAS) Log.d(LOG_TAG, "Computing best atlas configuration..."); long begin = System.nanoTime(); List results = Collections.synchronizedList(new ArrayList()); // 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 + (cpuCount - 1) * STEP; int end = MAX_SIZE; 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(); } boolean isAllWorkerFinished; try { isAllWorkerFinished = signal.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.w(LOG_TAG, "Could not complete configuration computation"); return null; } if (!isAllWorkerFinished) { // We have to abort here, otherwise the async updates on "results" would crash the // sort later. Log.w(LOG_TAG, "Could not complete configuration computation before timeout."); return null; } } // Maximize the number of packed bitmaps, minimize the texture size Collections.sort(results, new Comparator() { @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 (out of %d) in %.2fs", results.size(), 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 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 mBitmaps; private final List 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 bitmaps, int pixelCount, List 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 (int width = mEnd; width > mStart; width -= mStep) { for (int height = MAX_SIZE; height > MIN_SIZE; height -= STEP) { // If the atlas is not big enough, skip it if (width * height <= mThreshold) continue; boolean packSuccess = false; for (Atlas.Type type : Atlas.Type.values()) { final int count = packBitmaps(type, width, height, entry); if (count > 0) { mResults.add(new WorkerResult(type, width, height, count)); if (count == mBitmaps.size()) { // If we were able to pack everything let's stop here // Changing the type further won't make things better packSuccess = true; break; } } } // If we were not able to pack everything let's stop here // Decreasing the height further won't make things better if (!packSuccess) { 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; } } }