Commit eb82436d authored by Martin Straka's avatar Martin Straka

Added support for file loading/sending from iCloud

parent ca609eb3
......@@ -11,7 +11,8 @@ HEADERS += \
ios/src/app_delegate.h \
ios/src/doc_view_controller.h \
ios/src/qt_app_delegate.h \
ios/src/icloud_helper.h \
ios/src/icloud_controller.h \
ios/src/icloud_io.h \
ios/src/ios_file_opener.h \
ios/src/send_email_controller.h \
ios/src/url_opener.h
......@@ -19,7 +20,8 @@ HEADERS += \
OBJECTIVE_SOURCES += \
ios/src/app_delegate.mm \
ios/src/doc_view_controller.mm \
ios/src/icloud_helper.mm \
ios/src/icloud_controller.mm \
ios/src/icloud_io.mm \
ios/src/ios_file_opener.mm \
ios/src/send_email_controller.mm \
ios/src/url_opener.mm
......@@ -33,4 +35,3 @@ QMAKE_BUNDLE_DATA += ios_icon
app_launch_images.files = $$files($${_PRO_FILE_PWD_}/ios/Images.xcassets/LaunchImage.launchimage/*.png)
QMAKE_BUNDLE_DATA += app_launch_images
LIBS += -framework UIKit -framework MessageUI -framework MobileCoreServices
/*
* Copyright (C) 2014-2018 CZ.NIC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations including
* the two.
*/
#import <UIKit/UIKit.h>
@interface ICloudViewController : UIViewController
- (void)getCloudHierarchyAsync;
@property (strong) NSMetadataQuery *query;
@end
/*
* Copyright (C) 2014-2018 CZ.NIC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations including
* the two.
*/
#include <QStringList>
#import "ios/src/icloud_controller.h"
#include "src/auxiliaries/icloud_helper.h"
QStringList iCloudFileList;
bool isSearchRunning;
@implementation ICloudViewController
- (void)replyDataNotification:(NSNotification *)notification {
NSMetadataQuery *query = [notification object];
[query disableUpdates];
[query stopQuery];
QStringList files;
for (NSMetadataItem *item in [query results]) {
NSURL *documentURL = [item valueForAttribute:NSMetadataItemURLKey];
files.append(QString::fromNSString(documentURL.absoluteString));
//NSLog(@"iCloud:\n %@\n", documentURL);
}
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:self.query];
self.query = nil;
iCloudFileList = files;
isSearchRunning = false;
}
- (void)getCloudHierarchyAsync {
isSearchRunning = true;
self.query = [[NSMetadataQuery alloc] init];
//[self.query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDataScope]];
[self.query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like '*'", NSMetadataItemFSNameKey];
[self.query setPredicate:predicate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(replyDataNotification:) name:NSMetadataQueryDidFinishGatheringNotification object:self.query];
[self.query startQuery];
}
@end
/*
* Copyright (C) 2014-2018 CZ.NIC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations including
* the two.
*/
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
/*!
* @brief Provides objective-C IO methods for interaction with iCLoud.
*/
class ICloudIo : public QObject {
Q_OBJECT
public:
/*!
* @brief Return state describing what happened.
*/
enum ICloudResult {
ICLOUD_NOT_ON = 0, /*!< iCloud is not avalilable/turn on. */
ICLOUD_FILE_UPLOAD_SUCCESS, /*!< File upload was successful. */
ICLOUD_FILE_EXISTS, /*!< File exists on iCloud. */
ICLOUD_FILE_UPLOAD_ERROR, /*!< File upload failed. */
ICLOUD_TARGET_SAVE_DIR_ERROR /*!< Target dir is missing or not created. */
};
/*!
* @brief Constructor.
*
* @param[in] parent Parent object.
*/
ICloudIo(QObject *parent = Q_NULLPTR);
/*!
* @brief Test if iCloud is on.
*
* @return Return true if iCloud is on.
*/
static
bool isCloudOn(void);
/*!
* @brief Create Datovka iCloud conteiner if not exists.
*/
static
void createCloudConteiner(void);
/*!
* @brief Copy single file from iCloud continer to Datovka sandbox.
*
* @param[in] cloudFilePath Source file path.
* @param[in] localFilePath iCloud target path.
* @return Full path where file will stored.
*/
static
QString copyFileFromCloud(const QString &cloudFilePath,
const QString &localFilePath);
/*!
* @brief Search for local iCloud hierarchy - not used now.
*/
static
QStringList getCloudHierarchy(const QString &dir);
/*!
* @brief Create and send async search query for iCloud hierarchy.
*/
static
void getCloudHierarchyAsync(void);
/*!
* @brief Upload single file into iCloud.
*
* @param[in] srcFilePath Source file path.
* @param[in] destFilePath iCloud target path.
* @return Return operation error/success code.
*/
static
ICloudResult moveFileToCloud(const QString &srcFilePath,
const QString &destFilePath);
/*!
* @brief Test if file exists in local iCloud conteiner.
*
* @param[in] cloudFilePath iCloud file path.
* @return True if file exists in local iCloud conteiner.
*/
static
bool isDownloadedFromCloud(const QString &cloudFilePath);
};
/*
* Copyright (C) 2014-2018 CZ.NIC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations including
* the two.
*/
#include "ios/src/icloud_controller.h"
#include "ios/src/icloud_io.h"
ICloudIo::ICloudIo(QObject *parent)
: QObject(parent)
{
}
NSURL * getCloudBaseUrl() {
return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
}
NSURL * getCloudDocumentsUrl(NSURL *baseURL) {
return [baseURL URLByAppendingPathComponent:@"Documents"];
}
bool ICloudIo::isCloudOn(void) {
return [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
}
void ICloudIo::createCloudConteiner(void)
{
NSURL *baseURL = getCloudBaseUrl();
if (!baseURL) {
NSLog(@"iCloud: Unable to access iCloud.");
return;
}
// Create iCloud send message dir
NSURL *sendDirURL = [getCloudDocumentsUrl(baseURL) URLByAppendingPathComponent:@"Send"];
// Create send message subdirectorues for iCloud
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:sendDirURL
withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"iCloud: Create message subdirectories error: %@", error);
return;
}
// Upload send dir into iCloud
if ([[NSFileManager defaultManager] setUbiquitous:YES
itemAtURL:sendDirURL destinationURL:sendDirURL error:&error]) {
NSLog(@"iCloud: File %@ has been uploaded.", sendDirURL);
}
}
/* TODO - removed this tmp function */
QStringList ICloudIo::getCloudHierarchy(const QString &dir)
{
QStringList fileList;
NSURL *baseURL = getCloudBaseUrl();
if (!baseURL) {
NSLog(@"iCloud: Unable to access iCloud.");
return QStringList();
}
// Create iCloud target path
NSURL *documentURL = getCloudDocumentsUrl(baseURL);
NSURL *fileURL;
if (dir.isEmpty()) {
fileURL = documentURL;
} else {
fileURL = [documentURL URLByAppendingPathComponent:dir.toNSString()];
}
NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:fileURL includingPropertiesForKeys:nil options:0 error:nil];
for (NSUInteger i = 0; i < [directoryContent count]; i++) {
NSLog(@"iCloud:\n %@\n", directoryContent[i]);
NSURL *fileUrlx = directoryContent[i];
fileList.append(QString::fromNSString(fileUrlx.absoluteString));
}
return fileList;
}
void ICloudIo::getCloudHierarchyAsync(void)
{
static ICloudViewController* iCloudCntlr = nil;
iCloudCntlr = [[ICloudViewController alloc] init];
[iCloudCntlr getCloudHierarchyAsync];
}
ICloudIo::ICloudResult ICloudIo::moveFileToCloud(
const QString &srcFilePath, const QString &destFilePath)
{
NSURL *baseURL = getCloudBaseUrl();
if (!baseURL) {
return ICLOUD_NOT_ON;
}
// Create iCloud target path
NSURL *messageURL = [getCloudDocumentsUrl(baseURL) URLByAppendingPathComponent:destFilePath.toNSString()];
// Create subdirectorues on iCloud
NSError *error = nil;
if (![[NSFileManager defaultManager] createDirectoryAtURL:messageURL
withIntermediateDirectories:YES attributes:nil error:&error]) {
return ICLOUD_TARGET_SAVE_DIR_ERROR;
}
// Create target upload path for iCloud
NSURL *sourceFileUrl = [NSURL fileURLWithPath:srcFilePath.toNSString()];
NSString *fileName = [srcFilePath.toNSString() lastPathComponent];
NSURL *fileURL = [messageURL URLByAppendingPathComponent:fileName];
// Upload file to target directory on iCloud
if ([[NSFileManager defaultManager] setUbiquitous:YES
itemAtURL:sourceFileUrl destinationURL:fileURL error:&error]) {
return ICLOUD_FILE_UPLOAD_SUCCESS;
} else {
// Code 516 = file exists in the iCloud. See NSFileManager error codes.
if (error.code == NSFileWriteFileExistsError) {
return ICLOUD_FILE_EXISTS;
} else {
NSLog(@"iCloud: Error code: %zd", error.code);
NSLog(@"iCloud: %@", error);
return ICLOUD_FILE_UPLOAD_ERROR;
}
}
}
QString ICloudIo::copyFileFromCloud(const QString &cloudFilePath,
const QString &localFilePath)
{
if (!isDownloadedFromCloud(cloudFilePath)) {
NSLog(@"Local: File has not been downloaded yet from iCloud.");
return QString();
}
NSError *error = nil;
NSURL *fileCloudUrl = [NSURL URLWithString:cloudFilePath.toNSString()];
NSURL* localFileUrl = [NSURL fileURLWithPath:localFilePath.toNSString()];
NSString *fileName = [fileCloudUrl lastPathComponent];
// Create subdirectorues on Local
if (![[NSFileManager defaultManager] createDirectoryAtURL:localFileUrl
withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLog(@"Local: Create message subdirectories error: %@", error.localizedDescription);
return QString();
}
NSURL *fileLocalUrl = [localFileUrl URLByAppendingPathComponent:fileName];
NSLog(@"iCloud url: %@", fileCloudUrl);
NSLog(@"Local file url: %@", fileLocalUrl);
if ([[NSFileManager defaultManager] copyItemAtURL:fileCloudUrl toURL:fileLocalUrl error:&error]) {
NSLog(@"Local: File has been uploaded.");
return QString();
} else {
if (error.code == NSFileWriteFileExistsError) {
NSLog(@"Local: File with the same name already exists.");
return QString();
} else if (error.code == NSFileReadNoSuchFileError) {
NSLog(@"Local: File has not been downloaded yet from iCloud.");
return QString();
} else {
NSLog(@"Local: Error code: %zd %@", error.code, error.localizedDescription);
}
}
return QString();
}
bool ICloudIo::isDownloadedFromCloud(const QString &cloudFilePath)
{
NSURL *baseURL = getCloudBaseUrl();
if (!baseURL) {
NSLog(@"iCloud: Unable to access iCloud.");
return false;
}
NSError *error = nil;
NSString *downloadStatus = nil;
NSURL *fileCloudUrl = [NSURL URLWithString:cloudFilePath.toNSString()];
if ([fileCloudUrl getResourceValue:&downloadStatus forKey:NSURLUbiquitousItemDownloadingStatusKey error:&error] == YES) {
if ([downloadStatus isEqualToString:NSURLUbiquitousItemDownloadingStatusNotDownloaded] == YES) {
NSLog(@"File must be downloaded: %@", fileCloudUrl);
[[NSFileManager defaultManager] startDownloadingUbiquitousItemAtURL:fileCloudUrl error:&error];
return false;
} else {
NSLog(@"File has been downloaded: %@", fileCloudUrl);
}
}
return true;
}
......@@ -90,6 +90,7 @@ TRANSLATIONS_FILES += \
SOURCES += \
src/accounts.cpp \
src/auxiliaries/email_helper.cpp \
src/auxiliaries/icloud_helper.cpp \
src/datovka_shared/gov_services/helper.cpp \
src/datovka_shared/gov_services/service/gov_mv_crr_vbh.cpp \
src/datovka_shared/gov_services/service/gov_mv_ir_vp.cpp \
......@@ -153,6 +154,7 @@ SOURCES += \
src/main.cpp \
src/messages.cpp \
src/models/accountmodel.cpp \
src/models/cloudmodel.cpp \
src/models/databoxmodel.cpp \
src/models/filemodel.cpp \
src/models/list_sort_filter_proxy_model.cpp \
......@@ -211,6 +213,7 @@ SOURCES += \
HEADERS += \
src/accounts.h \
src/auxiliaries/email_helper.h \
src/auxiliaries/icloud_helper.h \
src/common.h \
src/datovka_shared/gov_services/helper.h \
src/datovka_shared/gov_services/service/gov_mv_crr_vbh.h \
......@@ -277,6 +280,7 @@ HEADERS += \
src/log.h \
src/messages.h \
src/models/accountmodel.h \
src/models/cloudmodel.h \
src/models/databoxmodel.h \
src/models/filemodel.h \
src/models/list_sort_filter_proxy_model.h \
......
/*
* Copyright (C) 2014-2018 CZ.NIC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, as a special exception, the copyright holders give
* permission to link the code of portions of this program with the
* OpenSSL library under certain conditions as described in each
* individual source file, and distribute linked combinations including
* the two.
*/
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import cz.nic.mobileDatovka 1.0
import cz.nic.mobileDatovka.qmlInteraction 1.0
import cz.nic.mobileDatovka.models 1.0
Dialog {
id: root
focus: true
modal: true
title: qsTr("Select files")
footer: DialogButtonBox {
AccessibleButton {
text: qsTr("Cancel")
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
}
AccessibleButton {
text: qsTr("Add")
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
}
}
/* Place the dialogue in the centre. */
x: 2 * defaultMargin
y: 2 * defaultMargin
height: parent.height - 4 * defaultMargin
width: parent.width - 4 * defaultMargin
signal finished(variant pathListModel)
function raise(title, filters, showFiles, targetLocation) {
cloudFileListModel.clearAll()
pathListModel.clear()
filterBar.filterField.text = ""
iCloudHelper.getCloudHierarchyAsync()
root.open()
}
/* Holds cloud file list model */
CloudFileListModel {
id: cloudFileListModel
Component.onCompleted: {
}
}
ListSortFilterProxyModel {
id: proxyCloudFileListModel
Component.onCompleted: {
setFilterRoles([CloudFileListModel.ROLE_FILE_NAME,
CloudFileListModel.ROLE_FILE_SHORT_PATH])
proxyCloudFileListModel.setSourceModel(cloudFileListModel)
}
}
ListModel {
id: pathListModel
}
contentItem: ColumnLayout {
spacing: formItemVerticalSpacing
Connections {
target: iCloudHelper
onCloudContentSig: {
iCloudHelper.setCloudFileModel(cloudFileListModel, iCloudFileList);
if (cloudFileListModel.rowCount() > 1) {
cloudFileList.visible = true
emptyList.visible = false
}
}
onCloudActivitySig: {
emptyList.text = txt
cloudFileList.visible = false
emptyList.visible = true
}
}
Component {
id: attachmentDelegate
Rectangle {
id: attachmentItem
width: parent.width
height: cloudFileList.delegateHeight
color: rFileSelected ? datovkaPalette.midlight : datovkaPalette.base
Image {
id: imageAttachment
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
sourceSize.height: cloudFileList.delegateHeight * 0.8
source: files.getAttachmentFileIcon(rFileName)
}
Item {
anchors.left: imageAttachment.right
anchors.leftMargin: defaultMargin
height: parent.height
width: parent.width
Column {
width: parent.width
anchors.verticalCenter: parent.verticalCenter
spacing: defaultMargin
Text {
text: rFileName
color: datovkaPalette.text
font.bold: true
}
Text {
text: rFileShortPath
color: datovkaPalette.mid
font.pointSize: textFontSizeSmall
}
}
}
Rectangle {
anchors.top: parent.bottom
height: 1
width: parent.width
color: datovkaPalette.dark
}
MouseArea {
anchors.fill: parent
onClicked: {
cloudFileListModel.setFileSelected(rFileCloudPath, !rFileSelected)
}
}
} // Rectangle
} // Component
FilterBar {
id: filterBar
visible: true
Layout.fillWidth: true
height: filterField.height
color: (filterField.text.length === 0) ? datovkaPalette.alternateBase :
(cloudFileList.count > 0) ? "#afffaf" : "#ffafaf"
border.color: filterField.activeFocus ? "#0066ff" : "#bdbebf"
placeholderText: qsTr("Set filter")
fontPointSize: defaultTextFont.font.pointSize
buttonImageHeight: imgHeight
buttonAccessibleName: qsTr("Clear and hide filter field")
onTextChanged: {
proxyCloudFileListModel.setFilterRegExpStr(text)
}
onClearClicked: {
filterField.text.clear
}
}
AccessibleText {
id: emptyList
visible: true
color: datovkaPalette.text
Layout.fillHeight: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
text: qsTr("No files in the iCloud.")
}
ScrollableListView {
id: cloudFileList
visible: false
delegateHeight: headerHeight
Layout.fillHeight: true
Layout.fillWidth: true
clip: true
spacing: 1
opacity: 1
interactive: true
model: proxyCloudFileListModel
delegate: attachmentDelegate
ScrollIndicator.vertical: ScrollIndicator {}
} // Listview
} // ColumnLayout
onAccepted: {
for (var i = 0; i < cloudFileListModel.rowCount(); ++i) {
if (cloudFileListModel.fileSelected(i)) {
pathListModel.append({path: cloudFileListModel.fileCloudPathFromRow(i)})
}
}
finished(pathListModel)
}
onRejected: {
pathListModel.clear()
}
}
......@@ -216,6 +216,32 @@ Item {
}
}
/* File dialog for choose of files from iCloud */
FileDialogueIos {
id: fileDialogueIos
onFinished: {
var listLength = pathListModel.count
for (var j = 0; j < listLength; ++j) {
var localFilePath = iCloudHelper.downloadFileFromCloud(pathListModel.get(j).path)
var isInFiletList = false
for (var i = 0; i < sendMsgAttachmentModel.rowCount(); i++) {
if (sendMsgAttachmentModel.filePathFromRow(i) === localFilePath) {
isInFiletList = true
break
}
}
if (!isInFiletList) {
var fileName = getFileNameFromPath(localFilePath)
var fileSizeBytes = files.getAttachmentSizeInBytes(localFilePath)
sendMsgAttachmentModel.appendFileFromPath(FileIdType.NO_FILE_ID,
fileName, localFilePath, fileSizeBytes)
totalAttachmentSizeBytes = sendMsgAttachmentModel.dataSizeSum()
}
}
pathListModel.clear()
}
}
/* Holds send message recipent list model */
DataboxListModel {
id: recipBoxModel
......@@ -488,15 +514,29 @@ Item {
//----ATTACHMENT SECTION------------
Item {
id: tabAttachments
AccessibleButton {
id: addFile
height: inputItemHeight
Row {
id: buttonBar
spacing: formItemVerticalSpacing * 5
anchors.horizontalCenter: parent.horizontalCenter
font.pointSize: defaultTextFont.font.pointSize
text: qsTr("Add file")
onClicked: {
fileDialogue.raise(qsTr("Select files"), ["*.*"], true, "")
AccessibleButton {
id: addFile
height: inputItemHeight
font.pointSize: defaultTextFont.font.pointSize
text: qsTr("Add file")
onClicked: {
fileDialogue.raise(qsTr("Select files"), ["*.*"], true, "")
}
}
AccessibleButton {
id: icloud
visible: files.isIos()
height: inputItemHeight
font.pointSize: defaultTextFont.font.pointSize
text: qsTr("iCloud")
onClicked: {
fileDialogueIos.raise(qsTr("Select files"), ["*.*"], true, "")
}
}