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

Commit 7a0cbc4b 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 am:...

Merge "Remove cover art's dependency on external storage" am: bac403d8 am: 9c62b066 am: 0ee52a27

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

Change-Id: I567c7e25a01a506661580b271374228bacb4d110
parents 92369529 0ee52a27
Loading
Loading
Loading
Loading
+14 −5
Original line number Original line Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.bluetooth.avrcpcontroller;


import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProfile;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.Uri;
import android.os.SystemProperties;
import android.os.SystemProperties;
import android.util.Log;
import android.util.Log;
@@ -77,8 +78,7 @@ public class AvrcpCoverArtManager {
         * Notify of a get image download completing
         * Notify of a get image download completing
         *
         *
         * @param device The device the image handle belongs to
         * @param device The device the image handle belongs to
         * @param imageHandle The handle of the requested image
         * @param event The download event, containing the downloaded image's information
         * @param uri The Uri that the image is available at in storage
         */
         */
        void onImageDownloadComplete(BluetoothDevice device, DownloadEvent event);
        void onImageDownloadComplete(BluetoothDevice device, DownloadEvent event);
    }
    }
@@ -193,7 +193,6 @@ public class AvrcpCoverArtManager {
        for (BluetoothDevice device : mClients.keySet()) {
        for (BluetoothDevice device : mClients.keySet()) {
            disconnect(device);
            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 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) {
    public void removeImage(BluetoothDevice device, String imageUuid) {
        mCoverArtStorage.removeImage(device, imageUuid);
        mCoverArtStorage.removeImage(device, imageUuid);
+57 −18
Original line number Original line Diff line number Diff line
@@ -21,12 +21,14 @@ import android.bluetooth.BluetoothDevice;
import android.content.ContentProvider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.util.Log;


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


/**
/**
 * A provider of downloaded cover art images.
 * 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 static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);


    private BluetoothAdapter mAdapter;
    private BluetoothAdapter mAdapter;
    private AvrcpCoverArtStorage mStorage;


    public AvrcpCoverArtProvider() {
    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
     * 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 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
     * @return The Uri this provider will store the downloaded image at
     */
     */
    public static Uri getImageUri(BluetoothDevice device, String imageHandle) {
    public static Uri getImageUri(BluetoothDevice device, String imageUuid) {
        if (device == null || imageHandle == null || "".equals(imageHandle)) return null;
        if (device == null || imageUuid == null || "".equals(imageUuid)) return null;
        Uri uri = CONTENT_URI.buildUpon().appendQueryParameter("device", device.getAddress())
        Uri uri = CONTENT_URI.buildUpon().appendQueryParameter("device", device.getAddress())
                .appendQueryParameter("handle", imageHandle)
                .appendQueryParameter("uuid", imageUuid)
                .build();
                .build();
        debug("getImageUri -> " + uri.toString());
        debug("getImageUri -> " + uri.toString());
        return uri;
        return uri;
    }
    }


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

        return pdf;
        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
    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        debug("openFile(" + uri + ", '" + mode + "')");
        debug("openFile(" + uri + ", '" + mode + "')");
        String address = null;
        String address = null;
        String imageHandle = null;
        String imageUuid = null;
        BluetoothDevice device = null;
        BluetoothDevice device = null;
        try {
        try {
            address = uri.getQueryParameter("device");
            address = uri.getQueryParameter("device");
            imageHandle = uri.getQueryParameter("handle");
            imageUuid = uri.getQueryParameter("uuid");
        } catch (NullPointerException e) {
        } catch (NullPointerException e) {
            throw new FileNotFoundException();
            throw new FileNotFoundException();
        }
        }
@@ -101,13 +134,19 @@ public class AvrcpCoverArtProvider extends ContentProvider {
            throw new FileNotFoundException();
            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
    @Override
    public boolean onCreate() {
    public boolean onCreate() {
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mAdapter = BluetoothAdapter.getDefaultAdapter();
        mStorage = new AvrcpCoverArtStorage(getContext());
        return true;
        return true;
    }
    }


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


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


/**
/**
 * 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 {
public class AvrcpCoverArtStorage {
    private static final String TAG = "AvrcpCoverArtStorage";
    private static final String TAG = "AvrcpCoverArtStorage";
@@ -36,6 +34,15 @@ public class AvrcpCoverArtStorage {


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


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


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


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

        if (images == null) {
        try {
            newImageSet.put(imageUuid, image);
            String deviceDirectoryPath = getDevicePath(device);
        } else {
            if (deviceDirectoryPath == null) {
            images.put(imageUuid, image);
                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();
        }
        }


            FileOutputStream outputStream = new FileOutputStream(path);
        Uri uri = AvrcpCoverArtProvider.getImageUri(device, imageUuid);
            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);
        mContext.getContentResolver().notifyChange(uri, null);
        mContext.getContentResolver().notifyChange(uri, null);
        debug("Image stored at '" + path + "'");
        debug("Image '" + imageUuid + "' stored for device '" + device.getAddress() + "'");
        return uri;
        return uri;
    }
    }


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

        if (path == null) {
        Map<String, Bitmap> images = mDeviceImages.get(device);
            error("Cannot remove image. Cannot get a valid path to storage");
        if (images == null) {
            return;
            return;
        }
        }
        File file = new File(path);

        if (!file.exists()) return;
        images.remove(imageUuid);
        file.delete();
        if (images.size() == 0) {
        debug("Image deleted at '" + path + "'");
            mDeviceImages.remove(device);
        }

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


    /**
    /**
@@ -145,91 +139,27 @@ public class AvrcpCoverArtStorage {
    public void removeImagesForDevice(BluetoothDevice device) {
    public void removeImagesForDevice(BluetoothDevice device) {
        if (device == null) return;
        if (device == null) return;
        debug("Remove cover art for device " + device.getAddress());
        debug("Remove cover art for device " + device.getAddress());
        String deviceDirectoryPath = getDevicePath(device);
        mDeviceImages.remove(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);
    }
    }


    /**
    /**
     * Clear the entirety of storage
     * Clear the entirety of storage
     */
     */
    public void clear() {
    public void clear() {
        String storageDirectoryPath = getStorageDirectory();
        debug("Clearing all images");
        if (storageDirectoryPath == null) {
        mDeviceImages.clear();
            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();
    }
    }


    @Override
    @Override
    public String toString() {
    public String toString() {
        String s = "CoverArtStorage:\n";
        String s = "CoverArtStorage:\n";
        String storageDirectory = getStorageDirectory();
        for (BluetoothDevice device : mDeviceImages.keySet()) {
        s += "    Storage Directory: " + storageDirectory + "\n";
            Map<String, Bitmap> images = mDeviceImages.get(device);
        if (storageDirectory == null) {
            s += "  " + device.getAddress() + " (" + images.size() + "):";
            return s;
            for (String uuid : images.keySet()) {
        }
                s += "\n    " + uuid;

        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";
                }
            }
            }
            s += "\n";
        }
        }
        return s;
        return s;
    }
    }
+10 −12
Original line number Original line Diff line number Diff line
@@ -34,7 +34,6 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runner.RunWith;


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


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


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


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


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


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


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


    @Test
    @Test