All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.smartdevicelink.managers.file.UploadFileOperation Maven / Gradle / Ivy

/*
 * Copyright (c) 2020 Livio, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following
 * disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * Neither the name of the Livio Inc. nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.smartdevicelink.managers.file;

import androidx.annotation.NonNull;

import com.livio.taskmaster.Task;
import com.smartdevicelink.managers.ISdl;
import com.smartdevicelink.managers.file.filetypes.SdlFile;
import com.smartdevicelink.protocol.SdlProtocolBase;
import com.smartdevicelink.protocol.enums.SessionType;
import com.smartdevicelink.proxy.RPCResponse;
import com.smartdevicelink.proxy.rpc.PutFile;
import com.smartdevicelink.proxy.rpc.PutFileResponse;
import com.smartdevicelink.proxy.rpc.listeners.OnRPCResponseListener;
import com.smartdevicelink.util.DebugTool;
import com.smartdevicelink.util.Version;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;

/**
 * Created by Bilal Alsharifi on 12/1/20.
 */
class UploadFileOperation extends Task {
    private static final String TAG = "UploadFileOperation";
    private final WeakReference internalInterface;
    private final WeakReference fileManager;
    private SdlFileWrapper fileWrapper;
    private InputStream inputStream;
    private int fileSize;
    private String streamError;
    private int bytesAvailable;
    private int highestCorrelationIDReceived;

    UploadFileOperation(ISdl internalInterface, BaseFileManager fileManager, SdlFileWrapper fileWrapper) {
        super("UploadFileOperation");

        this.internalInterface = new WeakReference<>(internalInterface);
        this.fileManager = new WeakReference<>(fileManager);
        this.fileWrapper = fileWrapper;
    }

    @Override
    public void onExecute() {
        start();
    }

    private void start() {
        if (getState() == Task.CANCELED) {
            return;
        }

        SdlFile file = fileWrapper.getFile();
        // HAX: [#827](https://github.com/smartdevicelink/sdl_ios/issues/827) Older versions of Core
        // had a bug where list files would cache incorrectly. This led to attempted uploads failing
        // due to the system thinking they were already there when they were not. This is only needed
        // if connecting to Core v4.3.1 or less which corresponds to RPC v4.3.1 or less
        if (internalInterface.get() != null && fileManager.get() != null) {
            Version rpcVersion = new Version(internalInterface.get().getSdlMsgVersion());
            if (!file.isPersistent() && !fileManager.get().hasUploadedFile(file) && new Version(4, 4, 0).isNewerThan(rpcVersion) == 1) {
                file.setOverwrite(true);
            }
            // Check our overwrite settings and error out if it would overwrite
            if (!file.getOverwrite() && fileManager.get().mutableRemoteFileNames.contains(file.getName())) {
                DebugTool.logWarning(TAG, fileManager.get().fileManagerCannotOverwriteError);
                if (this.fileWrapper.getCompletionListener() != null) {
                    this.fileWrapper.getCompletionListener().onComplete(true, bytesAvailable, null, fileManager.get().fileManagerCannotOverwriteError);
                }
                onFinished();
                return;
            }
        }

        int mtuSize = 0;
        if (internalInterface.get() != null) {
            mtuSize = (int) internalInterface.get().getMtu(SessionType.RPC);
        }
        sendFile(this.fileWrapper.getFile(), mtuSize, this.fileWrapper.getCompletionListener());
    }

    /**
     * Sends data asynchronously to the SDL Core by breaking the data into smaller packets, each of which is
     * sent via a PutFile. If the SDL Core receives all the PutFile successfully, a success response with
     * the amount of free storage space left on the SDL Core is returned. Otherwise the error returned by
     * the SDL Core is passed along.
     *
     * @param file               The file containing the data to be sent to the SDL Core
     * @param mtuSize            The maximum packet size allowed
     * @param completionListener listener returning whether or not the upload was a success
     */
    private void sendFile(SdlFile file, int mtuSize, final FileManagerCompletionListener completionListener) {
        streamError = null;
        bytesAvailable = BaseFileManager.SPACE_AVAILABLE_MAX_VALUE;
        highestCorrelationIDReceived = -1;

        if (getState() == Task.CANCELED) {
            String errorMessage = "The file upload transaction was canceled before it could be completed.";
            completionListener.onComplete(false, bytesAvailable, null, errorMessage);
            onFinished();
            return;
        }

        if (file == null) {
            String errorMessage = "The file manager was unable to send the file. This could be because the file does not exist at the specified file path or that passed data is invalid.";
            completionListener.onComplete(false, bytesAvailable, null, errorMessage);
            onFinished();
            return;
        }

        if (fileManager.get() != null) {
            this.inputStream = fileManager.get().openInputStreamWithFile(file);
            this.fileSize = getFileSizeFromInputStream(inputStream);
        }

        int maxBulkDataSize = getMaxBulkDataSize(mtuSize, file, fileSize);

        // If the file does not exist or the passed data is null, return an error
        if (inputStream == null || fileSize == 0) {
            closeInputStream();

            String errorMessage = "The file manager was unable to send the file. This could be because the file does not exist at the specified file path or that passed data is invalid.";
            completionListener.onComplete(false, bytesAvailable, null, errorMessage);
            onFinished();
            return;
        }

        final DispatchGroup putFileGroup = new DispatchGroup();
        putFileGroup.enter();

        // Wait for all packets be sent before returning whether or not the upload was a success
        putFileGroup.notify(new Runnable() {
            @Override
            public void run() {
                closeInputStream();

                if (streamError != null || getState() == Task.CANCELED) {
                    completionListener.onComplete(false, bytesAvailable, null, streamError);
                } else {
                    completionListener.onComplete(true, bytesAvailable, null, null);
                }

                onFinished();
            }
        });

        // Break the data into small pieces, each of which will be sent in a separate PutFile
        int currentOffset = 0;
        int numberOfPieces = ((fileSize - 1) / maxBulkDataSize) + 1;
        for (int i = 0; i < numberOfPieces; i++) {
            putFileGroup.enter();

            // Get a chunk of data from the input stream
            int putFileLength = getPutFileLengthForOffset(currentOffset, fileSize, maxBulkDataSize);
            int putFileBulkDataSize = getDataSizeForOffset(currentOffset, fileSize, maxBulkDataSize);
            byte[] putFileBulkData = getDataChunkWithSize(putFileBulkDataSize, this.inputStream);

            final PutFile putFile = new PutFile(file.getName(), file.getType())
                    .setPersistentFile(file.isPersistent())
                    .setSystemFile(false)
                    .setOffset(currentOffset)
                    .setLength(putFileLength)
                    .setCRC(file.getFileData());
            putFile.setBulkData(putFileBulkData);
            putFile.setOnRPCResponseListener(new OnRPCResponseListener() {
                @Override
                public void onResponse(int correlationId, RPCResponse response) {
                    PutFileResponse putFileResponse = (PutFileResponse) response;

                    // Check if the upload process has been cancelled by another packet. If so, stop the upload process.
                    if (getState() == Task.CANCELED) {
                        putFileGroup.leave();
                        return;
                    }

                    // If the SDL Core returned an error, cancel the upload the process in the future
                    if (!response.getSuccess() || getState() == Task.CANCELED) {
                        streamError = response.getInfo() + ": " + response.getResultCode();
                        putFileGroup.leave();
                        cancelTask();
                        return;
                    }

                    // If no errors, watch for a response containing the amount of storage left on the SDL Core
                    if (newHighestCorrelationID(correlationId, highestCorrelationIDReceived)) {
                        highestCorrelationIDReceived = correlationId;

                        // If spaceAvailable is null, set it to the max value
                        bytesAvailable = putFileResponse.getSpaceAvailable() != null ? putFileResponse.getSpaceAvailable() : BaseFileManager.SPACE_AVAILABLE_MAX_VALUE;
                    }

                    putFileGroup.leave();
                }
            });

            currentOffset += putFileBulkDataSize;

            if (internalInterface.get() != null) {
                internalInterface.get().sendRPC(putFile);
            }
        }

        putFileGroup.leave();
    }

    /**
     * Close the input stream once all the data has been read
     */
    private void closeInputStream() {
        if (this.inputStream == null) {
            return;
        }
        try {
            this.inputStream.close();
        } catch (IOException e) {
            DebugTool.logError(TAG, "Error attempting to close input stream", e);
        }
    }

    /**
     * Returns the max possible size for the JSON data in each of the PutFile pieces.
     *
     * @param file     The file containing the data to be sent to the SDL Core
     * @param fileSize The size of the file
     * @return max possible size for the JSON data
     */
    private int getMaxJSONSize(@NonNull SdlFile file, int fileSize) {
        int maxJSONSize = 0;

        final PutFile putFile = new PutFile(file.getName(), file.getType())
                .setPersistentFile(file.isPersistent())
                .setSystemFile(false)
                .setOffset(fileSize)
                .setLength(fileSize)
                .setCRC(file.getFileData());

        if (putFile != null && putFile.getStore() != null) {
            maxJSONSize = putFile.getStore().toString().getBytes().length;
        }
        return maxJSONSize;
    }

    /**
     * Returns the max size of bulk data that we can load into each PutFile to guarantee that the
     * packet size do not exceed the max MTU size allowed by the SDL Core.
     *
     * @param mtuSize  The maximum packet size allowed
     * @param file     The file containing the data to be sent to the SDL Core
     * @param fileSize The size of the file
     * @return max size of bulk data that we can load into each PutFile
     */
    private int getMaxBulkDataSize(int mtuSize, @NonNull SdlFile file, int fileSize) {
        // Each RPC packet contains : frame header + payload (binary header + JSON data + bulk data)
        // To make sure that packets do not exceed MTU size, the bulk data size for each packet should not exceed:
        // mtuSize - (frameHeaderSize + binaryHeaderSize + maxJSONSize)

        int frameHeaderSize = SdlProtocolBase.V2_HEADER_SIZE;
        int binaryHeaderSize = 12;
        int maxJSONSize = getMaxJSONSize(file, fileSize);
        return mtuSize - (frameHeaderSize + binaryHeaderSize + maxJSONSize);
    }

    /**
     * Returns the length of the data being sent in the PutFile. The first PutFile's length is unique in
     * that it sends the full size of the data. For the rest of the PutFiles, the length parameter is equal
     * to the size of the chunk of data being sent in the PutFile.
     *
     * @param currentOffset   The current position in the file
     * @param fileSize        The size of the file
     * @param maxBulkDataSize The max size of bulk data that we can load into each PutFile
     * @return The length of the data being sent in the PutFile
     */
    private int getPutFileLengthForOffset(int currentOffset, int fileSize, int maxBulkDataSize) {
        int putFileLength;
        if (currentOffset == 0) {
            // The first PutFile sends the full file size
            putFileLength = fileSize;
        } else if ((fileSize - currentOffset) < maxBulkDataSize) {
            // The last PutFile sends the size of the remaining data
            putFileLength = fileSize - currentOffset;
        } else {
            // All other PutFiles send the maximum bulk data size
            putFileLength = maxBulkDataSize;
        }
        return putFileLength;
    }

    /**
     * Gets the size of the data to be sent in a packet.
     * Packet size can not be greater than the max MTU size allowed by the SDL Core.
     *
     * @param currentOffset   The position in the file where to start reading data
     * @param fileSize        he size of the file
     * @param maxBulkDataSize The max size of bulk data that we can load into each PutFile
     * @return The size of the data to be sent in the packet.
     */
    private int getDataSizeForOffset(int currentOffset, int fileSize, int maxBulkDataSize) {
        int dataSize;
        int fileSizeRemaining = fileSize - currentOffset;
        if (fileSizeRemaining < maxBulkDataSize) {
            dataSize = fileSizeRemaining;
        } else {
            dataSize = maxBulkDataSize;
        }
        return dataSize;
    }

    /**
     * Reads a chunk of data from input stream.
     *
     * @param size        The amount of data to read from the input stream
     * @param inputStream The stream from which to read the data
     * @return The data read from the socket
     */
    private byte[] getDataChunkWithSize(int size, InputStream inputStream) {
        if (size < 0) {
            return null;
        }

        int bytesRead = 0;
        byte[] buffer = new byte[size];
        try {
            bytesRead = inputStream.read(buffer, 0, size);
        } catch (IOException e) {
            DebugTool.logError(TAG, "Error attempting to read from input stream", e);
        }

        if (bytesRead > 0) {
            return buffer;
        } else {
            return null;
        }
    }

    /**
     * One of the responses returned by the SDL Core will contain the correct remaining free storage
     * size on the SDL Core. Since communication with the SDL Core is asynchronous, there is no way
     * to predict which response contains the correct bytes available other than to watch for the
     * largest correlation id, since that will be the last response sent by the SDL Core.
     *
     * @param correlationID                The correlationID for the newest response returned by the SDL Core for a PutFile
     * @param highestCorrelationIDReceived The largest currently received correlation id
     * @return Whether or not the newest request contains the highest correlationId
     */
    private boolean newHighestCorrelationID(int correlationID, int highestCorrelationIDReceived) {
        return correlationID > highestCorrelationIDReceived;
    }

    /**
     * Gets the size of the data.
     *
     * @return The size of the data.
     */
    private int getFileSizeFromInputStream(InputStream inputStream) {
        int size = 0;
        if (inputStream != null) {
            try {
                size = inputStream.available();
            } catch (IOException e) {
                DebugTool.logError(TAG, "Error trying to get input stream size", e);
            }
        }
        return size;
    }

    @Override
    public String getName() {
        return super.getName() + " - " + fileWrapper.getFile().getName();
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy