1 /*
2 * Copyright (C) 2015 Jonathan Matthew <jonathan@d14n.org>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * The Rhythmbox authors hereby grant permission for non-GPL compatible
10 * GStreamer plugins to be used and distributed together with GStreamer
11 * and Rhythmbox. This permission is above and beyond the permissions granted
12 * by the GPL license by which Rhythmbox is covered. If you modify this code
13 * you may extend this exception to your version of the code, but you are not
14 * obligated to do so. If you do not wish to do so, delete this exception
15 * statement from your version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
25 *
26 */
27
28 #define __EXTENSIONS__
29
30 #include "config.h"
31
32 #include <string.h>
33
34 #include <glib/gi18n.h>
35 #include <gtk/gtk.h>
36
37 #include <gudev/gudev.h>
38
39 #include "mediaplayerid.h"
40
41 #include "rb-android-source.h"
42 #include "rb-debug.h"
43 #include "rb-util.h"
44 #include "rb-file-helpers.h"
45 #include "rhythmdb.h"
46 #include "rb-builder-helpers.h"
47 #include "rb-application.h"
48 #include "rb-transfer-target.h"
49 #include "rb-device-source.h"
50 #include "rb-sync-settings.h"
51 #include "rb-import-errors-source.h"
52 #include "rb-gst-media-types.h"
53 #include "rb-task-list.h"
54 #include "rb-encoder.h"
55 #include "rb-dialog.h"
56
57 static void rb_android_device_source_init (RBDeviceSourceInterface *interface);
58 static void rb_android_transfer_target_init (RBTransferTargetInterface *interface);
59
60 static void find_music_dirs (RBAndroidSource *source);
61 static void rescan_music_dirs (RBAndroidSource *source);
62 static void update_free_space_next (RBAndroidSource *source);
63
64 enum
65 {
66 PROP_0,
67 PROP_VOLUME,
68 PROP_MOUNT_ROOT,
69 PROP_IGNORE_ENTRY_TYPE,
70 PROP_ERROR_ENTRY_TYPE,
71 PROP_DEVICE_INFO,
72 PROP_DEVICE_SERIAL,
73 PROP_GUDEV_DEVICE
74 };
75
76 typedef struct
77 {
78 RhythmDB *db;
79
80 gboolean loaded;
81 RhythmDBImportJob *import_job;
82 RBSource *import_errors;
83 GCancellable *cancel;
84 GQueue to_scan;
85 int scanned;
86
87 RhythmDBEntryType *ignore_type;
88 RhythmDBEntryType *error_type;
89
90 MPIDDevice *device_info;
91 GUdevDevice *gudev_device;
92 GVolume *volume;
93 GObject *mount_root;
94 gboolean ejecting;
95
96 GList *storage;
97 guint64 storage_capacity;
98 guint64 storage_free_space;
99 GList *query_storage;
100 guint64 storage_free_space_next;
101 guint64 storage_capacity_next;
102 guint rescan_id;
103
104 GtkWidget *grid;
105 GtkWidget *info_bar;
106 } RBAndroidSourcePrivate;
107
108 G_DEFINE_DYNAMIC_TYPE_EXTENDED (
109 RBAndroidSource,
110 rb_android_source,
111 RB_TYPE_MEDIA_PLAYER_SOURCE,
112 0,
113 G_IMPLEMENT_INTERFACE_DYNAMIC (RB_TYPE_DEVICE_SOURCE, rb_android_device_source_init)
114 G_IMPLEMENT_INTERFACE_DYNAMIC (RB_TYPE_TRANSFER_TARGET, rb_android_transfer_target_init))
115
116 #define GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_ANDROID_SOURCE, RBAndroidSourcePrivate))
117
118 static void
free_space_cb(GObject * obj,GAsyncResult * res,gpointer data)119 free_space_cb (GObject *obj, GAsyncResult *res, gpointer data)
120 {
121 RBAndroidSource *source = RB_ANDROID_SOURCE (data);
122 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
123 GFileInfo *info;
124 GError *error = NULL;
125
126 info = g_file_query_filesystem_info_finish (G_FILE (obj), res, &error);
127 if (info == NULL) {
128 rb_debug ("error querying filesystem free space: %s", error->message);
129 g_clear_error (&error);
130 } else {
131 priv->storage_free_space_next += g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
132 priv->storage_capacity_next += g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
133 rb_debug ("capacity: %" G_GUINT64_FORMAT ", free space: %" G_GUINT64_FORMAT,
134 priv->storage_capacity_next, priv->storage_free_space_next);
135 }
136
137 priv->query_storage = priv->query_storage->next;
138 if (priv->query_storage != NULL) {
139 update_free_space_next (source);
140 } else {
141 priv->storage_free_space = priv->storage_free_space_next;
142 priv->storage_capacity = priv->storage_capacity_next;
143 }
144 }
145
146 static void
update_free_space_next(RBAndroidSource * source)147 update_free_space_next (RBAndroidSource *source)
148 {
149 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
150 GFile *file;
151 const char *attrs = G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," G_FILE_ATTRIBUTE_FILESYSTEM_SIZE;
152
153 file = G_FILE (priv->query_storage->data);
154 g_file_query_filesystem_info_async (file, attrs, G_PRIORITY_DEFAULT, NULL, free_space_cb, source);
155 }
156
157 static void
update_free_space(RBAndroidSource * source)158 update_free_space (RBAndroidSource *source)
159 {
160 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
161
162 if (priv->query_storage != NULL) {
163 rb_debug ("already updating free space");
164 return;
165 }
166
167 if (priv->storage == NULL) {
168 rb_debug ("no storage to query");
169 return;
170 }
171
172 priv->storage_free_space_next = 0;
173 priv->storage_capacity_next = 0;
174 priv->query_storage = priv->storage;
175 update_free_space_next (source);
176 }
177
178
179 static void
music_dirs_done(RBAndroidSource * source)180 music_dirs_done (RBAndroidSource *source)
181 {
182 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
183
184 if (priv->scanned > 1) {
185 gtk_widget_hide (priv->info_bar);
186 rhythmdb_import_job_start (priv->import_job);
187
188 if (priv->rescan_id != 0) {
189 g_source_remove (priv->rescan_id);
190 }
191
192 if (priv->storage != NULL) {
193 rb_debug ("finished checking for music dirs");
194 update_free_space (source);
195 } else {
196 rb_debug ("no music dirs found (%d)", priv->scanned);
197 }
198 } else {
199 GtkWidget *label;
200
201 rb_debug ("no storage areas found");
202 if (gtk_widget_get_visible (priv->info_bar) == FALSE) {
203 label = gtk_label_new (_("No storage areas found on this device. You may need to unlock it and enable MTP."));
204 gtk_container_add (GTK_CONTAINER (gtk_info_bar_get_content_area (GTK_INFO_BAR (priv->info_bar))), label);
205 gtk_info_bar_set_message_type (GTK_INFO_BAR (priv->info_bar), GTK_MESSAGE_INFO);
206 gtk_widget_show_all (priv->info_bar);
207 }
208 if (priv->rescan_id == 0)
209 priv->rescan_id = g_timeout_add_seconds (5, (GSourceFunc) rescan_music_dirs, source);
210 }
211 }
212
213
214 static void
enum_files_cb(GObject * obj,GAsyncResult * result,gpointer data)215 enum_files_cb (GObject *obj, GAsyncResult *result, gpointer data)
216 {
217 RBAndroidSource *source = RB_ANDROID_SOURCE (data);
218 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
219 GFileEnumerator *e = G_FILE_ENUMERATOR (obj);
220 GError *error = NULL;
221 GFileInfo *info;
222 GList *files;
223 GList *l;
224
225 files = g_file_enumerator_next_files_finish (e, result, &error);
226 if (error != NULL) {
227 rb_debug ("error listing files: %s", error->message);
228 music_dirs_done (source);
229 return;
230 }
231
232 if (files == NULL) {
233 priv->scanned++;
234 g_object_unref (e);
235 find_music_dirs (source);
236 return;
237 }
238
239 for (l = files; l != NULL; l = l->next) {
240 guint32 filetype;
241 info = (GFileInfo *)l->data;
242
243 filetype = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE);
244 if (filetype == G_FILE_TYPE_DIRECTORY) {
245 GFile *dir;
246 if (priv->scanned == 0) {
247
248 rb_debug ("got storage container %s", g_file_info_get_name (info));
249 dir = g_file_get_child (g_file_enumerator_get_container (e), g_file_info_get_name (info));
250 g_queue_push_tail (&priv->to_scan, dir);
251 } else if (g_ascii_strcasecmp (g_file_info_get_name (info), "music") == 0) {
252 GFile *storage;
253 char *uri;
254
255 storage = g_file_enumerator_get_container (e);
256 dir = g_file_get_child (storage, g_file_info_get_name (info));
257 uri = g_file_get_uri (dir);
258 rb_debug ("music dir found at %s", uri);
259
260 /* keep the container around for space/capacity calculation */
261 priv->storage = g_list_append (priv->storage, dir);
262
263 rhythmdb_import_job_add_uri (priv->import_job, uri);
264 g_free (uri);
265 }
266 }
267
268 g_object_unref (info);
269 }
270
271 g_list_free (files);
272
273 g_file_enumerator_next_files_async (G_FILE_ENUMERATOR (obj), 64, G_PRIORITY_DEFAULT, priv->cancel, enum_files_cb, source);
274 }
275
276 static void
enum_child_cb(GObject * obj,GAsyncResult * result,gpointer data)277 enum_child_cb (GObject *obj, GAsyncResult *result, gpointer data)
278 {
279 RBAndroidSource *source = RB_ANDROID_SOURCE (data);
280 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
281 GFileEnumerator *e;
282 GError *error = NULL;
283
284 e = g_file_enumerate_children_finish (G_FILE (obj), result, &error);
285 if (e == NULL) {
286 rb_debug ("enum error: %s", error->message);
287 g_clear_error (&error);
288 music_dirs_done (source);
289 return;
290 }
291
292 g_file_enumerator_next_files_async (e, 64, G_PRIORITY_DEFAULT, priv->cancel, enum_files_cb, source);
293 }
294
295 static void
find_music_dirs(RBAndroidSource * source)296 find_music_dirs (RBAndroidSource *source)
297 {
298 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
299 const char *attrs =
300 G_FILE_ATTRIBUTE_STANDARD_NAME ","
301 G_FILE_ATTRIBUTE_STANDARD_TYPE;
302
303 gpointer dir;
304
305 dir = g_queue_pop_head (&priv->to_scan);
306 if (dir == NULL) {
307 music_dirs_done (source);
308 return;
309 }
310
311 rb_debug ("scanning %s", g_file_get_uri (G_FILE (dir)));
312 g_file_enumerate_children_async (G_FILE (dir),
313 attrs,
314 G_FILE_QUERY_INFO_NONE,
315 G_PRIORITY_DEFAULT,
316 priv->cancel,
317 enum_child_cb,
318 source);
319 g_object_unref (dir);
320 }
321
322 static void
rescan_music_dirs(RBAndroidSource * source)323 rescan_music_dirs (RBAndroidSource *source)
324 {
325 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
326 GFile *root;
327
328 g_object_get (source, "mount-root", &root, NULL);
329
330 priv->scanned = 0;
331 g_queue_push_tail (&priv->to_scan, root);
332
333 find_music_dirs (source);
334 }
335
336 static void
import_complete_cb(RhythmDBImportJob * job,int total,RBAndroidSource * source)337 import_complete_cb (RhythmDBImportJob *job, int total, RBAndroidSource *source)
338 {
339 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
340 GSettings *settings;
341 RBShell *shell;
342
343 if (priv->ejecting) {
344 rb_device_source_default_eject (RB_DEVICE_SOURCE (source));
345 } else {
346 g_object_get (source, "shell", &shell, NULL);
347 rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (priv->import_errors), RB_DISPLAY_PAGE (source));
348 g_object_unref (shell);
349
350 g_object_set (source, "load-status", RB_SOURCE_LOAD_STATUS_LOADED, NULL);
351
352 g_object_get (source, "encoding-settings", &settings, NULL);
353 rb_transfer_target_transfer (RB_TRANSFER_TARGET (source), settings, NULL, FALSE);
354 g_object_unref (settings);
355
356 rb_media_player_source_purge_metadata_cache (RB_MEDIA_PLAYER_SOURCE (source));
357 }
358
359 g_clear_object (&priv->import_job);
360 }
361
362 static void
actually_load(RBAndroidSource * source)363 actually_load (RBAndroidSource *source)
364 {
365 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
366 RBTaskList *tasklist;
367 RhythmDBEntryType *entry_type;
368 RBShell *shell;
369 GFile *root;
370 char *name;
371 char *label;
372
373 priv->loaded = TRUE;
374 rb_media_player_source_load (RB_MEDIA_PLAYER_SOURCE (source));
375
376 /* identify storage containers and find music dirs within them */
377 g_object_get (source, "mount-root", &root, "entry-type", &entry_type, NULL);
378
379 priv->cancel = g_cancellable_new ();
380 priv->import_job = rhythmdb_import_job_new (priv->db, entry_type, priv->ignore_type, priv->error_type);
381 g_signal_connect_object (priv->import_job, "complete", G_CALLBACK (import_complete_cb), source, 0);
382
383 priv->scanned = 0;
384 g_queue_init (&priv->to_scan);
385 g_queue_push_tail (&priv->to_scan, root);
386 g_object_unref (entry_type);
387
388 find_music_dirs (source);
389
390 g_object_get (source, "name", &name, "shell", &shell, NULL);
391 label = g_strdup_printf (_("Scanning %s"), name);
392 g_object_set (priv->import_job, "task-label", label, NULL);
393
394 g_object_get (shell, "task-list", &tasklist, NULL);
395 rb_task_list_add_task (tasklist, RB_TASK_PROGRESS (priv->import_job));
396 g_object_unref (tasklist);
397 g_object_unref (shell);
398
399 g_free (label);
400 g_free (name);
401 }
402
403 static void
volume_mount_cb(GObject * source_object,GAsyncResult * res,gpointer user_data)404 volume_mount_cb (GObject *source_object, GAsyncResult *res, gpointer user_data)
405 {
406 RBAndroidSource *source = RB_ANDROID_SOURCE (user_data);
407 GVolume *volume = G_VOLUME (source_object);
408 GError *error = NULL;
409
410 rb_debug ("volume mount finished");
411 if (g_volume_mount_finish (volume, res, &error)) {
412 actually_load (source);
413 } else {
414 rb_error_dialog (NULL, _("Error mounting Android device"), "%s", error->message);
415 g_clear_error (&error);
416 }
417 }
418
419 static gboolean
ensure_loaded(RBAndroidSource * source)420 ensure_loaded (RBAndroidSource *source)
421 {
422 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
423 RBSourceLoadStatus status;
424 GMount *mount;
425
426 if (priv->loaded) {
427 g_object_get (source, "load-status", &status, NULL);
428 return (status == RB_SOURCE_LOAD_STATUS_LOADED);
429 }
430
431 mount = g_volume_get_mount (priv->volume);
432 if (mount != NULL) {
433 rb_debug ("volume is mounted");
434 g_object_unref (mount);
435 actually_load (source);
436 return FALSE;
437 }
438
439 rb_debug ("mounting volume");
440 g_volume_mount (priv->volume, G_MOUNT_MOUNT_NONE, NULL, NULL, volume_mount_cb, source);
441 return FALSE;
442 }
443
444 static void
delete_data_destroy(gpointer data)445 delete_data_destroy (gpointer data)
446 {
447 g_list_free_full (data, (GDestroyNotify) rhythmdb_entry_unref);
448 }
449
450
451 static gboolean
can_delete_directory(RBAndroidSource * source,GFile * dir)452 can_delete_directory (RBAndroidSource *source, GFile *dir)
453 {
454 GFile *root;
455 char *path;
456 int i;
457 int c;
458
459 g_object_get (source, "mount-root", &root, NULL);
460
461 /*
462 * path here will be sdcard/Music/something for anything we want to delete
463 */
464 path = g_file_get_relative_path (root, dir);
465 c = 0;
466 for (i = 0; path[i] != '\0'; i++) {
467 if (path[i] == '/')
468 c++;
469 }
470
471 g_free (path);
472 g_object_unref (root);
473 return (c > 1);
474 }
475
476 static void
delete_entries_task(GTask * task,gpointer source_object,gpointer task_data,GCancellable * cancellable)477 delete_entries_task (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable)
478 {
479 RBAndroidSource *source = RB_ANDROID_SOURCE (source_object);
480 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
481 GList *l;
482
483 for (l = task_data; l != NULL; l = l->next) {
484 RhythmDBEntry *entry;
485 const char *uri;
486 GFile *file;
487 GFile *dir;
488
489 entry = l->data;
490 uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
491 file = g_file_new_for_uri (uri);
492 g_file_delete (file, NULL, NULL);
493
494 /* now walk up the directory structure and delete empty dirs
495 * until we reach the root or one of the device's audio folders.
496 */
497 dir = g_file_get_parent (file);
498 while (can_delete_directory (source, dir)) {
499 GFile *parent;
500
501 if (g_file_delete (dir, NULL, NULL) == FALSE) {
502 break;
503 }
504
505 parent = g_file_get_parent (dir);
506 if (parent == NULL) {
507 break;
508 }
509 g_object_unref (dir);
510 dir = parent;
511 }
512
513 g_object_unref (dir);
514 g_object_unref (file);
515
516 rhythmdb_entry_delete (priv->db, entry);
517 }
518
519 rhythmdb_commit (priv->db);
520
521 g_task_return_boolean (task, TRUE);
522 g_object_unref (task);
523 }
524
525 static void
impl_delete_entries(RBMediaPlayerSource * source,GList * entries,GAsyncReadyCallback callback,gpointer data)526 impl_delete_entries (RBMediaPlayerSource *source, GList *entries, GAsyncReadyCallback callback, gpointer data)
527 {
528 GTask *task;
529 GList *task_entries;
530
531 task = g_task_new (source, NULL, callback, data);
532 task_entries = g_list_copy_deep (entries, (GCopyFunc) rhythmdb_entry_ref, NULL);
533 g_task_set_task_data (task, task_entries, delete_data_destroy);
534 g_task_run_in_thread (task, delete_entries_task);
535 }
536
537 static void
impl_show_properties(RBMediaPlayerSource * source,GtkWidget * info_box,GtkWidget * notebook)538 impl_show_properties (RBMediaPlayerSource *source, GtkWidget *info_box, GtkWidget *notebook)
539 {
540 RhythmDBQueryModel *model;
541 GtkBuilder *builder;
542 GtkWidget *widget;
543 GObject *plugin;
544 char *text;
545
546 g_object_get (source, "plugin", &plugin, NULL);
547 builder = rb_builder_load_plugin_file (G_OBJECT (plugin), "android-info.ui", NULL);
548 g_object_unref (plugin);
549
550 /* 'basic' tab stuff */
551
552 widget = GTK_WIDGET (gtk_builder_get_object (builder, "android-basic-info"));
553 gtk_box_pack_start (GTK_BOX (info_box), widget, TRUE, TRUE, 0);
554
555 g_object_get (source, "base-query-model", &model, NULL);
556 widget = GTK_WIDGET (gtk_builder_get_object (builder, "num-tracks"));
557 text = g_strdup_printf ("%d", gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL));
558 gtk_label_set_text (GTK_LABEL (widget), text);
559 g_free (text);
560 g_object_unref (model);
561
562 g_object_unref (builder);
563 }
564
565 static void
impl_get_entries(RBMediaPlayerSource * source,const char * category,GHashTable * map)566 impl_get_entries (RBMediaPlayerSource *source,
567 const char *category,
568 GHashTable *map)
569 {
570 RhythmDBQueryModel *model;
571 GtkTreeIter iter;
572 gboolean podcast;
573
574 /* we don't have anything else to distinguish podcasts from regular
575 * tracks, so just use the genre.
576 */
577 podcast = (g_str_equal (category, SYNC_CATEGORY_PODCAST));
578
579 g_object_get (source, "base-query-model", &model, NULL);
580 if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter) == FALSE) {
581 g_object_unref (model);
582 return;
583 }
584
585 do {
586 RhythmDBEntry *entry;
587 const char *genre;
588 entry = rhythmdb_query_model_iter_to_entry (model, &iter);
589 genre = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE);
590 if (g_str_equal (genre, "Podcast") == podcast) {
591 _rb_media_player_source_add_to_map (map, entry);
592 }
593 } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (model), &iter));
594
595 g_object_unref (model);
596 }
597
598 static guint64
impl_get_capacity(RBMediaPlayerSource * source)599 impl_get_capacity (RBMediaPlayerSource *source)
600 {
601 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
602 return priv->storage_capacity;
603 }
604
605 static guint64
impl_get_free_space(RBMediaPlayerSource * source)606 impl_get_free_space (RBMediaPlayerSource *source)
607 {
608 RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
609 return priv->storage_free_space;
610 }
611
612 static gboolean
impl_can_paste(RBSource * source)613 impl_can_paste (RBSource *source)
614 {
615 return TRUE;
616 }
617
618 static RBTrackTransferBatch *
impl_paste(RBSource * source,GList * entries)619 impl_paste (RBSource *source, GList *entries)
620 {
621 gboolean defer;
622 GSettings *settings;
623 RBTrackTransferBatch *batch;
624
625 defer = (ensure_loaded (RB_ANDROID_SOURCE (source)) == FALSE);
626 g_object_get (source, "encoding-settings", &settings, NULL);
627 batch = rb_transfer_target_transfer (RB_TRANSFER_TARGET (source), settings, entries, defer);
628 g_object_unref (settings);
629 return batch;
630 }
631
632 static gboolean
impl_can_delete(RBSource * source)633 impl_can_delete (RBSource *source)
634 {
635 return TRUE;
636 }
637
638 static void
impl_delete_selected(RBSource * source)639 impl_delete_selected (RBSource *source)
640 {
641 RBEntryView *view;
642 GList *sel;
643
644 view = rb_source_get_entry_view (source);
645 sel = rb_entry_view_get_selected_entries (view);
646
647 impl_delete_entries (RB_MEDIA_PLAYER_SOURCE (source), sel, NULL, NULL);
648 g_list_free_full (sel, (GDestroyNotify) rhythmdb_entry_unref);
649 }
650
651
652 static void
impl_eject(RBDeviceSource * source)653 impl_eject (RBDeviceSource *source)
654 {
655 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
656
657 if (priv->import_job != NULL) {
658 rhythmdb_import_job_cancel (priv->import_job);
659 priv->ejecting = TRUE;
660 } else {
661 rb_device_source_default_eject (source);
662 }
663 }
664
665
666 static char *
sanitize_path(const char * str)667 sanitize_path (const char *str)
668 {
669 char *res = NULL;
670 char *s;
671
672 /* Skip leading periods, otherwise files disappear... */
673 while (*str == '.')
674 str++;
675
676 s = g_strdup (str);
677 rb_sanitize_path_for_msdos_filesystem (s);
678 res = g_uri_escape_string (s, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH_ELEMENT, TRUE);
679 g_free (s);
680 return res;
681 }
682
683
684 static char *
build_device_uri(RBAndroidSource * source,RhythmDBEntry * entry,const char * media_type,const char * extension)685 build_device_uri (RBAndroidSource *source, RhythmDBEntry *entry, const char *media_type, const char *extension)
686 {
687 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
688 const char *in_artist;
689 char *artist, *album, *title;
690 gulong track_number, disc_number;
691 char *number;
692 char *file = NULL;
693 char *storage_uri;
694 char *uri;
695 char *ext;
696 GFile *storage = NULL;
697
698 if (extension != NULL) {
699 ext = g_strconcat (".", extension, NULL);
700 } else {
701 ext = g_strdup ("");
702 }
703
704 in_artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM_ARTIST);
705 if (in_artist[0] == '\0') {
706 in_artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
707 }
708 artist = sanitize_path (in_artist);
709 album = sanitize_path (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM));
710 title = sanitize_path (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
711
712 /* we really do need to fix this so untagged entries actually have NULL rather than
713 * a translated string.
714 */
715 if (strcmp (artist, _("Unknown")) == 0 && strcmp (album, _("Unknown")) == 0 &&
716 g_str_has_suffix (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION), title)) {
717 /* file isn't tagged, so just use the filename as-is, replacing the extension */
718 char *p;
719
720 p = g_utf8_strrchr (title, -1, '.');
721 if (p != NULL) {
722 *p = '\0';
723 }
724 file = g_strdup_printf ("%s%s", title, ext);
725 }
726
727 if (file == NULL) {
728 track_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
729 disc_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DISC_NUMBER);
730 if (disc_number > 0)
731 number = g_strdup_printf ("%.02u.%.02u", (guint)disc_number, (guint)track_number);
732 else
733 number = g_strdup_printf ("%.02u", (guint)track_number);
734
735 /* artist/album/number - title */
736 file = g_strdup_printf (G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S "%s%%20-%%20%s%s",
737 artist, album, number, title, ext);
738 g_free (number);
739 }
740
741 g_free (artist);
742 g_free (album);
743 g_free (title);
744 g_free (ext);
745
746 /* pick storage container to use somehow
747 for (l = priv->storage; l != NULL; l = l->next) {
748 }
749 */
750 if (priv->storage)
751 storage = priv->storage->data;
752
753 if (storage == NULL) {
754 rb_debug ("couldn't find a container to store anything in");
755 g_free (file);
756 return NULL;
757 }
758
759 storage_uri = g_file_get_uri (storage);
760 uri = g_strconcat (storage_uri, file, NULL);
761 g_free (file);
762 g_free (storage_uri);
763
764 return uri;
765 }
766
767 static char *
impl_build_dest_uri(RBTransferTarget * target,RhythmDBEntry * entry,const char * media_type,const char * extension)768 impl_build_dest_uri (RBTransferTarget *target,
769 RhythmDBEntry *entry,
770 const char *media_type,
771 const char *extension)
772 {
773 return g_strdup (RB_ENCODER_DEST_TEMPFILE);
774 }
775
776 static void
track_copy_cb(GObject * src,GAsyncResult * res,gpointer data)777 track_copy_cb (GObject *src, GAsyncResult *res, gpointer data)
778 {
779 RBAndroidSource *source = RB_ANDROID_SOURCE (data);
780 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
781 RhythmDBEntryType *entry_type;
782 RBShell *shell;
783 RhythmDB *db;
784 GFile *dest;
785 char *uri;
786 GError *error = NULL;
787
788 if (g_task_propagate_boolean (G_TASK (res), &error)) {
789
790 dest = G_FILE (src);
791 uri = g_file_get_uri (dest);
792
793 g_object_get (source, "shell", &shell, NULL);
794 g_object_get (shell, "db", &db, NULL);
795 g_object_unref (shell);
796
797 g_object_get (source, "entry-type", &entry_type, NULL);
798 rhythmdb_add_uri_with_types (db,
799 uri,
800 entry_type,
801 priv->ignore_type,
802 priv->error_type);
803 g_object_unref (entry_type);
804 g_object_unref (db);
805 g_free (uri);
806
807 update_free_space (source);
808 } else {
809 rb_error_dialog (NULL, _("Error transferring track"), "%s", error->message);
810 }
811
812 g_clear_error (&error);
813 g_object_unref (src);
814 g_object_unref (source);
815 }
816
817 static void
copy_track_task(GTask * task,gpointer pdest,gpointer psource,GCancellable * cancel)818 copy_track_task (GTask *task, gpointer pdest, gpointer psource, GCancellable *cancel)
819 {
820 GFile *source = G_FILE (psource);
821 GFile *dest = G_FILE (pdest);
822 GError *error = NULL;
823 char *uri;
824
825 uri = g_file_get_uri (dest);
826 rb_debug ("creating parent dirs for %s", uri);
827 if (rb_uri_create_parent_dirs (uri, &error) == FALSE) {
828 g_file_delete (source, NULL, NULL);
829 g_free (uri);
830 g_task_return_error (task, error);
831 return;
832 }
833 rb_debug ("moving %s", uri);
834 g_free (uri);
835
836 g_file_move (source,
837 dest,
838 G_FILE_COPY_OVERWRITE,
839 NULL,
840 NULL,
841 NULL,
842 &error);
843 if (error) {
844 g_file_delete (source, NULL, NULL);
845 g_task_return_error (task, error);
846 } else {
847 g_task_return_boolean (task, TRUE);
848 }
849 }
850
851 static gboolean
impl_track_added(RBTransferTarget * target,RhythmDBEntry * entry,const char * dest,guint64 filesize,const char * media_type)852 impl_track_added (RBTransferTarget *target,
853 RhythmDBEntry *entry,
854 const char *dest,
855 guint64 filesize,
856 const char *media_type)
857 {
858 RBAndroidSource *source = RB_ANDROID_SOURCE (target);
859 char *realdest;
860 GFile *dfile, *sfile;
861 GTask *task;
862
863 realdest = build_device_uri (source, entry, media_type, rb_gst_media_type_to_extension (media_type));
864 dfile = g_file_new_for_uri (realdest);
865 sfile = g_file_new_for_uri (dest);
866 g_free (realdest);
867
868 task = g_task_new (dfile, NULL, track_copy_cb, g_object_ref (source));
869 g_task_set_task_data (task, sfile, g_object_unref);
870 g_task_run_in_thread (task, copy_track_task);
871 return FALSE;
872 }
873
874
875 static void
impl_selected(RBDisplayPage * page)876 impl_selected (RBDisplayPage *page)
877 {
878 ensure_loaded (RB_ANDROID_SOURCE (page));
879 }
880
881 static void
impl_delete_thyself(RBDisplayPage * page)882 impl_delete_thyself (RBDisplayPage *page)
883 {
884 RBAndroidSourcePrivate *priv = GET_PRIVATE (page);
885
886 if (priv->import_errors != NULL) {
887 rb_display_page_delete_thyself (RB_DISPLAY_PAGE (priv->import_errors));
888 priv->import_errors = NULL;
889 }
890
891 RB_DISPLAY_PAGE_CLASS (rb_android_source_parent_class)->delete_thyself (page);
892 }
893
894 static void
impl_pack_content(RBBrowserSource * source,GtkWidget * content)895 impl_pack_content (RBBrowserSource *source, GtkWidget *content)
896 {
897 RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
898 gtk_grid_attach (GTK_GRID (priv->grid), content, 0, 1, 1, 1);
899 }
900
901 static void
rb_android_source_init(RBAndroidSource * source)902 rb_android_source_init (RBAndroidSource *source)
903 {
904
905 }
906
907 static void
impl_constructed(GObject * object)908 impl_constructed (GObject *object)
909 {
910 RBAndroidSource *source;
911 RBAndroidSourcePrivate *priv;
912 RhythmDBEntryType *entry_type;
913 RBShell *shell;
914 char **output_formats;
915
916 source = RB_ANDROID_SOURCE (object);
917 priv = GET_PRIVATE (source);
918 priv->grid = gtk_grid_new ();
919
920 RB_CHAIN_GOBJECT_METHOD (rb_android_source_parent_class, constructed, object);
921
922 priv->info_bar = gtk_info_bar_new ();
923 gtk_grid_attach (GTK_GRID (priv->grid), priv->info_bar, 0, 0, 1, 1);
924
925 gtk_container_add (GTK_CONTAINER (source), priv->grid);
926 gtk_widget_show_all (priv->grid);
927 gtk_widget_hide (priv->info_bar);
928
929 rb_device_source_set_display_details (RB_DEVICE_SOURCE (source));
930
931 g_object_get (source,
932 "shell", &shell,
933 "entry-type", &entry_type,
934 NULL);
935
936 g_object_get (shell, "db", &priv->db, NULL);
937
938 priv->import_errors = rb_import_errors_source_new (shell,
939 priv->error_type,
940 entry_type,
941 priv->ignore_type);
942
943 g_object_get (priv->device_info, "output-formats", &output_formats, NULL);
944 if (output_formats != NULL) {
945 GstEncodingTarget *target;
946 int i;
947
948 target = gst_encoding_target_new ("android-device", "device", "", NULL);
949 for (i = 0; output_formats[i] != NULL; i++) {
950 const char *media_type = rb_gst_mime_type_to_media_type (output_formats[i]);
951 if (media_type != NULL) {
952 GstEncodingProfile *profile;
953 profile = rb_gst_get_encoding_profile (media_type);
954 if (profile != NULL) {
955 gst_encoding_target_add_profile (target, profile);
956 }
957 }
958 }
959 g_object_set (source, "encoding-target", target, NULL);
960 }
961 g_strfreev (output_formats);
962
963 g_object_unref (shell);
964 }
965
966 static void
impl_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)967 impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
968 {
969 RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
970
971 switch (prop_id) {
972 case PROP_IGNORE_ENTRY_TYPE:
973 priv->ignore_type = g_value_get_object (value);
974 break;
975 case PROP_ERROR_ENTRY_TYPE:
976 priv->error_type = g_value_get_object (value);
977 break;
978 case PROP_DEVICE_INFO:
979 priv->device_info = g_value_dup_object (value);
980 break;
981 case PROP_VOLUME:
982 priv->volume = g_value_dup_object (value);
983 break;
984 case PROP_MOUNT_ROOT:
985 priv->mount_root = g_value_dup_object (value);
986 break;
987 case PROP_GUDEV_DEVICE:
988 priv->gudev_device = g_value_dup_object (value);
989 break;
990 default:
991 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
992 break;
993 }
994 }
995
996 static void
impl_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)997 impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
998 {
999 RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
1000
1001 switch (prop_id) {
1002 case PROP_IGNORE_ENTRY_TYPE:
1003 g_value_set_object (value, priv->ignore_type);
1004 break;
1005 case PROP_ERROR_ENTRY_TYPE:
1006 g_value_set_object (value, priv->error_type);
1007 break;
1008 case PROP_DEVICE_INFO:
1009 g_value_set_object (value, priv->device_info);
1010 break;
1011 case PROP_VOLUME:
1012 g_value_set_object (value, priv->volume);
1013 break;
1014 case PROP_MOUNT_ROOT:
1015 g_value_set_object (value, priv->mount_root);
1016 break;
1017 case PROP_GUDEV_DEVICE:
1018 g_value_set_object (value, priv->gudev_device);
1019 break;
1020 case PROP_DEVICE_SERIAL:
1021 g_value_set_string (value, g_udev_device_get_property (priv->gudev_device, "ID_SERIAL"));
1022 break;
1023 default:
1024 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
1025 break;
1026 }
1027 }
1028
1029 static void
impl_dispose(GObject * object)1030 impl_dispose (GObject *object)
1031 {
1032 RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
1033
1034 if (priv->db != NULL) {
1035 if (priv->ignore_type != NULL) {
1036 rhythmdb_entry_delete_by_type (priv->db, priv->ignore_type);
1037 g_clear_object (&priv->ignore_type);
1038 }
1039 if (priv->error_type != NULL) {
1040 rhythmdb_entry_delete_by_type (priv->db, priv->error_type);
1041 g_clear_object (&priv->error_type);
1042 }
1043
1044 g_clear_object (&priv->db);
1045 }
1046
1047 if (priv->import_job != NULL) {
1048 rhythmdb_import_job_cancel (priv->import_job);
1049 g_clear_object (&priv->import_job);
1050 }
1051 if (priv->rescan_id != 0) {
1052 g_source_remove (priv->rescan_id);
1053 }
1054
1055 g_clear_object (&priv->device_info);
1056 g_clear_object (&priv->volume);
1057 g_clear_object (&priv->mount_root);
1058 g_clear_object (&priv->gudev_device);
1059
1060 G_OBJECT_CLASS (rb_android_source_parent_class)->dispose (object);
1061 }
1062
1063 static void
impl_finalize(GObject * object)1064 impl_finalize (GObject *object)
1065 {
1066 RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
1067
1068 g_list_free_full (priv->storage, g_object_unref);
1069
1070 G_OBJECT_CLASS (rb_android_source_parent_class)->finalize (object);
1071 }
1072
1073
1074 static void
rb_android_device_source_init(RBDeviceSourceInterface * interface)1075 rb_android_device_source_init (RBDeviceSourceInterface *interface)
1076 {
1077 interface->eject = impl_eject;
1078 }
1079
1080 static void
rb_android_transfer_target_init(RBTransferTargetInterface * interface)1081 rb_android_transfer_target_init (RBTransferTargetInterface *interface)
1082 {
1083 interface->build_dest_uri = impl_build_dest_uri;
1084 interface->track_added = impl_track_added;
1085 }
1086
1087 static void
rb_android_source_class_init(RBAndroidSourceClass * klass)1088 rb_android_source_class_init (RBAndroidSourceClass *klass)
1089 {
1090 GObjectClass *object_class = G_OBJECT_CLASS (klass);
1091 RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
1092 RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
1093 RBBrowserSourceClass *browser_class = RB_BROWSER_SOURCE_CLASS (klass);
1094 RBMediaPlayerSourceClass *mps_class = RB_MEDIA_PLAYER_SOURCE_CLASS (klass);
1095
1096 object_class->set_property = impl_set_property;
1097 object_class->get_property = impl_get_property;
1098 object_class->constructed = impl_constructed;
1099 object_class->dispose = impl_dispose;
1100 object_class->finalize = impl_finalize;
1101
1102 page_class->delete_thyself = impl_delete_thyself;
1103 page_class->selected = impl_selected;
1104
1105 browser_class->pack_content = impl_pack_content;
1106
1107 source_class->can_delete = impl_can_delete;
1108 source_class->delete_selected = impl_delete_selected;
1109 source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_false_function;
1110 source_class->can_paste = impl_can_paste;
1111 source_class->paste = impl_paste;
1112 source_class->want_uri = rb_device_source_want_uri;
1113 source_class->uri_is_source = rb_device_source_uri_is_source;
1114
1115 mps_class->get_entries = impl_get_entries;
1116 mps_class->get_capacity = impl_get_capacity;
1117 mps_class->get_free_space = impl_get_free_space;
1118 mps_class->delete_entries = impl_delete_entries;
1119 mps_class->show_properties = impl_show_properties;
1120
1121 g_object_class_install_property (object_class,
1122 PROP_ERROR_ENTRY_TYPE,
1123 g_param_spec_object ("error-entry-type",
1124 "Error entry type",
1125 "Entry type to use for import error entries added by this source",
1126 RHYTHMDB_TYPE_ENTRY_TYPE,
1127 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1128 g_object_class_install_property (object_class,
1129 PROP_IGNORE_ENTRY_TYPE,
1130 g_param_spec_object ("ignore-entry-type",
1131 "Ignore entry type",
1132 "Entry type to use for ignore entries added by this source",
1133 RHYTHMDB_TYPE_ENTRY_TYPE,
1134 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1135 g_object_class_install_property (object_class,
1136 PROP_DEVICE_INFO,
1137 g_param_spec_object ("device-info",
1138 "device info",
1139 "device information object",
1140 MPID_TYPE_DEVICE,
1141 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1142 g_object_class_install_property (object_class,
1143 PROP_VOLUME,
1144 g_param_spec_object ("volume",
1145 "volume",
1146 "GVolume object",
1147 G_TYPE_VOLUME,
1148 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1149 g_object_class_install_property (object_class,
1150 PROP_MOUNT_ROOT,
1151 g_param_spec_object ("mount-root",
1152 "mount root",
1153 "Mount root",
1154 G_TYPE_OBJECT,
1155 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1156
1157 g_object_class_install_property (object_class,
1158 PROP_GUDEV_DEVICE,
1159 g_param_spec_object ("gudev-device",
1160 "gudev-device",
1161 "GUdev device object",
1162 G_UDEV_TYPE_DEVICE,
1163 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
1164
1165 g_object_class_override_property (object_class, PROP_DEVICE_SERIAL, "serial");
1166
1167 g_type_class_add_private (klass, sizeof (RBAndroidSourcePrivate));
1168 }
1169
1170 static void
rb_android_source_class_finalize(RBAndroidSourceClass * klass)1171 rb_android_source_class_finalize (RBAndroidSourceClass *klass)
1172 {
1173 }
1174
1175 void
_rb_android_source_register_type(GTypeModule * module)1176 _rb_android_source_register_type (GTypeModule *module)
1177 {
1178 rb_android_source_register_type (module);
1179 }
1180
1181