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