Short version:
This little bit of code never has a call back. I've looked around in places like msdn and I couldn't find a solution. It seems to me like BackgroundTransfer's BackgroundUploader's createUploadAsync's startAsync starts but then doesn't do anything. It doesn't have an error or a success callback.
And "rarely" it actually works a few times before it stops working again. Giving the device a hard reset didn't seem to solve this either. The network connection is over a local lan. Connection to server is stable. I'm running a copy of the API on a local machine in visual studio in which requests successfully work.
I've attempted to regress down to cordova 5.1.1 and cordova-plugin-file-transfer 1.3.0 and 1.2.0 to see if it had anything to do with the version I was using.
The server I am connecting with works with any other device. Notably, this entire functionality seems to work in the windows emulator which leaves me to think this may be a specific device issue?
I may have cut myself attempting to go bleeding edge for windows phone 8.1
var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader();
uploader.createUploadAsync(uri, transferParts).then(function(result) {
var uploadOperation = upload.startAsync();
fileTransferOps[uploadId].promise = uploadOperation;
uploadOperation.then(successFunc, errorFunc);
}, errorCallBack);
Edit: I have set a timeout to this operation so that it aborts after 25 seconds, which it does. On all other (iOS/Android 4.2/5.x) devices this is instant. It isn't that Windows Phone is being slower for some reason, because the request is there for longer than 60 minutes. Maybe BackgroundUploader() silently fails... will look into this.
Edit2: Filed a bug report here: https://issues.apache.org/jira/browse/CB-10254 The other things I have attempted to help solve this issue is by disabling synchronization of time and date, restarted the device and it worked like a charm.... for a moment. Further debugging suggests that all my requests were either canceled or pending.
Below is the boring stuf for those interested in helping me dive into this.
Example of code:
$scope.album = {
uploadPhotos: function (photos) {
var promises = [];
for (var i = 0; i < photos.length; i++) {
// Windows phone doesn't work so we have to add a dirty workaround, as usual.
if (ionic.Platform.isWindowsPhone()) {
photos[i] = "cdvfile://localhost/persistent/" + decodeURIComponent(photos[i].split("/").pop());
}
var scannInvoice = parseToBool($localstorage.get("remindInvoiceScan")) ? 1 : 0;
promises.push($cordovaFileTransfer.upload("***HTTP Development/HTTPS Production***", photos[i], {
fileKey: "file",
fileName: photos[i].split("/").pop(),
httpMethod: "POST",
timeout: 25000,
chunkedMode: false,
contentType: "application/json; charset=utf-8",
// mimeType: "image/png",
params: { 'scannInvoice': scannInvoice },
headers: { 'Authorization': 'Token ' + $settings.accesstoken }
}).then(function (result) {
$fileService.setFotoUploaded(result.response);
}, function (err) {
$logService.handleException("PhotoCtrl", "Afbeelding is niet geupload", "Er is een onbekende fout opgetreden.", err, true, false, true, true);
}));
} // Some $q.all call comes here that handles everything.
}
}
In FileTransfer.js
FileTransfer.prototype.upload = function(filePath, server, successCallback, errorCallback, options, trustAllHosts) {
argscheck.checkArgs('ssFFO*', 'FileTransfer.upload', arguments);
// check for options
var fileKey = null;
var fileName = null;
var mimeType = null;
var params = null;
var chunkedMode = true;
var headers = null;
var httpMethod = null;
var basicAuthHeader = getBasicAuthHeader(server);
if (basicAuthHeader) {
server = server.replace(getUrlCredentials(server) + '@', '');
options = options || {};
options.headers = options.headers || {};
options.headers[basicAuthHeader.name] = basicAuthHeader.value;
}
if (options) {
fileKey = options.fileKey;
fileName = options.fileName;
mimeType = options.mimeType;
headers = options.headers;
httpMethod = options.httpMethod || "POST";
if (httpMethod.toUpperCase() == "PUT"){
httpMethod = "PUT";
} else {
httpMethod = "POST";
}
if (options.chunkedMode !== null || typeof options.chunkedMode != "undefined") {
chunkedMode = options.chunkedMode;
}
if (options.params) {
params = options.params;
}
else {
params = {};
}
}
if (cordova.platformId === "windowsphone") {
headers = headers && convertHeadersToArray(headers);
params = params && convertHeadersToArray(params);
}
var fail = errorCallback && function(e) {
var error = new FileTransferError(e.code, e.source, e.target, e.http_status, e.body, e.exception);
errorCallback(error);
};
var self = this;
var win = function(result) {
if (typeof result.lengthComputable != "undefined") {
if (self.onprogress) {
self.onprogress(newProgressEvent(result));
}
} else {
successCallback && successCallback(result);
}
};
exec(win, fail, 'FileTransfer', 'upload', [filePath, server, fileKey, fileName, mimeType, params, trustAllHosts, chunkedMode, headers, this._id, httpMethod]);
};
And in FileTransferProxy.js
module.exports = {
/*
exec(win, fail, 'FileTransfer', 'upload',
[filePath, server, fileKey, fileName, mimeType, params, trustAllHosts, chunkedMode, headers, this._id, httpMethod]);
*/
upload:function(successCallback, errorCallback, options) {
var filePath = options[0];
var server = options[1];
var fileKey = options[2] || 'source';
var fileName = options[3];
var mimeType = options[4];
var params = options[5];
// var trustAllHosts = options[6]; // todo
// var chunkedMode = options[7]; // todo
var headers = options[8] || {};
var uploadId = options[9];
if (!filePath || (typeof filePath !== 'string')) {
errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR,null,server));
return;
}
if (filePath.substr(0, 8) === "file:///") {
filePath = appData.localFolder.path + filePath.substr(8).split("/").join("\\");
} else if (filePath.indexOf('ms-appdata:///') === 0) {
// Handle 'ms-appdata' scheme
filePath = filePath.replace('ms-appdata:///local', appData.localFolder.path)
.replace('ms-appdata:///temp', appData.temporaryFolder.path);
}
// normalize path separators
filePath = cordovaPathToNative(filePath);
// Create internal download operation object
fileTransferOps[uploadId] = new FileTransferOperation(FileTransferOperation.PENDING, null);
Windows.Storage.StorageFile.getFileFromPathAsync(filePath)
.then(function (storageFile) {
if(!fileName) {
fileName = storageFile.name;
}
if(!mimeType) {
// use the actual content type of the file, probably this should be the default way.
// other platforms probably can't look this up.
mimeType = storageFile.contentType;
}
// check if download isn't already cancelled
var uploadOp = fileTransferOps[uploadId];
if (uploadOp && uploadOp.state === FileTransferOperation.CANCELLED) {
// Here we should call errorCB with ABORT_ERR error
errorCallback(new FTErr(FTErr.ABORT_ERR, nativePathToCordova(filePath), server));
return;
}
// setting request headers for uploader
var uploader = new Windows.Networking.BackgroundTransfer.BackgroundUploader();
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
uploader.setRequestHeader(header, headers[header]);
}
}
// adding params supplied to request payload
var transferParts = [];
for (var key in params) {
if (params.hasOwnProperty(key)) {
var contentPart = new Windows.Networking.BackgroundTransfer.BackgroundTransferContentPart();
contentPart.setHeader("Content-Disposition", "form-data; name=\"" + key + "\"");
contentPart.setText(params[key]);
transferParts.push(contentPart);
}
}
// Adding file to upload to request payload
var fileToUploadPart = new Windows.Networking.BackgroundTransfer.BackgroundTransferContentPart(fileKey, fileName);
fileToUploadPart.setFile(storageFile);
transferParts.push(fileToUploadPart);
// create download object. This will throw an exception if URL is malformed
var uri = new Windows.Foundation.Uri(server);
try {
uploader.createUploadAsync(uri, transferParts).then(
function (upload) {
// update internal TransferOperation object with newly created promise
var uploadOperation = upload.startAsync();
fileTransferOps[uploadId].promise = uploadOperation;
uploadOperation.then(
function (result) {
// Update TransferOperation object with new state, delete promise property
// since it is not actual anymore
var currentUploadOp = fileTransferOps[uploadId];
if (currentUploadOp) {
currentUploadOp.state = FileTransferOperation.DONE;
currentUploadOp.promise = null;
}
var response = result.getResponseInformation();
var ftResult = new FileUploadResult(result.progress.bytesSent, response.statusCode, '');
// if server's response doesn't contain any data, then resolve operation now
if (result.progress.bytesReceived === 0) {
successCallback(ftResult);
return;
}
// otherwise create a data reader, attached to response stream to get server's response
var reader = new Windows.Storage.Streams.DataReader(result.getResultStreamAt(0));
reader.loadAsync(result.progress.bytesReceived).then(function (size) {
ftResult.response = reader.readString(size);
successCallback(ftResult);
reader.close();
});
},
function (error) {
var source = nativePathToCordova(filePath);
// Handle download error here.
// Wrap this routines into promise due to some async methods
var getTransferError = new WinJS.Promise(function(resolve) {
if (error.message === 'Canceled') {
// If download was cancelled, message property will be specified
resolve(new FTErr(FTErr.ABORT_ERR, source, server, null, null, error));
} else {
// in the other way, try to get response property
var response = upload.getResponseInformation();
if (!response) {
resolve(new FTErr(FTErr.CONNECTION_ERR, source, server));
} else {
var reader = new Windows.Storage.Streams.DataReader(upload.getResultStreamAt(0));
reader.loadAsync(upload.progress.bytesReceived).then(function (size) {
var responseText = reader.readString(size);
resolve(new FTErr(FTErr.FILE_NOT_FOUND_ERR, source, server, response.statusCode, responseText, error));
reader.close();
});
}
}
});
// Update TransferOperation object with new state, delete promise property
// since it is not actual anymore
var currentUploadOp = fileTransferOps[uploadId];
if (currentUploadOp) {
currentUploadOp.state = FileTransferOperation.CANCELLED;
currentUploadOp.promise = null;
}
// Cleanup, remove incompleted file
getTransferError.then(function(transferError) {
storageFile.deleteAsync().then(function() {
errorCallback(transferError);
});
});
},
function (evt) {
var progressEvent = new ProgressEvent('progress', {
loaded: evt.progress.bytesSent,
total: evt.progress.totalBytesToSend,
target: evt.resultFile
});
progressEvent.lengthComputable = true;
successCallback(progressEvent, { keepCallback: true });
}
);
},
function (err) {
var errorObj = new FTErr(FTErr.INVALID_URL_ERR);
errorObj.exception = err;
errorCallback(errorObj);
}
);
} catch (e) {
errorCallback(new FTErr(FTErr.INVALID_URL_ERR));
}
}, function(err) {
errorCallback(new FTErr(FTErr.FILE_NOT_FOUND_ERR, server, server, null, null, err));
});
},
};
My setup information:
Windows Phone 8.1, Nokia Lumia 520
In my taco.json
{
"cordova-cli": "5.4.1"
}
Using windows platform 4.2.0
The currently added plugins
<vs:plugin name="cordova-plugin-device" version="1.1.0" />
<vs:plugin name="cordova-plugin-whitelist" version="1.2.0" />
<vs:plugin name="cordova-plugin-file" version="3.0.0" />
<vs:plugin name="cordova-plugin-network-information" version="1.1.0" />
<vs:plugin name="cordova-plugin-inappbrowser" version="1.1.1" />
<vs:plugin name="cordova-plugin-statusbar" version="2.0.0" />
<vs:plugin name="cordova-plugin-camera" version="2.0.0" src="https://github.com/apache/cordova-plugin-camera" />
<vs:plugin name="cordova-plugin-file-transfer" version="1.4.0" />
And finally in my index.html
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
<meta http-equiv="Content-Security-Policy" content="default-src * 'self' cdvfile://*; data: gap: https://ssl.gstatic.com; style-src 'unsafe-inline' 'self' cdvfile://*; script-src 'self' 'unsafe-eval' cdvfile://*">
<meta name="format-detection" content="telephone=no">
<title></title>
<!-- stylesheets -->
<link href="css/ionic.min.css" rel="stylesheet" />
<link href="css/ionic-overrides.css" rel="stylesheet" />
<link href="css/angular-chart.css" rel="stylesheet" />
<link href="css/isteven-omni-bar.css" rel="stylesheet" />
<!-- library -->
<script src="cordova.js"></script>
<script src="scripts/platformOverrides.js"></script>
<script src="scripts/ionic.bundle.custom.js"></script>
<script src="scripts/ng-cordova.js"></script>
<!-- application -->
<script src="js/app.js"></script>
<script src="js/services/localstorage.js"></script>
<script src="js/services/settings.js"></script>
<!-- config -->
<script src="js/config/ionicPlatform.js"></script>
<script src="js/config/compileProvider.js"></script>
<script src="js/config/stateProvider.js"></script>
<script src="js/config/formatFilters.js"></script>
<script src="js/config/directives.js"></script>
<!-- controllers -->
<script src="js/controllers/LoginController.js"></script>
<script src="js/controllers/AppController.js"></script>
<script src="js/controllers/DashboardController.js"></script>
<script src="js/controllers/PhotoController.js"></script>
<script src="js/controllers/AdministrationController.js"></script>
<script src="js/controllers/MutationController.js"></script>
<!-- services -->
<script src="js/services/administrationService.js"></script>
<script src="js/services/fileService.js"></script>
<script src="js/services/logService.js"></script>
<!-- scripts -->
<script src="scripts/helpers.js"></script>
<script src="scripts/chart/highcharts.js"></script>
<script src="scripts/chart/highcharts-ng.js"></script>
<script src="scripts/isteven-omni-bar.js"></script>
</head>
After this problem occurs, I managed to solve this problem consistently by going through the following steps.