Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9c62b066 authored by Sal Savage's avatar Sal Savage Committed by Automerger Merge Worker
Browse files

Merge "Remove cover art's dependency on external storage" am: bac403d8

Original change: https://android-review.googlesource.com/c/platform/packages/apps/Bluetooth/+/1363341

Change-Id: Ic049c697705502a507653ec59d1935786328ddae
parents 19506f02 bac403d8
Loading
Loading
Loading
Loading
+14 −5
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.bluetooth.avrcpcontroller;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.SystemProperties;
import android.util.Log;
@@ -77,8 +78,7 @@ public class AvrcpCoverArtManager {
         * Notify of a get image download completing
         *
         * @param device The device the image handle belongs to
         * @param imageHandle The handle of the requested image
         * @param uri The Uri that the image is available at in storage
         * @param event The download event, containing the downloaded image's information
         */
        void onImageDownloadComplete(BluetoothDevice device, DownloadEvent event);
    }
@@ -193,7 +193,6 @@ public class AvrcpCoverArtManager {
        for (BluetoothDevice device : mClients.keySet()) {
            disconnect(device);
        }
        mCoverArtStorage.clear();
    }

    /**
@@ -310,10 +309,20 @@ public class AvrcpCoverArtManager {
    }

    /**
     * Remote a specific downloaded image if it exists
     * Get a specific downloaded image if it exists
     *
     * @param device The remote Bluetooth device associated with the image
     * @param imageUuid The UUID associated with the image you wish to retrieve
     */
    public Bitmap getImage(BluetoothDevice device, String imageUuid) {
        return mCoverArtStorage.getImage(device, imageUuid);
    }

    /**
     * Remove a specific downloaded image if it exists
     *
     * @param device The remote Bluetooth device associated with the image
     * @param imageHandle The handle associated with the image you wish to remove
     * @param imageUuid The UUID associated with the image you wish to remove
     */
    public void removeImage(BluetoothDevice device, String imageUuid) {
        mCoverArtStorage.removeImage(device, imageUuid);
+57 −18
Original line number Diff line number Diff line
@@ -21,12 +21,14 @@ import android.bluetooth.BluetoothDevice;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * A provider of downloaded cover art images.
@@ -48,7 +50,6 @@ public class AvrcpCoverArtProvider extends ContentProvider {
    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);

    private BluetoothAdapter mAdapter;
    private AvrcpCoverArtStorage mStorage;

    public AvrcpCoverArtProvider() {
    }
@@ -60,37 +61,69 @@ public class AvrcpCoverArtProvider extends ContentProvider {
     * Get the Uri for a cover art image based on the device and image handle
     *
     * @param device The Bluetooth device from which an image originated
     * @param imageHandle The provided handle of the cover artwork
     * @param imageUuid The provided UUID of the cover artwork
     * @return The Uri this provider will store the downloaded image at
     */
    public static Uri getImageUri(BluetoothDevice device, String imageHandle) {
        if (device == null || imageHandle == null || "".equals(imageHandle)) return null;
    public static Uri getImageUri(BluetoothDevice device, String imageUuid) {
        if (device == null || imageUuid == null || "".equals(imageUuid)) return null;
        Uri uri = CONTENT_URI.buildUpon().appendQueryParameter("device", device.getAddress())
                .appendQueryParameter("handle", imageHandle)
                .appendQueryParameter("uuid", imageUuid)
                .build();
        debug("getImageUri -> " + uri.toString());
        return uri;
    }

    private ParcelFileDescriptor getImageDescriptor(BluetoothDevice device, String imageHandle)
            throws FileNotFoundException {
        debug("getImageDescriptor(" + device + ", " + imageHandle + ")");
        File file = mStorage.getImageFile(device, imageHandle);
        if (file == null) throw new FileNotFoundException();
        ParcelFileDescriptor pdf = ParcelFileDescriptor.open(file,
                ParcelFileDescriptor.MODE_READ_ONLY);
        return pdf;
    private Bitmap getImage(BluetoothDevice device, String imageUuid) {
        AvrcpControllerService service = AvrcpControllerService.getAvrcpControllerService();
        if (service == null) {
            debug("Failed to get service, cover art not available");
            return null;
        }

        AvrcpCoverArtManager manager = service.getCoverArtManager();
        if (manager == null) {
            debug("Failed to get cover art manager. Cover art may not be enabled.");
            return null;
        }
        return manager.getImage(device, imageUuid);
    }

    private ParcelFileDescriptor getImageDescriptor(BluetoothDevice device, String imageUuid)
            throws FileNotFoundException, IOException {
        debug("getImageDescriptor(" + device + ", " + imageUuid + ")");
        Bitmap image = getImage(device, imageUuid);
        if (image == null) {
            debug("Could not get requested image");
            throw new FileNotFoundException();
        }

        final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
        Thread transferThread = new Thread() {
            public void run() {
                try {
                    FileOutputStream fout =
                            new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]);
                    image.compress(Bitmap.CompressFormat.PNG, 100, fout);
                    fout.flush();
                    fout.close();
                } catch (IOException e) {
                    /* Something bad must have happened writing the image data */
                }
            }
        };
        transferThread.start();
        return pipe[0];
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        debug("openFile(" + uri + ", '" + mode + "')");
        String address = null;
        String imageHandle = null;
        String imageUuid = null;
        BluetoothDevice device = null;
        try {
            address = uri.getQueryParameter("device");
            imageHandle = uri.getQueryParameter("handle");
            imageUuid = uri.getQueryParameter("uuid");
        } catch (NullPointerException e) {
            throw new FileNotFoundException();
        }
@@ -101,13 +134,19 @@ public class AvrcpCoverArtProvider extends ContentProvider {
            throw new FileNotFoundException();
        }

        return getImageDescriptor(device, imageHandle);
        ParcelFileDescriptor pfd = null;
        try {
            pfd = getImageDescriptor(device, imageUuid);
        } catch (IOException e) {
            debug("Failed to create inputstream from Bitmap");
            throw new FileNotFoundException();
        }
        return pfd;
    }

    @Override
    public boolean onCreate() {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mStorage = new AvrcpCoverArtStorage(getContext());
        return true;
    }

+64 −134
Original line number Diff line number Diff line
@@ -20,15 +20,13 @@ import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * An abstraction of the file system storage of the downloaded cover art images.
 * An abstraction of the cover art image storage mechanism.
 */
public class AvrcpCoverArtStorage {
    private static final String TAG = "AvrcpCoverArtStorage";
@@ -36,6 +34,15 @@ public class AvrcpCoverArtStorage {

    private final Context mContext;

    /* Each device gets its own place to land images. This makes it easier to clean things up on a
     * per device basis. This also allows us to be confident that acting on one device will not
     * impact the images of another.
     *
     * The "landing place" is simply a map that will direct a given UUID to the proper bitmap image
     */
    private final Map<BluetoothDevice, Map<String, Bitmap>> mDeviceImages =
            new ConcurrentHashMap<>(1);

    /**
     * Create and initialize this Cover Art storage interface
     */
@@ -47,94 +54,81 @@ public class AvrcpCoverArtStorage {
     * Determine if an image already exists in storage
     *
     * @param device - The device the images was downloaded from
     * @param imageHandle - The handle that identifies the image
     * @param imageUuid - The UUID that identifies the image
     */
    public boolean doesImageExist(BluetoothDevice device, String imageHandle) {
        if (device == null || imageHandle == null || "".equals(imageHandle)) return false;
        String path = getImagePath(device, imageHandle);
        if (path == null) return false;
        File file = new File(path);
        return file.exists();
    public boolean doesImageExist(BluetoothDevice device, String imageUuid) {
        if (device == null || imageUuid == null || "".equals(imageUuid)) return false;
        Map<String, Bitmap> images = mDeviceImages.get(device);
        if (images == null) return false;
        return images.containsKey(imageUuid);
    }

    /**
     * Retrieve an image file from storage
     *
     * @param device - The device the images was downloaded from
     * @param imageHandle - The handle that identifies the image
     * @return A file descriptor for the image
     * @param imageUuid - The UUID that identifies the image
     * @return A Bitmap object of the image
     */
    public File getImageFile(BluetoothDevice device, String imageHandle) {
        if (device == null || imageHandle == null || "".equals(imageHandle)) return null;
        String path = getImagePath(device, imageHandle);
        if (path == null) return null;
        File file = new File(path);
        return file.exists() ? file : null;
    public Bitmap getImage(BluetoothDevice device, String imageUuid) {
        if (device == null || imageUuid == null || "".equals(imageUuid)) return null;
        Map<String, Bitmap> images = mDeviceImages.get(device);
        if (images == null) return null;
        return images.get(imageUuid);
    }

    /**
     * Add an image to storage
     *
     * @param device - The device the images was downloaded from
     * @param imageHandle - The handle that identifies the image
     * @param imageUuid - The UUID that identifies the image
     * @param image - The image
     */
    public Uri addImage(BluetoothDevice device, String imageHandle, Bitmap image) {
        debug("Storing image '" + imageHandle + "' from device " + device);
        if (device == null || imageHandle == null || "".equals(imageHandle) || image == null) {
    public Uri addImage(BluetoothDevice device, String imageUuid, Bitmap image) {
        debug("Storing image '" + imageUuid + "' from device " + device);
        if (device == null || imageUuid == null || "".equals(imageUuid) || image == null) {
            debug("Cannot store image. Improper aruguments");
            return null;
        }

        String path = getImagePath(device, imageHandle);
        if (path == null) {
            error("Cannot store image. Cannot provide a valid path to storage");
            return null;
        }

        try {
            String deviceDirectoryPath = getDevicePath(device);
            if (deviceDirectoryPath == null) {
                error("Cannot store image. Cannot get a valid path to per-device storage");
                return null;
            }
            File deviceDirectory = new File(deviceDirectoryPath);
            if (!deviceDirectory.exists()) {
                deviceDirectory.mkdirs();
        // A Thread safe way of creating a new UUID->Image set for a device. The putIfAbsent()
        // function will return the value of the key if it wasn't absent. If it returns null, then
        // there was no value there and we are to assume the reference we passed in was added.
        Map<String, Bitmap> newImageSet = new ConcurrentHashMap<String, Bitmap>(1);
        Map<String, Bitmap> images = mDeviceImages.putIfAbsent(device, newImageSet);
        if (images == null) {
            newImageSet.put(imageUuid, image);
        } else {
            images.put(imageUuid, image);
        }

            FileOutputStream outputStream = new FileOutputStream(path);
            image.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
            outputStream.flush();
            outputStream.close();
        } catch (IOException e) {
            error("Failed to store '" + imageHandle + "' to '" + path + "'");
            return null;
        }
        Uri uri = AvrcpCoverArtProvider.getImageUri(device, imageHandle);
        Uri uri = AvrcpCoverArtProvider.getImageUri(device, imageUuid);
        mContext.getContentResolver().notifyChange(uri, null);
        debug("Image stored at '" + path + "'");
        debug("Image '" + imageUuid + "' stored for device '" + device.getAddress() + "'");
        return uri;
    }

    /**
     * Remove a specific image
     *
     * @param device The device you wish to have images removed for
     * @param imageHandle The handle that identifies the image to delete
     * @param device The device the image belongs to
     * @param imageUuid - The UUID that identifies the image
     */
    public void removeImage(BluetoothDevice device, String imageHandle) {
        debug("Removing image '" + imageHandle + "' from device " + device);
        if (device == null || imageHandle == null || "".equals(imageHandle)) return;
        String path = getImagePath(device, imageHandle);
        if (path == null) {
            error("Cannot remove image. Cannot get a valid path to storage");
    public void removeImage(BluetoothDevice device, String imageUuid) {
        debug("Removing image '" + imageUuid + "' from device " + device);
        if (device == null || imageUuid == null || "".equals(imageUuid)) return;

        Map<String, Bitmap> images = mDeviceImages.get(device);
        if (images == null) {
            return;
        }
        File file = new File(path);
        if (!file.exists()) return;
        file.delete();
        debug("Image deleted at '" + path + "'");

        images.remove(imageUuid);
        if (images.size() == 0) {
            mDeviceImages.remove(device);
        }

        debug("Image '" + imageUuid + "' removed for device '" + device.getAddress() + "'");
    }

    /**
@@ -145,91 +139,27 @@ public class AvrcpCoverArtStorage {
    public void removeImagesForDevice(BluetoothDevice device) {
        if (device == null) return;
        debug("Remove cover art for device " + device.getAddress());
        String deviceDirectoryPath = getDevicePath(device);
        if (deviceDirectoryPath == null) {
            error("Cannot remove images for device. Cannot get a valid path to storage");
            return;
        }
        File deviceDirectory = new File(deviceDirectoryPath);
        deleteStorageDirectory(deviceDirectory);
        mDeviceImages.remove(device);
    }

    /**
     * Clear the entirety of storage
     */
    public void clear() {
        String storageDirectoryPath = getStorageDirectory();
        if (storageDirectoryPath == null) {
            error("Cannot remove images, cannot get a valid path to storage. Is it mounted?");
            return;
        }
        File storageDirectory = new File(storageDirectoryPath);
        deleteStorageDirectory(storageDirectory);
    }

    private String getStorageDirectory() {
        String dir = null;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            dir = mContext.getExternalFilesDir(null).getAbsolutePath() + "/coverart";
        } else {
            error("Cannot get storage directory, state=" + Environment.getExternalStorageState());
        }
        return dir;
    }

    private String getDevicePath(BluetoothDevice device) {
        String storageDir = getStorageDirectory();
        if (storageDir == null) return null;
        return storageDir + "/" + device.getAddress().replace(":", "");
    }

    private String getImagePath(BluetoothDevice device, String imageHandle) {
        String deviceDir = getDevicePath(device);
        if (deviceDir == null) return null;
        return deviceDir + "/" + imageHandle + ".png";
    }

    private void deleteStorageDirectory(File directory) {
        if (directory == null) {
            error("Cannot delete directory, file is null");
            return;
        }
        if (!directory.exists()) return;
        File[] files = directory.listFiles();
        if (files == null) {
            return;
        }
        for (int i = 0; i < files.length; i++) {
            debug("Deleting " + files[i].getAbsolutePath());
            if (files[i].isDirectory()) {
                deleteStorageDirectory(files[i]);
            } else {
                files[i].delete();
            }
        }
        directory.delete();
        debug("Clearing all images");
        mDeviceImages.clear();
    }

    @Override
    public String toString() {
        String s = "CoverArtStorage:\n";
        String storageDirectory = getStorageDirectory();
        s += "    Storage Directory: " + storageDirectory + "\n";
        if (storageDirectory == null) {
            return s;
        }

        File storage = new File(storageDirectory);
        File[] devices = storage.listFiles();
        if (devices != null) {
            for (File deviceDirectory : devices) {
                s += "    " + deviceDirectory.getName() + ":\n";
                File[] images = deviceDirectory.listFiles();
                if (images == null) continue;
                for (File image : images) {
                    s += "      " + image.getName() + "\n";
                }
        for (BluetoothDevice device : mDeviceImages.keySet()) {
            Map<String, Bitmap> images = mDeviceImages.get(device);
            s += "  " + device.getAddress() + " (" + images.size() + "):";
            for (String uuid : images.keySet()) {
                s += "\n    " + uuid;
            }
            s += "\n";
        }
        return s;
    }
+10 −12
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.File;
import java.io.InputStream;

/**
@@ -89,9 +88,8 @@ public final class AvrcpCoverArtStorageTest {
    }

    private void assertImageSame(Bitmap expected, BluetoothDevice device, String handle) {
        File file = mAvrcpCoverArtStorage.getImageFile(device, handle);
        Bitmap fromStorage = BitmapFactory.decodeFile(file.getPath());
        Assert.assertTrue(expected.sameAs(fromStorage));
        Bitmap image = mAvrcpCoverArtStorage.getImage(device, handle);
        Assert.assertTrue(expected.sameAs(image));
    }

    @Test
@@ -203,29 +201,29 @@ public final class AvrcpCoverArtStorageTest {
    @Test
    public void getImageThatDoesntExist_returnsNull() {
        Assert.assertFalse(mAvrcpCoverArtStorage.doesImageExist(mDevice1, mHandle1));
        File file = mAvrcpCoverArtStorage.getImageFile(mDevice1, mHandle1);
        Assert.assertEquals(null, file);
        Bitmap image = mAvrcpCoverArtStorage.getImage(mDevice1, mHandle1);
        Assert.assertEquals(null, image);
    }

    @Test
    public void getImageNullDevice_returnsNull() {
        Assert.assertFalse(mAvrcpCoverArtStorage.doesImageExist(mDevice1, mHandle1));
        File file = mAvrcpCoverArtStorage.getImageFile(null, mHandle1);
        Assert.assertEquals(null, file);
        Bitmap image = mAvrcpCoverArtStorage.getImage(null, mHandle1);
        Assert.assertEquals(null, image);
    }

    @Test
    public void getImageNullHandle_returnsNull() {
        Assert.assertFalse(mAvrcpCoverArtStorage.doesImageExist(mDevice1, mHandle1));
        File file = mAvrcpCoverArtStorage.getImageFile(mDevice1, null);
        Assert.assertEquals(null, file);
        Bitmap image = mAvrcpCoverArtStorage.getImage(mDevice1, null);
        Assert.assertEquals(null, image);
    }

    @Test
    public void getImageEmptyHandle_returnsNull() {
        Assert.assertFalse(mAvrcpCoverArtStorage.doesImageExist(mDevice1, mHandle1));
        File file = mAvrcpCoverArtStorage.getImageFile(mDevice1, "");
        Assert.assertEquals(null, file);
        Bitmap image = mAvrcpCoverArtStorage.getImage(mDevice1, "");
        Assert.assertEquals(null, image);
    }

    @Test