1 /*
2 This file is part of Android File Transfer For Linux.
3 Copyright (C) 2015-2020 Vladimir Menshakov
4
5 This library is free software; you can redistribute it and/or modify it
6 under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License,
8 or (at your option) any later version.
9
10 This library is distributed in the hope that it will be useful, but
11 WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License
16 along with this library; if not, write to the Free Software Foundation,
17 Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 */
19
20 #include "mtpobjectsmodel.h"
21 #include "qtobjectstream.h"
22 #include "utils.h"
23 #include <QDebug>
24 #include <QBrush>
25 #include <QColor>
26 #include <QIcon>
27 #include <QFile>
28 #include <QFont>
29 #include <QFileInfo>
30 #include <QMimeData>
31 #include <QUrl>
32
33 #include <mtp/ptp/ByteArrayObjectStream.h>
34 #include <cli/PosixStreams.h> //for mtime
35
MtpObjectsModel(QObject * parent)36 MtpObjectsModel::MtpObjectsModel(QObject *parent):
37 QAbstractListModel(parent),
38 _storageId(mtp::Session::AllStorages),
39 _parentObjectId(mtp::Session::Root),
40 _enableThumbnails(false)
41 { }
42
~MtpObjectsModel()43 MtpObjectsModel::~MtpObjectsModel()
44 { }
45
setStorageId(mtp::StorageId storageId)46 void MtpObjectsModel::setStorageId(mtp::StorageId storageId)
47 {
48 _storageId = storageId;
49 setParent(mtp::Session::Root);
50 }
51
setParent(mtp::ObjectId parentObjectId)52 void MtpObjectsModel::setParent(mtp::ObjectId parentObjectId)
53 {
54 beginResetModel();
55
56 _parentObjectId = parentObjectId;
57 mtp::msg::ObjectHandles handles;
58 try
59 {
60 handles = _session->GetObjectHandles(_storageId, mtp::ObjectFormat::Any, parentObjectId);
61 }
62 catch(const std::exception & ex)
63 { qWarning() << "setParent failed:" << fromUtf8(ex.what()); }
64 _rows.clear();
65 _rows.reserve(handles.ObjectHandles.size());
66 for(size_t i = 0; i < handles.ObjectHandles.size(); ++i)
67 {
68 mtp::ObjectId oid = handles.ObjectHandles[i];
69 _rows.append(Row(oid));
70 }
71
72 endResetModel();
73 }
74
enter(int idx)75 bool MtpObjectsModel::enter(int idx)
76 {
77 if (idx < 0 || idx >= _rows.size())
78 return false;
79
80 Row &row = _rows[idx];
81 if (row.IsAssociation(_session))
82 {
83 setParent(row.ObjectId);
84 return true;
85 }
86 else
87 return false;
88 }
89
findObject(mtp::ObjectId objectId) const90 QModelIndex MtpObjectsModel::findObject(mtp::ObjectId objectId) const
91 {
92 auto idx = std::find_if(_rows.begin(), _rows.end(), [objectId](const Row & row) { return row.ObjectId == objectId; } );
93 return idx != _rows.end()? createIndex(std::distance(_rows.begin(), idx), 0): QModelIndex();
94 }
95
findObject(const QString & filename) const96 QModelIndex MtpObjectsModel::findObject(const QString &filename) const
97 {
98 auto idx = std::find_if(_rows.begin(), _rows.end(), [filename, this](Row & row) { return fromUtf8(row.GetInfo(_session)->Filename) == filename; } );
99 return idx != _rows.end()? createIndex(std::distance(_rows.begin(), idx), 0): QModelIndex();
100 }
101
setSession(mtp::SessionPtr session)102 void MtpObjectsModel::setSession(mtp::SessionPtr session)
103 {
104 beginResetModel();
105 _session = session;
106 setParent(mtp::Session::Root);
107 endResetModel();
108 }
109
rowCount(const QModelIndex &) const110 int MtpObjectsModel::rowCount(const QModelIndex &) const
111 { return _rows.size(); }
112
GetInfo(mtp::SessionPtr session)113 mtp::msg::ObjectInfoPtr MtpObjectsModel::Row::GetInfo(mtp::SessionPtr session)
114 {
115 if (!_info)
116 {
117 _info = std::make_shared<mtp::msg::ObjectInfo>();
118 try
119 {
120 *_info = session->GetObjectInfo(ObjectId);
121 //qDebug() << fromUtf8(row.Info->Filename);
122 }
123 catch(const std::exception &ex)
124 { qWarning() << "failed to get object info " << fromUtf8(ex.what()); }
125 }
126 return _info;
127 }
128
GetThumbnail(mtp::SessionPtr session,QSize maxSize)129 MtpObjectsModel::ThumbnailPtr MtpObjectsModel::Row::GetThumbnail(mtp::SessionPtr session, QSize maxSize)
130 {
131 if (!_thumbnail)
132 {
133 auto stream = std::make_shared<mtp::ByteArrayObjectOutputStream>();
134 _thumbnail = std::make_shared<QPixmap>();
135 try
136 {
137 auto format = GetInfo(session)->ThumbFormat;
138 if (format == 0)
139 throw std::runtime_error("invalid thumbnail format");
140
141 qDebug() << "requesting thumbnail for " << ObjectId.Id << ", format: " << format;
142 session->GetThumb(ObjectId, stream);
143
144 auto & data = stream->GetData();
145 QPixmap pixmap;
146 if (!pixmap.loadFromData(data.data(), data.size()))
147 throw std::runtime_error("couldn't load pixmap");
148
149 *_thumbnail = pixmap.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
150
151 qDebug() << "loaded " << data.size() << " bytes of thumbnail data";
152 }
153 catch(const std::exception &ex)
154 {
155 qWarning() << "failed to get thumbnail" << fromUtf8(ex.what());
156 *_thumbnail = QIcon::fromTheme("image-missing").pixmap(maxSize);
157 }
158 }
159 return _thumbnail;
160 }
161
IsAssociation(mtp::SessionPtr session)162 bool MtpObjectsModel::Row::IsAssociation(mtp::SessionPtr session)
163 {
164 mtp::ObjectFormat format = GetInfo(session)->ObjectFormat;
165 return format == mtp::ObjectFormat::Association;
166 }
167
rename(int idx,const QString & fileName)168 void MtpObjectsModel::rename(int idx, const QString &fileName)
169 {
170 qDebug() << "renaming row " << idx << " to " << fileName;
171 try
172 {
173 _session->SetObjectProperty(objectIdAt(idx), mtp::ObjectProperty::ObjectFilename, toUtf8(fileName));
174 }
175 catch(const std::exception &ex)
176 { qWarning() << "failed to rename object" << fromUtf8(ex.what()); }
177
178 _rows[idx].ResetInfo();
179 emit dataChanged(createIndex(idx, 0), createIndex(idx, 0));
180 }
181
deleteObjects(const MtpObjectList & objects)182 void MtpObjectsModel::deleteObjects(const MtpObjectList &objects)
183 {
184 for(mtp::ObjectId objectId: objects)
185 {
186 try
187 {
188 qDebug() << "deleting object " << objectId;
189 _session->DeleteObject(objectId);
190 }
191 catch(const std::exception &ex)
192 { qWarning() << "failed to delete object " << fromUtf8(ex.what()); }
193 }
194 refresh();
195 }
196
197
objectIdAt(int idx)198 mtp::ObjectId MtpObjectsModel::objectIdAt(int idx)
199 {
200 return (idx >= 0 && idx < _rows.size())? _rows[idx].ObjectId: mtp::ObjectId();
201 }
202
data(const QModelIndex & index,int role) const203 QVariant MtpObjectsModel::data(const QModelIndex &index, int role) const
204 {
205 int row_idx = index.row();
206 if (row_idx < 0 || row_idx > _rows.size())
207 return QVariant();
208
209 Row &row = _rows[row_idx];
210
211 switch(role)
212 {
213 case Qt::ToolTipRole:
214 case Qt::DisplayRole:
215 return fromUtf8(row.GetInfo(_session)->Filename);
216
217 case Qt::FontRole:
218 {
219 QFont font;
220 if (row.IsAssociation(_session))
221 font.setBold(true);
222 return font;
223 }
224
225 case Qt::DecorationRole:
226 if (_enableThumbnails)
227 return *row.GetThumbnail(_session, QSize(_maxThumbnailSize.width(), _maxThumbnailSize.height() / 1.5f));
228 else
229 return QVariant();
230
231 case Qt::SizeHintRole:
232 if (_enableThumbnails)
233 return _maxThumbnailSize;
234 else
235 return QVariant();
236
237 default:
238 return QVariant();
239 }
240 }
241
createDirectory(mtp::ObjectId parentObjectId,const QString & name,mtp::AssociationType type)242 mtp::ObjectId MtpObjectsModel::createDirectory(mtp::ObjectId parentObjectId, const QString &name, mtp::AssociationType type)
243 {
244 QModelIndex existingDir = findObject(name);
245 if (existingDir.isValid())
246 return _rows.at(existingDir.row()).ObjectId;
247
248 mtp::StorageId storageId = _storageId != mtp::Session::AllStorages? _storageId: mtp::Session::AnyStorage;
249 auto noi = _session->CreateDirectory(toUtf8(name), parentObjectId, storageId, type);
250 if (parentObjectId == _parentObjectId)
251 {
252 beginInsertRows(QModelIndex(), _rows.size(), _rows.size());
253 _rows.push_back(Row(noi.ObjectId));
254 endInsertRows();
255 }
256 return noi.ObjectId;
257 }
258
uploadFile(mtp::ObjectId parentObjectId,const QString & filePath,QString filename)259 bool MtpObjectsModel::uploadFile(mtp::ObjectId parentObjectId, const QString &filePath, QString filename)
260 {
261 QFileInfo fileInfo(filePath);
262 mtp::ObjectFormat objectFormat = mtp::ObjectFormatFromFilename(toUtf8(filePath));
263
264 if (filename.isEmpty())
265 filename = fileInfo.fileName();
266
267 qDebug() << "uploadFile " << fileInfo.fileName() << " as " << filename;
268
269 bool needReset = false;
270 QModelIndex existingObject = findObject(filename);
271 if (existingObject.isValid())
272 {
273 if (!emit existingFileOverwrite(filename))
274 {
275 qDebug() << "skipping, overwrite not confirmed";
276 return false;
277 }
278 try
279 {
280 _session->DeleteObject(_rows.at(existingObject.row()).ObjectId);
281 }
282 catch(const std::exception &ex)
283 { qWarning() << "failed to delete object" << fromUtf8(ex.what()); return false; }
284
285 needReset = parentObjectId == _parentObjectId;
286 }
287
288 std::shared_ptr<QtObjectInputStream> object(new QtObjectInputStream(filePath));
289 if (!object->Valid())
290 {
291 qWarning() << "file " << filePath << " could not be opened";
292 return false;
293 }
294 qDebug() << "sending " << fileInfo.size() << " bytes";
295 connect(object.get(), SIGNAL(positionChanged(qint64,qint64)), this, SIGNAL(filePositionChanged(qint64,qint64)));
296
297 mtp::msg::ObjectInfo oi;
298 oi.Filename = toUtf8(filename);
299 oi.ObjectFormat = objectFormat;
300 oi.ObjectCompressedSize = fileInfo.size();
301 mtp::msg::NewObjectInfo noi;
302 try
303 {
304 noi = _session->SendObjectInfo(oi, _storageId != mtp::Session::AllStorages? _storageId: mtp::Session::AnyStorage, parentObjectId);
305 qDebug() << "new object id: " << noi.ObjectId << ", sending...";
306 _session->SendObject(object);
307 qDebug() << "ok";
308 }
309 catch(const std::exception &ex)
310 { qWarning() << "failed to send new object info " << fromUtf8(ex.what()); return false; }
311
312 if (parentObjectId == _parentObjectId)
313 {
314 beginInsertRows(QModelIndex(), _rows.size(), _rows.size());
315 _rows.push_back(Row(noi.ObjectId));
316 endInsertRows();
317 }
318 if (needReset)
319 refresh();
320 return true;
321 }
322
sendFile(const QString & filePath)323 bool MtpObjectsModel::sendFile(const QString &filePath)
324 {
325 try
326 {
327 std::shared_ptr<QtObjectInputStream> object(new QtObjectInputStream(filePath));
328 connect(object.get(), SIGNAL(positionChanged(qint64,qint64)), this, SIGNAL(filePositionChanged(qint64,qint64)));
329 _session->SendObject(object);
330 return true;
331 }
332 catch(const std::exception &ex)
333 { qWarning() << "failed to send file " << fromUtf8(ex.what()); return false; }
334 }
335
downloadFile(const QString & filePath,mtp::ObjectId objectId)336 bool MtpObjectsModel::downloadFile(const QString &filePath, mtp::ObjectId objectId)
337 {
338 auto object = std::make_shared<QtObjectOutputStream>(filePath);
339 if (!object->Valid())
340 {
341 qWarning() << "cannot open file " << filePath;
342 return false;
343 }
344 connect(object.get(), SIGNAL(positionChanged(qint64,qint64)), this, SIGNAL(filePositionChanged(qint64,qint64)));
345 _session->GetObject(objectId, object);
346 object.reset();
347 cli::ObjectOutputStream::SetModificationTime(filePath.toStdString(), _session->GetObjectModificationTime(objectId));
348 return true;
349 }
350
getInfoById(mtp::ObjectId objectId) const351 MtpObjectsModel::ObjectInfo MtpObjectsModel::getInfoById(mtp::ObjectId objectId) const
352 {
353 mtp::msg::ObjectInfo oi(_session->GetObjectInfo(objectId));
354 qint64 size = oi.ObjectCompressedSize;
355 if (size == mtp::MaxObjectSize)
356 size = _session->GetObjectIntegerProperty(objectId, mtp::ObjectProperty::ObjectSize);
357 return ObjectInfo(fromUtf8(oi.Filename), oi.ObjectFormat, size);
358 }
359
extractMimeData(const QMimeData * data)360 QStringList MtpObjectsModel::extractMimeData(const QMimeData *data)
361 {
362 QStringList files;
363 QList<QUrl> urls = data->urls();
364 for (auto url : urls)
365 {
366 //qDebug() << "url " << url;
367 if (url.isLocalFile())
368 files.push_back(url.toLocalFile());
369 else
370 qWarning() << "skipping non-local url" << url;
371 }
372 return files;
373 }
374
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)375 bool MtpObjectsModel::dropMimeData(const QMimeData *data,
376 Qt::DropAction action, int row, int column, const QModelIndex &parent)
377 {
378 qDebug() << "data: " << data << action << row << column;
379 if (action != Qt::CopyAction || !data)
380 return false;
381
382 QStringList files = extractMimeData(data);
383 qDebug() << "files dropped: " << files;
384 emit onFilesDropped(files);
385 return true;
386 }
387
mimeTypes() const388 QStringList MtpObjectsModel::mimeTypes () const
389 { return QStringList("text/uri-list"); }
390
flags(const QModelIndex & index) const391 Qt::ItemFlags MtpObjectsModel::flags(const QModelIndex &index) const
392 {
393 Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
394 return defaultFlags | Qt::ItemIsDropEnabled;
395 }
396
enableThumbnail(bool enable,QSize maxSize)397 void MtpObjectsModel::enableThumbnail(bool enable, QSize maxSize)
398 {
399 beginResetModel();
400
401 _enableThumbnails = enable;
402 _maxThumbnailSize = maxSize;
403
404 endResetModel();
405 }
406