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

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

/*
 * Copyright (c) 2019 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 androidx.annotation.RestrictTo;

import com.livio.taskmaster.Queue;
import com.smartdevicelink.managers.BaseSubManager;
import com.smartdevicelink.managers.CompletionListener;
import com.smartdevicelink.managers.ISdl;
import com.smartdevicelink.managers.file.filetypes.SdlArtwork;
import com.smartdevicelink.managers.file.filetypes.SdlFile;
import com.smartdevicelink.proxy.rpc.enums.Result;
import com.smartdevicelink.util.DebugTool;
import com.smartdevicelink.util.Version;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * FileManager 
* Note: This class must be accessed through the SdlManager. Do not instantiate it by itself.
* The FileManager uploads files and keeps track of all the uploaded files names during a session.
*/ abstract class BaseFileManager extends BaseSubManager { final static String TAG = "FileManager"; final static int SPACE_AVAILABLE_MAX_VALUE = 2000000000; final Set mutableRemoteFileNames; private final Set uploadedEphemeralFileNames; private int bytesAvailable; private Queue transactionQueue; private HashMap failedFileUploadsCount; private final int maxFileUploadAttempts; private final int maxArtworkUploadAttempts; final String fileManagerCannotOverwriteError = "Cannot overwrite remote file. The remote file system already has a file of this name, and the file manager is set to not automatically overwrite files."; /** * Constructor for BaseFileManager * * @param internalInterface ISDL * @param fileManagerConfig FileManagerConfig */ BaseFileManager(ISdl internalInterface, FileManagerConfig fileManagerConfig) { super(internalInterface); this.bytesAvailable = 0; this.mutableRemoteFileNames = new HashSet<>(); this.transactionQueue = internalInterface.getTaskmaster().createQueue("FileManager", 5, false); this.uploadedEphemeralFileNames = new HashSet<>(); this.failedFileUploadsCount = new HashMap<>(); this.maxFileUploadAttempts = fileManagerConfig.getFileRetryCount() + 1; this.maxArtworkUploadAttempts = fileManagerConfig.getArtworkRetryCount() + 1; } @Override @RestrictTo(RestrictTo.Scope.LIBRARY) public void start(CompletionListener listener) { // Prepare manager - don't set state to ready until we have list of files retrieveRemoteFiles(); super.start(listener); } @Override public void dispose() { super.dispose(); // Cancel the operations if (transactionQueue != null) { transactionQueue.close(); transactionQueue = null; } if (mutableRemoteFileNames != null) { mutableRemoteFileNames.clear(); } bytesAvailable = 0; // Clear the failed uploads tracking so failed files can be uploaded again when a new connection has been established with Core if (failedFileUploadsCount != null) { failedFileUploadsCount.clear(); } } /** * Returns a list of file names currently residing on core * * @return List of remote file names */ public List getRemoteFileNames() { return new ArrayList<>(mutableRemoteFileNames); } /** * Get the number of bytes still available for files for this app. * * @return int value representing The number of bytes still available */ public int getBytesAvailable() { return this.bytesAvailable; } private void retrieveRemoteFiles() { listRemoteFilesWithCompletionListener(new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (errorMessage != null) { // HAX: In the case we are DISALLOWED we still want to transition to a ready state. // Some head units return DISALLOWED for this RPC but otherwise work. DebugTool.logWarning(TAG, "ListFiles is disallowed. Certain file manager APIs may not work properly."); transitionToState(READY); return; } // If no error, make sure we're in the ready state transitionToState(READY); } }); } private void listRemoteFilesWithCompletionListener(final FileManagerCompletionListener completionListener) { ListFilesOperation operation = new ListFilesOperation(internalInterface, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (errorMessage != null || !success) { completionListener.onComplete(success, bytesAvailable, fileNames, errorMessage); return; } // If there was no error, set our properties and call back to the completion listener BaseFileManager.this.mutableRemoteFileNames.addAll(fileNames); BaseFileManager.this.bytesAvailable = bytesAvailable; completionListener.onComplete(success, bytesAvailable, fileNames, errorMessage); } }); transactionQueue.add(operation, false); } /** * Attempts to delete the desired file from core, calls listener with indication of success/failure * * @param fileName name of file to be deleted * @param listener callback that is called on response from core */ public void deleteRemoteFileWithName(@NonNull final String fileName, final CompletionListener listener) { deleteRemoteFileWithNamePrivate(fileName, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (listener != null) { listener.onComplete(success); } } }); } private void deleteRemoteFileWithNamePrivate(@NonNull final String fileName, final FileManagerCompletionListener listener) { DeleteFileOperation operation = new DeleteFileOperation(internalInterface, fileName, mutableRemoteFileNames, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (success) { BaseFileManager.this.bytesAvailable = bytesAvailable; BaseFileManager.this.mutableRemoteFileNames.remove(fileName); } if (listener != null) { listener.onComplete(success, bytesAvailable, mutableRemoteFileNames, errorMessage); } } }); transactionQueue.add(operation, false); } /** * Attempts to delete a list of files from core, calls listener with indication of success/failure * * @param fileNames list of file names to be deleted * @param listener callback that is called once core responds to all deletion requests */ public void deleteRemoteFilesWithNames(@NonNull List fileNames, final MultipleFileCompletionListener listener) { if (fileNames.isEmpty()) { throw new IllegalArgumentException("This request requires that the array of files not be empty"); } final Map failedDeletes = new HashMap<>(); final DispatchGroup deleteFilesTask = new DispatchGroup(); deleteFilesTask.enter(); for (final String name : fileNames) { deleteFilesTask.enter(); deleteRemoteFileWithNamePrivate(name, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (!success) { failedDeletes.put(name, errorMessage); } deleteFilesTask.leave(); } }); } deleteFilesTask.leave(); // Wait for all files to be deleted deleteFilesTask.notify(new Runnable() { @Override public void run() { if (listener == null) { return; } if (failedDeletes.size() > 0) { listener.onComplete(failedDeletes); return; } listener.onComplete(null); } }); } /** * Check if an SdlFile has been uploaded to core * * @param file SdlFile * @return boolean that tells whether file has been uploaded to core (true) or not (false) */ public boolean hasUploadedFile(@NonNull SdlFile file) { // HAX: [#827](https://github.com/smartdevicelink/sdl_ios/issues/827) Older versions of Core had a bug where list files would cache incorrectly. Version rpcVersion = new Version(internalInterface.getSdlMsgVersion()); if (new Version(4, 4, 0).isNewerThan(rpcVersion) == 1) { if (file.isPersistent() && mutableRemoteFileNames != null && mutableRemoteFileNames.contains(file.getName())) { // HAX: If it's a persistent file, the bug won't present itself; just check if it's on the remote system return true; } else if (!file.isPersistent() && mutableRemoteFileNames != null && mutableRemoteFileNames.contains(file.getName()) && uploadedEphemeralFileNames.contains(file.getName())) { // HAX: If it's an ephemeral file, the bug will present itself; check that it's a remote file AND that we've uploaded it this session return true; } } else if (mutableRemoteFileNames != null && mutableRemoteFileNames.contains(file.getName())) { // If not connected to a system where the bug presents itself, we can trust the `remoteFileNames` return true; } return false; } /** * Check if an SdlFile needs to be uploaded to Core or not. * It is different from hasUploadedFile() because it takes isStaticIcon and overwrite properties into consideration. * ie, if the file is static icon, the method always returns false. * If the file is dynamic, it returns true in one of these situations: * 1) the file has the overwrite property set to true * 2) the file hasn't been uploaded to Core before. * * @param file the SdlFile that needs to be checked * @return boolean that tells whether file needs to be uploaded to Core or not */ public boolean fileNeedsUpload(@NonNull SdlFile file) { if (file != null && !file.isStaticIcon()) { return file.getOverwrite() || !hasUploadedFile(file); } return false; } /** * Attempts to upload a list of SdlFiles to core * * @param files list of SdlFiles with file name and one of A) fileData, B) Uri, or C) resourceID set * @param listener callback that is called once core responds to all upload requests */ public void uploadFiles(@NonNull List files, final MultipleFileCompletionListener listener) { if (files.isEmpty()) { throw new IllegalArgumentException("This request requires that the array of files not be empty."); } final Map failedUploads = new HashMap<>(); final DispatchGroup uploadFilesTask = new DispatchGroup(); uploadFilesTask.enter(); // Wait for all files to be uploaded uploadFilesTask.notify(new Runnable() { @Override public void run() { if (listener == null) { return; } if (failedUploads.size() > 0) { listener.onComplete(failedUploads); return; } listener.onComplete(null); } }); for (int i = 0; i < files.size(); i++) { final SdlFile file = files.get(i); uploadFilesTask.enter(); uploadFilePrivate(file, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (!success) { failedUploads.put(file.getName(), errorMessage); } uploadFilesTask.leave(); } }); } uploadFilesTask.leave(); } /** * Attempts to upload a SdlFile to core * * @param file SdlFile with file name and one of A) fileData, B) Uri, or C) resourceID set * @param listener called when core responds to the attempt to upload the file */ public void uploadFile(@NonNull final SdlFile file, final CompletionListener listener) { uploadFilePrivate(file, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (!success && errorMessage != null) { DebugTool.logWarning(TAG, errorMessage); } if (listener != null) { listener.onComplete(success); } } }); } private void uploadFilePrivate(@NonNull final SdlFile file, final FileManagerCompletionListener listener) { if (file == null) { if (listener != null) { listener.onComplete(false, bytesAvailable, null, "The file upload was canceled. The data for the file is missing."); } return; } if (file.getName() == null) { if (listener != null) { listener.onComplete(false, bytesAvailable, null, "You must specify an file name in the SdlFile."); } return; } if (file.isStaticIcon()) { if (listener != null) { listener.onComplete(false, bytesAvailable, null, "The file upload was canceled. The file is a static icon, which cannot be uploaded."); } return; } if (getState() != READY) { if (listener != null) { listener.onComplete(false, bytesAvailable, null, "The file manager was unable to send this file. This could be because the file manager has not started, or the head unit does not support files."); } return; } // If we didn't error out over the overwrite, then continue on sdl_uploadFilePrivate(file, listener); } private void sdl_uploadFilePrivate(@NonNull final SdlFile file, final FileManagerCompletionListener listener) { final SdlFile fileClone = file.clone(); SdlFileWrapper fileWrapper = new SdlFileWrapper(fileClone, new FileManagerCompletionListener() { @Override public void onComplete(boolean success, int bytesAvailable, Collection fileNames, String errorMessage) { if (success) { BaseFileManager.this.bytesAvailable = bytesAvailable; BaseFileManager.this.mutableRemoteFileNames.add(fileClone.getName()); if (!file.isPersistent()) { BaseFileManager.this.uploadedEphemeralFileNames.add(fileClone.getName()); } } else { incrementFailedUploadCountForFileName(fileClone.getName(), BaseFileManager.this.failedFileUploadsCount); int maxUploadCount = fileClone instanceof SdlArtwork ? maxArtworkUploadAttempts : maxFileUploadAttempts; if (canFileBeUploadedAgain(fileClone, maxUploadCount, failedFileUploadsCount)) { DebugTool.logInfo(TAG, String.format("Attempting to resend file with name %s after a failed upload attempt", file.getName())); sdl_uploadFilePrivate(fileClone, listener); return; } } if (listener != null) { listener.onComplete(success, bytesAvailable, null, errorMessage); } } }); UploadFileOperation operation = new UploadFileOperation(internalInterface, this, fileWrapper); transactionQueue.add(operation, false); } /** * Attempts to upload a SdlArtwork to core * * @param file SdlArtwork with file name and one of A) fileData, B) Uri, or C) resourceID set * @param listener called when core responds to the attempt to upload the file */ public void uploadArtwork(final SdlArtwork file, final CompletionListener listener) { uploadFile(file, listener); } /** * Attempts to upload a list of SdlArtworks to core * * @param files list of SdlArtworks with file name and one of A) fileData, B) Uri, or C) resourceID set * @param listener callback that is called once core responds to all upload requests */ public void uploadArtworks(List files, final MultipleFileCompletionListener listener) { uploadFiles(files, listener); } /** * Checks if an artwork needs to be uploaded to Core. The artwork should not be sent to Core if * the artwork is already on Core or if the artwork is not on Core after the maximum number of * repeated upload attempts has been reached. * * @param file The file to be uploaded to Core * @param maxUploadCount The max number of times the file is allowed to be uploaded to Core * @param failedFileUploadsCount * @return True if the file still needs to be (re)sent to Core; false if not. */ private boolean canFileBeUploadedAgain(SdlFile file, int maxUploadCount, HashMap failedFileUploadsCount) { if (getState() != READY) { DebugTool.logWarning(TAG, String.format("File named %s failed to upload. The file manager has shutdown so the file upload will not retry.", file.getName())); return false; } if (file == null) { DebugTool.logError(TAG, "File can not be uploaded because it is not a valid file."); return false; } if (hasUploadedFile(file)) { DebugTool.logInfo(TAG, String.format("File named %s has already been uploaded.", file.getName())); return false; } Integer failedUploadCount = failedFileUploadsCount.get(file.getName()); boolean canFileBeUploadedAgain = (failedUploadCount == null) || (failedUploadCount < maxUploadCount); if (!canFileBeUploadedAgain) { DebugTool.logError(TAG, String.format("File named %s failed to upload. Max number of upload attempts reached.", file.getName())); } return canFileBeUploadedAgain; } /** * Increments the number of upload attempts for a file name by 1. * * @param name The name used to upload the file to Core * @param failedFileUploadsCount * @return */ private void incrementFailedUploadCountForFileName(String name, HashMap failedFileUploadsCount) { Integer currentFailedUploadCount = failedFileUploadsCount.get(name); Integer newFailedUploadCount = (currentFailedUploadCount != null) ? (currentFailedUploadCount + 1) : 1; failedFileUploadsCount.put(name, newFailedUploadCount); DebugTool.logWarning(TAG, String.format("File with name %s failed to upload %s times", name, newFailedUploadCount)); } /** * Opens a socket for reading data. * * @param file The file containing the data or the file url of the data */ abstract InputStream openInputStreamWithFile(@NonNull SdlFile file); /** * Builds an error string for a given Result and info string * * @param resultCode Result * @param info String returned from OnRPCRequestListener.onError() * @return Error string */ @Deprecated static public String buildErrorString(Result resultCode, String info) { return resultCode.toString() + " : " + info; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy