diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/GifDecoder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/GifDecoder.java
new file mode 100644
index 000000000..877e801f6
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/GifDecoder.java
@@ -0,0 +1,797 @@
+/**
+ * Copyright (c) 2013 Xcellent Creations, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.divoom;
+
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+/**
+ * Reads frame data from a GIF image source and decodes it into individual frames
+ * for animation purposes. Image data can be read from either and InputStream source
+ * or a byte[].
+ *
+ * This class is optimized for running animations with the frames, there
+ * are no methods to get individual frame images, only to decode the next frame in the
+ * animation sequence. Instead, it lowers its memory footprint by only housing the minimum
+ * data necessary to decode the next frame in the animation sequence.
+ *
+ * The animation must be manually moved forward using {@link #advance()} before requesting the next
+ * frame. This method must also be called before you request the first frame or an error will
+ * occur.
+ *
+ * Implementation adapted from sample code published in Lyons. (2004). Java for Programmers,
+ * republished under the MIT Open Source License
+ */
+public class GifDecoder {
+ private static final String TAG = GifDecoder.class.getSimpleName();
+
+ /**
+ * File read status: No errors.
+ */
+ public static final int STATUS_OK = 0;
+ /**
+ * File read status: Error decoding file (may be partially decoded)
+ */
+ public static final int STATUS_FORMAT_ERROR = 1;
+ /**
+ * File read status: Unable to open source.
+ */
+ public static final int STATUS_OPEN_ERROR = 2;
+ /**
+ * max decoder pixel stack size
+ */
+ protected static final int MAX_STACK_SIZE = 4096;
+
+ /**
+ * GIF Disposal Method meaning take no action
+ */
+ private static final int DISPOSAL_UNSPECIFIED = 0;
+ /**
+ * GIF Disposal Method meaning leave canvas from previous frame
+ */
+ private static final int DISPOSAL_NONE = 1;
+ /**
+ * GIF Disposal Method meaning clear canvas to background color
+ */
+ private static final int DISPOSAL_BACKGROUND = 2;
+ /**
+ * GIF Disposal Method meaning clear canvas to frame before last
+ */
+ private static final int DISPOSAL_PREVIOUS = 3;
+
+ /**
+ * Global status code of GIF data parsing
+ */
+ protected int status;
+
+ //Global File Header values and parsing flags
+ protected int width; // full image width
+ protected int height; // full image height
+ protected boolean gctFlag; // global color table used
+ protected int gctSize; // size of global color table
+ protected int loopCount = 1; // iterations; 0 = repeat forever
+ protected int[] gct; // global color table
+ protected int[] act; // active color table
+ protected int bgIndex; // background color index
+ protected int bgColor; // background color
+ protected int pixelAspect; // pixel aspect ratio
+ protected boolean lctFlag; // local color table flag
+ protected int lctSize; // local color table size
+
+ // Raw GIF data from input source
+ protected ByteBuffer rawData;
+
+ // Raw data read working array
+ protected byte[] block = new byte[256]; // current data block
+ protected int blockSize = 0; // block size last graphic control extension info
+
+ // LZW decoder working arrays
+ protected short[] prefix;
+ protected byte[] suffix;
+ protected byte[] pixelStack;
+ protected byte[] mainPixels;
+ protected int[] mainScratch, copyScratch;
+
+ protected ArrayList frames; // frames read from current file
+ protected GifFrame currentFrame;
+ protected Bitmap previousImage, currentImage, renderImage;
+
+ protected int framePointer;
+ protected int frameCount;
+
+ /**
+ * Inner model class housing metadata for each frame
+ */
+ private static class GifFrame {
+ public int ix, iy, iw, ih;
+ /* Control Flags */
+ public boolean interlace;
+ public boolean transparency;
+ /* Disposal Method */
+ public int dispose;
+ /* Transparency Index */
+ public int transIndex;
+ /* Delay, in ms, to next frame */
+ public int delay;
+ /* Index in the raw buffer where we need to start reading to decode */
+ public int bufferFrameStart;
+ /* Local Color Table */
+ public int[] lct;
+ }
+
+ /**
+ * Move the animation frame counter forward
+ */
+ public void advance() {
+ framePointer = (framePointer + 1) % frameCount;
+ }
+
+ /**
+ * Gets display duration for specified frame.
+ *
+ * @param n int index of frame
+ * @return delay in milliseconds
+ */
+ public int getDelay(int n) {
+ int delay = -1;
+ if ((n >= 0) && (n < frameCount)) {
+ delay = frames.get(n).delay;
+ }
+ return delay;
+ }
+
+ /**
+ * Gets display duration for the upcoming frame
+ */
+ public int getNextDelay() {
+ if (frameCount <=0 || framePointer < 0) {
+ return -1;
+ }
+
+ return getDelay(framePointer);
+ }
+
+ /**
+ * Gets the number of frames read from file.
+ *
+ * @return frame count
+ */
+ public int getFrameCount() {
+ return frameCount;
+ }
+
+ /**
+ * Gets the current index of the animation frame, or -1 if animation hasn't not yet started
+ *
+ * @return frame index
+ */
+ public int getCurrentFrameIndex() {
+ return framePointer;
+ }
+
+ /**
+ * Gets the "Netscape" iteration count, if any. A count of 0 means repeat indefinitiely.
+ *
+ * @return iteration count if one was specified, else 1.
+ */
+ public int getLoopCount() {
+ return loopCount;
+ }
+
+ /**
+ * Get the next frame in the animation sequence.
+ *
+ * @return Bitmap representation of frame
+ */
+ public Bitmap getNextFrame() {
+ if (frameCount <= 0 || framePointer < 0 || currentImage == null) {
+ return null;
+ }
+
+ GifFrame frame = frames.get(framePointer);
+
+ //Set the appropriate color table
+ if (frame.lct == null) {
+ act = gct;
+ } else {
+ act = frame.lct;
+ if (bgIndex == frame.transIndex) {
+ bgColor = 0;
+ }
+ }
+
+ int save = 0;
+ if (frame.transparency) {
+ save = act[frame.transIndex];
+ act[frame.transIndex] = 0; // set transparent color if specified
+ }
+ if (act == null) {
+ Log.w(TAG, "No Valid Color Table");
+ status = STATUS_FORMAT_ERROR; // no color table defined
+ return null;
+ }
+
+ setPixels(framePointer); // transfer pixel data to image
+
+ // Reset the transparent pixel in the color table
+ if (frame.transparency) {
+ act[frame.transIndex] = save;
+ }
+
+ return currentImage;
+ }
+
+ /**
+ * Reads GIF image from stream
+ *
+ * @param is containing GIF file.
+ * @return read status code (0 = no errors)
+ */
+ public int read(InputStream is, int contentLength) {
+ long startTime = System.currentTimeMillis();
+ if (is != null) {
+ try {
+ int capacity = (contentLength > 0) ? (contentLength + 4096) : 4096;
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream(capacity);
+ int nRead;
+ byte[] data = new byte[16384];
+ while ((nRead = is.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+
+ read(buffer.toByteArray());
+ } catch (IOException e) {
+ Log.w(TAG, "Error reading data from stream", e);
+ }
+ } else {
+ status = STATUS_OPEN_ERROR;
+ }
+
+ try {
+ is.close();
+ } catch (Exception e) {
+ Log.w(TAG, "Error closing stream", e);
+ }
+
+ return status;
+ }
+
+ /**
+ * Reads GIF image from byte array
+ *
+ * @param data containing GIF file.
+ * @return read status code (0 = no errors)
+ */
+ public int read(byte[] data) {
+ init();
+ if (data != null) {
+ //Initiliaze the raw data buffer
+ rawData = ByteBuffer.wrap(data);
+ rawData.rewind();
+ rawData.order(ByteOrder.LITTLE_ENDIAN);
+
+ readHeader();
+ if (!err()) {
+ readContents();
+ if (frameCount < 0) {
+ status = STATUS_FORMAT_ERROR;
+ }
+ }
+ } else {
+ status = STATUS_OPEN_ERROR;
+ }
+
+ return status;
+ }
+
+ /**
+ * Creates new frame image from current data (and previous frames as specified by their disposition codes).
+ */
+ protected void setPixels(int frameIndex) {
+ GifFrame currentFrame = frames.get(frameIndex);
+ GifFrame previousFrame = null;
+ int previousIndex = frameIndex - 1;
+ if (previousIndex >= 0) {
+ previousFrame = frames.get(previousIndex);
+ }
+
+ // final location of blended pixels
+ final int[] dest = mainScratch;
+
+ // fill in starting image contents based on last image's dispose code
+ if (previousFrame != null && previousFrame.dispose > DISPOSAL_UNSPECIFIED) {
+ if (previousFrame.dispose == DISPOSAL_NONE && currentImage != null) {
+ // Start with the current image
+ currentImage.getPixels(dest, 0, width, 0, 0, width, height);
+ }
+ if (previousFrame.dispose == DISPOSAL_BACKGROUND) {
+ // Start with a canvas filled with the background color
+ int c = 0;
+ if (!currentFrame.transparency) {
+ c = bgColor;
+ }
+ for (int i = 0; i < previousFrame.ih; i++) {
+ int n1 = (previousFrame.iy + i) * width + previousFrame.ix;
+ int n2 = n1 + previousFrame.iw;
+ for (int k = n1; k < n2; k++) {
+ dest[k] = c;
+ }
+ }
+ }
+ if (previousFrame.dispose == DISPOSAL_PREVIOUS && previousImage != null) {
+ // Start with the previous frame
+ previousImage.getPixels(dest, 0, width, 0, 0, width, height);
+ }
+ }
+
+ //Decode pixels for this frame into the global pixels[] scratch
+ decodeBitmapData(currentFrame, mainPixels); // decode pixel data
+
+ // copy each source line to the appropriate place in the destination
+ int pass = 1;
+ int inc = 8;
+ int iline = 0;
+ for (int i = 0; i < currentFrame.ih; i++) {
+ int line = i;
+ if (currentFrame.interlace) {
+ if (iline >= currentFrame.ih) {
+ pass++;
+ switch (pass) {
+ case 2:
+ iline = 4;
+ break;
+ case 3:
+ iline = 2;
+ inc = 4;
+ break;
+ case 4:
+ iline = 1;
+ inc = 2;
+ break;
+ default:
+ break;
+ }
+ }
+ line = iline;
+ iline += inc;
+ }
+ line += currentFrame.iy;
+ if (line < height) {
+ int k = line * width;
+ int dx = k + currentFrame.ix; // start of line in dest
+ int dlim = dx + currentFrame.iw; // end of dest line
+ if ((k + width) < dlim) {
+ dlim = k + width; // past dest edge
+ }
+ int sx = i * currentFrame.iw; // start of line in source
+ while (dx < dlim) {
+ // map color and insert in destination
+ int index = ((int) mainPixels[sx++]) & 0xff;
+ int c = act[index];
+ if (c != 0) {
+ dest[dx] = c;
+ }
+ dx++;
+ }
+ }
+ }
+
+ //Copy pixels into previous image
+ currentImage.getPixels(copyScratch, 0, width, 0, 0, width, height);
+ previousImage.setPixels(copyScratch, 0, width, 0, 0, width, height);
+ //Set pixels for current image
+ currentImage.setPixels(dest, 0, width, 0, 0, width, height);
+ }
+
+ /**
+ * Decodes LZW image data into pixel array. Adapted from John Cristy's BitmapMagick.
+ */
+ protected void decodeBitmapData(GifFrame frame, byte[] dstPixels) {
+ long startTime = System.currentTimeMillis();
+ long stepOne, stepTwo, stepThree;
+ if (frame != null) {
+ //Jump to the frame start position
+ rawData.position(frame.bufferFrameStart);
+ }
+
+ int nullCode = -1;
+ int npix = (frame == null) ? width * height : frame.iw * frame.ih;
+ int available, clear, code_mask, code_size, end_of_information, in_code, old_code, bits, code, count, i, datum, data_size, first, top, bi, pi;
+
+ if (dstPixels == null || dstPixels.length < npix) {
+ dstPixels = new byte[npix]; // allocate new pixel array
+ }
+ if (prefix == null) {
+ prefix = new short[MAX_STACK_SIZE];
+ }
+ if (suffix == null) {
+ suffix = new byte[MAX_STACK_SIZE];
+ }
+ if (pixelStack == null) {
+ pixelStack = new byte[MAX_STACK_SIZE + 1];
+ }
+
+ // Initialize GIF data stream decoder.
+ data_size = read();
+ clear = 1 << data_size;
+ end_of_information = clear + 1;
+ available = clear + 2;
+ old_code = nullCode;
+ code_size = data_size + 1;
+ code_mask = (1 << code_size) - 1;
+ for (code = 0; code < clear; code++) {
+ prefix[code] = 0; // XXX ArrayIndexOutOfBoundsException
+ suffix[code] = (byte) code;
+ }
+
+ // Decode GIF pixel stream.
+ datum = bits = count = first = top = pi = bi = 0;
+ for (i = 0; i < npix; ) {
+ if (top == 0) {
+ if (bits < code_size) {
+ // Load bytes until there are enough bits for a code.
+ if (count == 0) {
+ // Read a new data block.
+ count = readBlock();
+ if (count <= 0) {
+ break;
+ }
+ bi = 0;
+ }
+ datum += (((int) block[bi]) & 0xff) << bits;
+ bits += 8;
+ bi++;
+ count--;
+ continue;
+ }
+ // Get the next code.
+ code = datum & code_mask;
+ datum >>= code_size;
+ bits -= code_size;
+ // Interpret the code
+ if ((code > available) || (code == end_of_information)) {
+ break;
+ }
+ if (code == clear) {
+ // Reset decoder.
+ code_size = data_size + 1;
+ code_mask = (1 << code_size) - 1;
+ available = clear + 2;
+ old_code = nullCode;
+ continue;
+ }
+ if (old_code == nullCode) {
+ pixelStack[top++] = suffix[code];
+ old_code = code;
+ first = code;
+ continue;
+ }
+ in_code = code;
+ if (code == available) {
+ pixelStack[top++] = (byte) first;
+ code = old_code;
+ }
+ while (code > clear) {
+ pixelStack[top++] = suffix[code];
+ code = prefix[code];
+ }
+ first = ((int) suffix[code]) & 0xff;
+ // Add a new string to the string table,
+ if (available >= MAX_STACK_SIZE) {
+ break;
+ }
+ pixelStack[top++] = (byte) first;
+ prefix[available] = (short) old_code;
+ suffix[available] = (byte) first;
+ available++;
+ if (((available & code_mask) == 0) && (available < MAX_STACK_SIZE)) {
+ code_size++;
+ code_mask += available;
+ }
+ old_code = in_code;
+ }
+ // Pop a pixel off the pixel stack.
+ top--;
+ dstPixels[pi++] = pixelStack[top];
+ i++;
+ }
+
+ for (i = pi; i < npix; i++) {
+ dstPixels[i] = 0; // clear missing pixels
+ }
+ }
+
+ /**
+ * Returns true if an error was encountered during reading/decoding
+ */
+ protected boolean err() {
+ return status != STATUS_OK;
+ }
+
+ /**
+ * Initializes or re-initializes reader
+ */
+ protected void init() {
+ status = STATUS_OK;
+ frameCount = 0;
+ framePointer = -1;
+ frames = new ArrayList();
+ gct = null;
+ }
+
+ /**
+ * Reads a single byte from the input stream.
+ */
+ protected int read() {
+ int curByte = 0;
+ try {
+ curByte = (rawData.get() & 0xFF);
+ } catch (Exception e) {
+ status = STATUS_FORMAT_ERROR;
+ }
+ return curByte;
+ }
+
+ /**
+ * Reads next variable length block from input.
+ *
+ * @return number of bytes stored in "buffer"
+ */
+ protected int readBlock() {
+ blockSize = read();
+ int n = 0;
+ if (blockSize > 0) {
+ try {
+ int count;
+ while (n < blockSize) {
+ count = blockSize - n;
+ rawData.get(block, n, count);
+
+ n += count;
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Error Reading Block", e);
+ status = STATUS_FORMAT_ERROR;
+ }
+ }
+ return n;
+ }
+
+ /**
+ * Reads color table as 256 RGB integer values
+ *
+ * @param ncolors int number of colors to read
+ * @return int array containing 256 colors (packed ARGB with full alpha)
+ */
+ protected int[] readColorTable(int ncolors) {
+ int nbytes = 3 * ncolors;
+ int[] tab = null;
+ byte[] c = new byte[nbytes];
+
+ try {
+ rawData.get(c);
+
+ tab = new int[256]; // max size to avoid bounds checks
+ int i = 0;
+ int j = 0;
+ while (i < ncolors) {
+ int r = ((int) c[j++]) & 0xff;
+ int g = ((int) c[j++]) & 0xff;
+ int b = ((int) c[j++]) & 0xff;
+ tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
+ }
+ } catch (BufferUnderflowException e) {
+ Log.w(TAG, "Format Error Reading Color Table", e);
+ status = STATUS_FORMAT_ERROR;
+ }
+
+ return tab;
+ }
+
+ /**
+ * Main file parser. Reads GIF content blocks.
+ */
+ protected void readContents() {
+ // read GIF file content blocks
+ boolean done = false;
+ while (!(done || err())) {
+ int code = read();
+ switch (code) {
+ case 0x2C: // image separator
+ readBitmap();
+ break;
+ case 0x21: // extension
+ code = read();
+ switch (code) {
+ case 0xf9: // graphics control extension
+ //Start a new frame
+ currentFrame = new GifFrame();
+ readGraphicControlExt();
+ break;
+ case 0xff: // application extension
+ readBlock();
+ String app = "";
+ for (int i = 0; i < 11; i++) {
+ app += (char) block[i];
+ }
+ if (app.equals("NETSCAPE2.0")) {
+ readNetscapeExt();
+ } else {
+ skip(); // don't care
+ }
+ break;
+ case 0xfe:// comment extension
+ skip();
+ break;
+ case 0x01:// plain text extension
+ skip();
+ break;
+ default: // uninteresting extension
+ skip();
+ }
+ break;
+ case 0x3b: // terminator
+ done = true;
+ break;
+ case 0x00: // bad byte, but keep going and see what happens break;
+ default:
+ status = STATUS_FORMAT_ERROR;
+ }
+ }
+ }
+
+ /**
+ * Reads GIF file header information.
+ */
+ protected void readHeader() {
+ String id = "";
+ for (int i = 0; i < 6; i++) {
+ id += (char) read();
+ }
+ if (!id.startsWith("GIF")) {
+ status = STATUS_FORMAT_ERROR;
+ return;
+ }
+ readLSD();
+ if (gctFlag && !err()) {
+ gct = readColorTable(gctSize);
+ bgColor = gct[bgIndex];
+ }
+ }
+
+ /**
+ * Reads Graphics Control Extension values
+ */
+ protected void readGraphicControlExt() {
+ read(); // block size
+ int packed = read(); // packed fields
+ currentFrame.dispose = (packed & 0x1c) >> 2; // disposal method
+ if (currentFrame.dispose == 0) {
+ currentFrame.dispose = 1; // elect to keep old image if discretionary
+ }
+ currentFrame.transparency = (packed & 1) != 0;
+ currentFrame.delay = readShort() * 10; // delay in milliseconds
+ currentFrame.transIndex = read(); // transparent color index
+ read(); // block terminator
+ }
+
+ /**
+ * Reads next frame image
+ */
+ protected void readBitmap() {
+ currentFrame.ix = readShort(); // (sub)image position & size
+ currentFrame.iy = readShort();
+ currentFrame.iw = readShort();
+ currentFrame.ih = readShort();
+
+ int packed = read();
+ lctFlag = (packed & 0x80) != 0; // 1 - local color table flag interlace
+ lctSize = (int) Math.pow(2, (packed & 0x07) + 1);
+ // 3 - sort flag
+ // 4-5 - reserved lctSize = 2 << (packed & 7); // 6-8 - local color
+ // table size
+ currentFrame.interlace = (packed & 0x40) != 0;
+ if (lctFlag) {
+ currentFrame.lct = readColorTable(lctSize); // read table
+ } else {
+ currentFrame.lct = null; //No local color table
+ }
+
+ currentFrame.bufferFrameStart = rawData.position(); //Save this as the decoding position pointer
+
+ decodeBitmapData(null, mainPixels); // false decode pixel data to advance buffer
+ skip();
+ if (err()) {
+ return;
+ }
+
+ frameCount++;
+ frames.add(currentFrame); // add image to frame
+ }
+
+ /**
+ * Reads Logical Screen Descriptor
+ */
+ protected void readLSD() {
+ // logical screen size
+ width = readShort();
+ height = readShort();
+ // packed fields
+ int packed = read();
+ gctFlag = (packed & 0x80) != 0; // 1 : global color table flag
+ // 2-4 : color resolution
+ // 5 : gct sort flag
+ gctSize = 2 << (packed & 7); // 6-8 : gct size
+ bgIndex = read(); // background color index
+ pixelAspect = read(); // pixel aspect ratio
+
+ //Now that we know the size, init scratch arrays
+ mainPixels = new byte[width * height];
+ mainScratch = new int[width * height];
+ copyScratch = new int[width * height];
+
+ previousImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ currentImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ }
+
+ /**
+ * Reads Netscape extenstion to obtain iteration count
+ */
+ protected void readNetscapeExt() {
+ do {
+ readBlock();
+ if (block[0] == 1) {
+ // loop count sub-block
+ int b1 = ((int) block[1]) & 0xff;
+ int b2 = ((int) block[2]) & 0xff;
+ loopCount = (b2 << 8) | b1;
+ }
+ } while ((blockSize > 0) && !err());
+ }
+
+ /**
+ * Reads next 16-bit value, LSB first
+ */
+ protected int readShort() {
+ // read 16-bit value
+ return rawData.getShort();
+ }
+
+ /**
+ * Skips variable length blocks up to and including next zero length block.
+ */
+ protected void skip() {
+ do {
+ readBlock();
+ } while ((blockSize > 0) && !err());
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java
index 3bb2e54b3..815719eee 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/divoom/PixooProtocol.java
@@ -25,6 +25,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore;
+import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -32,7 +33,9 @@ import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.BufferedInputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
@@ -58,11 +61,14 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
+import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class PixooProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(PixooProtocol.class);
@@ -357,10 +363,36 @@ public class PixooProtocol extends GBDeviceProtocol {
protected byte[] encodeShowFrame(Uri uri) {
try {
- Bitmap bitmap = MediaStore.Images.Media.getBitmap(GBApplication.getContext().getContentResolver(), uri);
- return encodeShowFrame(bitmap);
+ Bitmap bitmap = null;
+ UriHelper uriHelper = null;
+ try {
+ uriHelper = UriHelper.get(uri, GBApplication.getContext());
+ } catch (IOException e) {
+ GB.toast(GBApplication.getContext(), "Could not open image: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ if (uriHelper != null) {
+ String fileName = AndroidUtils.getFilePath(GBApplication.getContext(), uri);
+ if (fileName.endsWith(".gif") || fileName.endsWith(".GIF")) {
+
+ try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
+ GifDecoder gifDecoder = new GifDecoder();
+ gifDecoder.read(in, 4096);
+ gifDecoder.advance();
+ gifDecoder.advance();
+
+ bitmap = gifDecoder.getNextFrame();
+ } catch (Exception e) {
+ GB.toast(GBApplication.getContext(), "Could not open image: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+ } else {
+ bitmap = MediaStore.Images.Media.getBitmap(GBApplication.getContext().getContentResolver(), uri);
+ }
+ if (bitmap != null) {
+ return encodeShowFrame(bitmap);
+ }
} catch (IOException e) {
- LOG.error("could not decode Image",e);
+ LOG.error("could not decode Image", e);
}
return null;
}